This is an automated email from the ASF dual-hosted git repository. potiuk pushed a commit to branch airflow-modifications in repository https://gitbox.apache.org/repos/asf/airflow-openldap.git
commit 9b18b3b205d7c19786d529a7428b3be452ef3add Author: Jarek Potiuk <[email protected]> AuthorDate: Sat Jul 4 15:52:23 2020 +0200 Add Airflow-specific modifications - image can be build on its own It uses only officially released code. Part of https://github.com/apache/airflow/issues/9401 --- .idea/.gitignore | 8 + .idea/docker-openldap.iml | 9 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + build_and_push.sh | 38 + image/Dockerfile | 39 +- image/base-image/Dockerfile | 10 + image/base-image/build.sh | 62 ++ image/base-image/file/dpkg_nodoc | 9 + image/base-image/file/dpkg_nolocales | 2 + .../base-image/service-available/:cron/download.sh | 4 + .../base-image/service-available/:cron/install.sh | 14 + .../base-image/service-available/:cron/process.sh | 4 + .../base-image/service-available/:cron/startup.sh | 12 + .../:logrotate/assets/config/logrotate.conf | 36 + .../:logrotate/assets/config/logrotate_syslogng | 39 + .../service-available/:logrotate/download.sh | 4 + .../service-available/:logrotate/install.sh | 4 + .../service-available/:logrotate/startup.sh | 6 + .../service-available/:runit/download.sh | 4 + .../:ssl-tools/assets/cfssl-default-env | 44 + .../:ssl-tools/assets/default-ca/README.md | 2 + .../assets/default-ca/config/ca-config.json | 13 + .../assets/default-ca/config/ca-csr.json | 16 + .../assets/default-ca/config/req-csr.json.tmpl | 19 + .../assets/default-ca/default-ca-key.pem | 6 + .../:ssl-tools/assets/default-ca/default-ca.csr | 11 + .../:ssl-tools/assets/default-ca/default-ca.pem | 18 + .../:ssl-tools/assets/default-env | 10 + .../:ssl-tools/assets/jsonssl-default-env | 10 + .../:ssl-tools/assets/tool/cfssl-helper | 238 ++++++ .../:ssl-tools/assets/tool/jsonssl-helper | 122 +++ .../:ssl-tools/assets/tool/ssl-auto-renew | 152 ++++ .../:ssl-tools/assets/tool/ssl-helper | 100 +++ .../service-available/:ssl-tools/download.sh | 69 ++ .../service-available/:ssl-tools/startup.sh | 5 + .../:syslog-ng-core/assets/config/syslog-ng.conf | 152 ++++ .../assets/config/syslog_ng_default | 12 + .../service-available/:syslog-ng-core/download.sh | 4 + .../service-available/:syslog-ng-core/install.sh | 8 + .../service-available/:syslog-ng-core/process.sh | 9 + .../service-available/:syslog-ng-core/startup.sh | 22 + image/base-image/tool/add-multiple-process-stack | 4 + image/base-image/tool/add-service-available | 30 + image/base-image/tool/complex-bash-env | 91 ++ image/base-image/tool/install-service | 41 + image/base-image/tool/log-helper | 121 +++ image/base-image/tool/run | 930 +++++++++++++++++++++ image/base-image/tool/setuser | 64 ++ image/base-image/tool/wait-process | 16 + 51 files changed, 2660 insertions(+), 3 deletions(-) diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/docker-openldap.iml b/.idea/docker-openldap.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/docker-openldap.iml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3a37236 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager" version="2" languageLevel="JDK_14" project-jdk-name="14" project-jdk-type="JavaSDK"> + <output url="file://$PROJECT_DIR$/out" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2b3ab4d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/docker-openldap.iml" filepath="$PROJECT_DIR$/.idea/docker-openldap.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/build_and_push.sh b/build_and_push.sh new file mode 100755 index 0000000..cd1ef52 --- /dev/null +++ b/build_and_push.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +set -euo pipefail +DOCKERHUB_USER=${DOCKERHUB_USER:="apache"} +DOCKERHUB_REPO=${DOCKERHUB_REPO:="airflow"} +OPENLDAP_VERSION="2.4.50" +AIRFLOW_OPENLDAP_VERSION="2020.07.10" +COMMIT_SHA=$(git rev-parse HEAD) + +cd "$( dirname "${BASH_SOURCE[0]}" )" || exit 1 + +TAG="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:openldap-${AIRFLOW_OPENLDAP_VERSION}-${OPENLDAP_VERSION}" + +cd image + +docker build . \ + --pull \ + --build-arg "OPENLDAP_VERSION=${OPENLDAP_VERSION}" \ + --build-arg "AIRFLOW_OPENLDAP_VERSION=${AIRFLOW_OPENLDAP_VERSION}" \ + --build-arg "COMMIT_SHA=${COMMIT_SHA}" \ + --tag "${TAG}" + +docker push "${TAG}" diff --git a/image/Dockerfile b/image/Dockerfile index cfac93e..fc91ae0 100644 --- a/image/Dockerfile +++ b/image/Dockerfile @@ -1,6 +1,28 @@ -# Use osixia/light-baseimage -# sources: https://github.com/osixia/docker-light-baseimage -FROM osixia/light-baseimage:1.2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +FROM debian:buster-slim + +COPY base-image /container +RUN /container/build.sh + +ARG AIRFLOW_OPENLDAP_VERSION +ARG OPENLDAP_VERSION +ARG COMMIT_SHA ARG LDAP_OPENLDAP_GID ARG LDAP_OPENLDAP_UID @@ -8,6 +30,17 @@ ARG LDAP_OPENLDAP_UID ARG PQCHECKER_VERSION=2.0.0 ARG PQCHECKER_MD5=c005ce596e97d13e39485e711dcbc7e1 +LABEL org.apache.airflow.component="apache-rat" +LABEL org.apache.airflow.apache_rat.version="${OPENLDAP_VERSION}" +LABEL org.apache.airflow.airflow_apache_rat.version="${AIRFLOW_OPENLDAP_VERSION}" +LABEL org.apache.airflow.commit_sha="${COMMIT_SHA}" + +MAINTAINER "Apache Airflow Community <[email protected]>" + +ENV LANG="en_US.UTF-8" \ + LANGUAGE="en_US:en" \ + LC_ALL="en_US.UTF-8" + # Add openldap user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added # If explicit uid or gid is given, use it. RUN if [ -z "${LDAP_OPENLDAP_GID}" ]; then groupadd -g 911 -r openldap; else groupadd -r -g ${LDAP_OPENLDAP_GID} openldap; fi \ diff --git a/image/base-image/Dockerfile b/image/base-image/Dockerfile new file mode 100644 index 0000000..be4b87b --- /dev/null +++ b/image/base-image/Dockerfile @@ -0,0 +1,10 @@ +FROM debian:buster-slim + +COPY . /container +RUN /container/build.sh + +ENV LANG="en_US.UTF-8" \ + LANGUAGE="en_US:en" \ + LC_ALL="en_US.UTF-8" + +ENTRYPOINT ["/container/tool/run"] diff --git a/image/base-image/build.sh b/image/base-image/build.sh new file mode 100755 index 0000000..342444e --- /dev/null +++ b/image/base-image/build.sh @@ -0,0 +1,62 @@ +#!/bin/sh -ex + +## Add bash tools to /sbin +ln -s /container/tool/* /sbin/ + +mkdir -p /container/service +mkdir -p /container/environment /container/environment/startup +chmod 700 /container/environment/ /container/environment/startup + +groupadd -g 8377 docker_env + +# dpkg options +cp /container/file/dpkg_nodoc /etc/dpkg/dpkg.cfg.d/01_nodoc +cp /container/file/dpkg_nolocales /etc/dpkg/dpkg.cfg.d/01_nolocales + +# General config +export LC_ALL=C +export DEBIAN_FRONTEND=noninteractive +MINIMAL_APT_GET_INSTALL='apt-get install -y --no-install-recommends' + +## Prevent initramfs updates from trying to run grub and lilo. +## https://journal.paul.querna.org/articles/2013/10/15/docker-ubuntu-on-rackspace/ +## http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=594189 +export INITRD=no +printf no > /container/environment/INITRD + +apt-get update + +## Fix some issues with APT packages. +## See https://github.com/dotcloud/docker/issues/1024 +dpkg-divert --local --rename --add /sbin/initctl +ln -sf /bin/true /sbin/initctl + +## Replace the 'ischroot' tool to make it always return true. +## Prevent initscripts updates from breaking /dev/shm. +## https://journal.paul.querna.org/articles/2013/10/15/docker-ubuntu-on-rackspace/ +## https://bugs.launchpad.net/launchpad/+bug/974584 +dpkg-divert --local --rename --add /usr/bin/ischroot +ln -sf /bin/true /usr/bin/ischroot + +## Install apt-utils. +$MINIMAL_APT_GET_INSTALL apt-utils apt-transport-https ca-certificates locales procps dirmngr gnupg iproute2 python3-minimal python3-yaml + +## Upgrade all packages. +apt-get dist-upgrade -y --no-install-recommends -o Dpkg::Options::="--force-confold" + +# fix locale +echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen +locale-gen en_US +update-locale LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 + +printf en_US.UTF-8 > /container/environment/LANG +printf en_US.UTF-8 > /container/environment/LANGUAGE +printf en_US.UTF-8 > /container/environment/LC_CTYPE + +apt-get clean +rm -rf /tmp/* /var/tmp/* +rm -rf /var/lib/apt/lists/* + +# Remove useless files +rm -rf /container/file +rm -rf /container/build.sh /container/Dockerfile diff --git a/image/base-image/file/dpkg_nodoc b/image/base-image/file/dpkg_nodoc new file mode 100644 index 0000000..7320020 --- /dev/null +++ b/image/base-image/file/dpkg_nodoc @@ -0,0 +1,9 @@ +path-exclude /usr/share/doc/* +# we need to keep copyright files for legal reasons +path-include /usr/share/doc/*/copyright +path-exclude /usr/share/man/* +path-exclude /usr/share/groff/* +path-exclude /usr/share/info/* +# lintian stuff is small, but really unnecessary +path-exclude /usr/share/lintian/* +path-exclude /usr/share/linda/* diff --git a/image/base-image/file/dpkg_nolocales b/image/base-image/file/dpkg_nolocales new file mode 100644 index 0000000..384dc19 --- /dev/null +++ b/image/base-image/file/dpkg_nolocales @@ -0,0 +1,2 @@ +path-exclude /usr/share/locale/* +path-include /usr/share/locale/en* diff --git a/image/base-image/service-available/:cron/download.sh b/image/base-image/service-available/:cron/download.sh new file mode 100755 index 0000000..b4f814a --- /dev/null +++ b/image/base-image/service-available/:cron/download.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e + +# download cron from apt-get +LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cron diff --git a/image/base-image/service-available/:cron/install.sh b/image/base-image/service-available/:cron/install.sh new file mode 100755 index 0000000..ece2191 --- /dev/null +++ b/image/base-image/service-available/:cron/install.sh @@ -0,0 +1,14 @@ +#!/bin/sh -e + +chmod 600 /etc/crontab + +# Fix https://github.com/phusion/baseimage-docker/issues/345 +sed -i 's/^\s*session\s\+required\s\+pam_loginuid.so/# &/' /etc/pam.d/cron + +## Remove useless cron entries. +# Checks for lost+found and scans for mtab. +rm -f /etc/cron.daily/standard +rm -f /etc/cron.daily/upstart +rm -f /etc/cron.daily/dpkg +rm -f /etc/cron.daily/password +rm -f /etc/cron.weekly/fstrim diff --git a/image/base-image/service-available/:cron/process.sh b/image/base-image/service-available/:cron/process.sh new file mode 100755 index 0000000..6b4d633 --- /dev/null +++ b/image/base-image/service-available/:cron/process.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e +log-helper level eq trace && set -x + +exec /usr/sbin/cron -f diff --git a/image/base-image/service-available/:cron/startup.sh b/image/base-image/service-available/:cron/startup.sh new file mode 100755 index 0000000..5f79925 --- /dev/null +++ b/image/base-image/service-available/:cron/startup.sh @@ -0,0 +1,12 @@ +#!/bin/sh -e +log-helper level eq trace && set -x + +# prevent NUMBER OF HARD LINKS > 1 error +# https://github.com/phusion/baseimage-docker/issues/198 +touch /etc/crontab /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.monthly /etc/cron.weekly + +find /etc/cron.d/ -exec touch {} \; +find /etc/cron.daily/ -exec touch {} \; +find /etc/cron.hourly/ -exec touch {} \; +find /etc/cron.monthly/ -exec touch {} \; +find /etc/cron.weekly/ -exec touch {} \; diff --git a/image/base-image/service-available/:logrotate/assets/config/logrotate.conf b/image/base-image/service-available/:logrotate/assets/config/logrotate.conf new file mode 100644 index 0000000..cb2e78c --- /dev/null +++ b/image/base-image/service-available/:logrotate/assets/config/logrotate.conf @@ -0,0 +1,36 @@ +# see "man logrotate" for details +# rotate log files weekly +weekly + +# use the syslog group by default, since this is the owning group +# of /var/log/syslog. +# su root syslog + +# keep 4 weeks worth of backlogs +rotate 4 + +# create new (empty) log files after rotating old ones +create + +# uncomment this if you want your log files compressed +#compress + +# packages drop log rotation information into this directory +include /etc/logrotate.d + +# no packages own wtmp, or btmp -- we'll rotate them here +/var/log/wtmp { + missingok + monthly + create 0664 root utmp + rotate 1 +} + +/var/log/btmp { + missingok + monthly + create 0660 root utmp + rotate 1 +} + +# system-specific logs may be configured here diff --git a/image/base-image/service-available/:logrotate/assets/config/logrotate_syslogng b/image/base-image/service-available/:logrotate/assets/config/logrotate_syslogng new file mode 100644 index 0000000..93d6b02 --- /dev/null +++ b/image/base-image/service-available/:logrotate/assets/config/logrotate_syslogng @@ -0,0 +1,39 @@ +/var/log/syslog { + rotate 7 + daily + missingok + notifempty + delaycompress + compress + postrotate + if [ -f /var/run/syslog-ng.pid ]; then + kill -HUP `cat /var/run/syslog-ng.pid` + fi + endscript +} + +/var/log/mail.info +/var/log/mail.warn +/var/log/mail.err +/var/log/mail.log +/var/log/daemon.log +/var/log/kern.log +/var/log/auth.log +/var/log/user.log +/var/log/lpr.log +/var/log/cron.log +/var/log/debug +/var/log/messages { + rotate 4 + weekly + missingok + notifempty + compress + delaycompress + sharedscripts + postrotate + if [ -f /var/run/syslog-ng.pid ]; then + kill -HUP `cat /var/run/syslog-ng.pid` + fi + endscript +} \ No newline at end of file diff --git a/image/base-image/service-available/:logrotate/download.sh b/image/base-image/service-available/:logrotate/download.sh new file mode 100755 index 0000000..155c7ab --- /dev/null +++ b/image/base-image/service-available/:logrotate/download.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e + +# download logrotate from apt-get +LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends logrotate diff --git a/image/base-image/service-available/:logrotate/install.sh b/image/base-image/service-available/:logrotate/install.sh new file mode 100755 index 0000000..5323d03 --- /dev/null +++ b/image/base-image/service-available/:logrotate/install.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e + +rm -f /etc/logrotate.conf +rm -f /etc/logrotate.d/syslog-ng diff --git a/image/base-image/service-available/:logrotate/startup.sh b/image/base-image/service-available/:logrotate/startup.sh new file mode 100755 index 0000000..2d2d122 --- /dev/null +++ b/image/base-image/service-available/:logrotate/startup.sh @@ -0,0 +1,6 @@ +#!/bin/sh -e +log-helper level eq trace && set -x +ln -sf "${CONTAINER_SERVICE_DIR}/:logrotate/assets/config/logrotate.conf" /etc/logrotate.conf +ln -sf "${CONTAINER_SERVICE_DIR}/:logrotate/assets/config/logrotate_syslogng" /etc/logrotate.d/syslog-ng + +chmod 444 -R "${CONTAINER_SERVICE_DIR}"/:logrotate/assets/config/* diff --git a/image/base-image/service-available/:runit/download.sh b/image/base-image/service-available/:runit/download.sh new file mode 100755 index 0000000..e1d66a2 --- /dev/null +++ b/image/base-image/service-available/:runit/download.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e + +# download runit from apt-get +LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends runit diff --git a/image/base-image/service-available/:ssl-tools/assets/cfssl-default-env b/image/base-image/service-available/:ssl-tools/assets/cfssl-default-env new file mode 100644 index 0000000..ed8d49f --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/cfssl-default-env @@ -0,0 +1,44 @@ +#!/bin/bash +# +# Default CA config +# +CFSSL_DEFAULT_CACERT="${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/default-ca/default-ca.pem" +CFSSL_DEFAULT_CA_KEY="${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/default-ca/default-ca-key.pem" +CFSSL_DEFAULT_CA_CONFIG="${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/default-ca/config/ca-config.json" +CFSSL_DEFAULT_CSR="${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/default-ca/config/req-csr.json.tmpl" + +# default csr file params +CFSSL_DEFAULT_CA_CSR_CN=${CFSSL_DEFAULT_CA_CSR_CN:-${HOSTNAME}} + +CFSSL_DEFAULT_CA_CSR_KEY_ALGO=${CFSSL_DEFAULT_CA_CSR_KEY_ALGO:-"ecdsa"} +CFSSL_DEFAULT_CA_CSR_KEY_SIZE=${CFSSL_DEFAULT_CA_CSR_KEY_SIZE:-384} + +CFSSL_DEFAULT_CA_CSR_ORGANIZATION=${CFSSL_DEFAULT_CA_CSR_ORGANIZATION:-"A1A Car Wash"} +CFSSL_DEFAULT_CA_CSR_ORGANIZATION_UNIT=${CFSSL_DEFAULT_CA_CSR_ORGANIZATION_UNIT:-"Information Technology Dep."} +CFSSL_DEFAULT_CA_CSR_LOCATION=${CFSSL_DEFAULT_CA_CSR_LOCATION:-"Albuquerque"} +CFSSL_DEFAULT_CA_CSR_STATE=${CFSSL_DEFAULT_CA_CSR_STATE:-"New Mexico"} +CFSSL_DEFAULT_CA_CSR_COUNTRY=${CFSSL_DEFAULT_CA_CSR_COUNTRY:-"US"} + +# +# General CFSSL config +# + +CFSSL_RETRY=${CFSSL_RETRY:-3} +CFSSL_RETRY_DELAY=${CFSSL_RETRY_DELAY:-1} + +# remote config +CFSSL_REMOTE=${CFSSL_REMOTE:-} +CFSSL_REMOTE_HTTPS_CA_CERT=${CFSSL_REMOTE_HTTPS_CA_CERT:-} + +# local config +CFSSL_CA_CERT=${CFSSL_CA_CERT:-${CFSSL_DEFAULT_CACERT}} +CFSSL_CA_KEY=${CFSSL_CA_KEY:-${CFSSL_DEFAULT_CA_KEY}} + +# gencert +CFSSL_CSR=${CFSSL_CSR:-${CFSSL_DEFAULT_CSR}} +CFSSL_CSR_JSON=${CFSSL_CSR_JSON:-} +CFSSL_CONFIG=${CFSSL_CONFIG:-${CFSSL_CA_CONFIG}} +CFSSL_CONFIG_JSON=${CFSSL_CONFIG_JSON:-${CFSSL_CA_CONFIG_JSON}} +CFSSL_HOSTNAME=${CFSSL_HOSTNAME:-${HOSTNAME}} +CFSSL_PROFILE=${CFSSL_PROFILE:-} +CFSSL_LABEL=${CFSSL_LABEL:-} diff --git a/image/base-image/service-available/:ssl-tools/assets/default-ca/README.md b/image/base-image/service-available/:ssl-tools/assets/default-ca/README.md new file mode 100644 index 0000000..5c4f202 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/default-ca/README.md @@ -0,0 +1,2 @@ +# How to generate the default CA: +cfssl gencert -initca config/ca-csr.json | cfssljson -bare default-ca diff --git a/image/base-image/service-available/:ssl-tools/assets/default-ca/config/ca-config.json b/image/base-image/service-available/:ssl-tools/assets/default-ca/config/ca-config.json new file mode 100644 index 0000000..e492de1 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/default-ca/config/ca-config.json @@ -0,0 +1,13 @@ +{ + "signing": { + "default": { + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ], + "expiry": "8760h" + } + } +} diff --git a/image/base-image/service-available/:ssl-tools/assets/default-ca/config/ca-csr.json b/image/base-image/service-available/:ssl-tools/assets/default-ca/config/ca-csr.json new file mode 100644 index 0000000..d9fb6d3 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/default-ca/config/ca-csr.json @@ -0,0 +1,16 @@ +{ + "CN": "docker-light-baseimage", + "key": { + "algo": "ecdsa", + "size": 384 + }, + "names": [ + { + "O": "A1A Car Wash", + "OU": "Information Technology Dep.", + "L": "Albuquerque", + "ST": "New Mexico", + "C": "US" + } + ] +} diff --git a/image/base-image/service-available/:ssl-tools/assets/default-ca/config/req-csr.json.tmpl b/image/base-image/service-available/:ssl-tools/assets/default-ca/config/req-csr.json.tmpl new file mode 100644 index 0000000..d9f4545 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/default-ca/config/req-csr.json.tmpl @@ -0,0 +1,19 @@ +{ + "CN": "{{ CFSSL_DEFAULT_CA_CSR_CN }}", + "hosts": [ + "{{ CFSSL_DEFAULT_CA_CSR_CN }}" + ], + "key": { + "algo": "{{ CFSSL_DEFAULT_CA_CSR_KEY_ALGO }}", + "size": {{ CFSSL_DEFAULT_CA_CSR_KEY_SIZE }} + }, + "names": [ + { + "O": "{{ CFSSL_DEFAULT_CA_CSR_ORGANIZATION }}", + "OU": "{{ CFSSL_DEFAULT_CA_CSR_ORGANIZATION_UNIT }}", + "L": "{{ CFSSL_DEFAULT_CA_CSR_LOCATION }}", + "ST": "{{ CFSSL_DEFAULT_CA_CSR_STATE }}", + "C": "{{ CFSSL_DEFAULT_CA_CSR_COUNTRY }}" + } + ] +} diff --git a/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca-key.pem b/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca-key.pem new file mode 100644 index 0000000..cffc6ed --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca-key.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDC/DGTr5mqDp1hUVqDKmoc8r13ziQ0Gfx20YCmZLR8LNamT7+1y6eSk +CBTZxWiGLcSgBwYFK4EEACKhZANiAATGX/9dqbqQIJfEpvo/BpozXjW0hQGVlqE5 +iL39jLuC1jx8uG05XZEIB1GwaU98Vs/H9JQf67u+fegh7BqC9gNvIcbnJauYW1Md +cqyLd2ySGN07ol9uRxk3upBgiTwdWi0= +-----END EC PRIVATE KEY----- diff --git a/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca.csr b/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca.csr new file mode 100644 index 0000000..2de0013 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca.csr @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBkDCCARYCAQAwgZYxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxBMUEgQ2FyIFdh +c2gxJDAiBgNVBAsTG0luZm9ybWF0aW9uIFRlY2hub2xvZ3kgRGVwLjEUMBIGA1UE +BxMLQWxidXF1ZXJxdWUxEzARBgNVBAgTCk5ldyBNZXhpY28xHzAdBgNVBAMTFmRv +Y2tlci1saWdodC1iYXNlaW1hZ2UwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATGX/9d +qbqQIJfEpvo/BpozXjW0hQGVlqE5iL39jLuC1jx8uG05XZEIB1GwaU98Vs/H9JQf +67u+fegh7BqC9gNvIcbnJauYW1MdcqyLd2ySGN07ol9uRxk3upBgiTwdWi2gADAK +BggqhkjOPQQDAwNoADBlAjBiYclv+pS3gnE5p1nGf00IqcJeEK38SuOoAbUrwmNd +eKyYHiaAnR/XDq/ceD9qMfgCMQCyBTfO6Jy8wzrUSUMDA2CA707I7+rz6iHc+F9T +EYf0QEb/FzOj1Mt5LpdKqUnL0gg= +-----END CERTIFICATE REQUEST----- diff --git a/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca.pem b/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca.pem new file mode 100644 index 0000000..ab543a3 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/default-ca/default-ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC0zCCAlmgAwIBAgIUCfQ+m0pgZ/BjYAJvxrn/bdGNZokwCgYIKoZIzj0EAwMw +gZYxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxBMUEgQ2FyIFdhc2gxJDAiBgNVBAsT +G0luZm9ybWF0aW9uIFRlY2hub2xvZ3kgRGVwLjEUMBIGA1UEBxMLQWxidXF1ZXJx +dWUxEzARBgNVBAgTCk5ldyBNZXhpY28xHzAdBgNVBAMTFmRvY2tlci1saWdodC1i +YXNlaW1hZ2UwHhcNMTUxMjIzMTM1MzAwWhcNMjAxMjIxMTM1MzAwWjCBljELMAkG +A1UEBhMCVVMxFTATBgNVBAoTDEExQSBDYXIgV2FzaDEkMCIGA1UECxMbSW5mb3Jt +YXRpb24gVGVjaG5vbG9neSBEZXAuMRQwEgYDVQQHEwtBbGJ1cXVlcnF1ZTETMBEG +A1UECBMKTmV3IE1leGljbzEfMB0GA1UEAxMWZG9ja2VyLWxpZ2h0LWJhc2VpbWFn +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMZf/12pupAgl8Sm+j8GmjNeNbSFAZWW +oTmIvf2Mu4LWPHy4bTldkQgHUbBpT3xWz8f0lB/ru7596CHsGoL2A28hxuclq5hb +Ux1yrIt3bJIY3TuiX25HGTe6kGCJPB1aLaNmMGQwDgYDVR0PAQH/BAQDAgEGMBIG +A1UdEwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYEFE+l6XolXDAYnGLTl4W6ULKHrm74 +MB8GA1UdIwQYMBaAFE+l6XolXDAYnGLTl4W6ULKHrm74MAoGCCqGSM49BAMDA2gA +MGUCMQCXLZj8okyxW6UTL7hribUUbu63PbjuwIXnwi420DdNsvA9A7fcQEXScWFL +XAGC8rkCMGcqwXZPSRfwuI9r+R11gTrP92hnaVxs9sjRikctpkQpOyNlIXFPopFK +8FdfWPypvA== +-----END CERTIFICATE----- diff --git a/image/base-image/service-available/:ssl-tools/assets/default-env b/image/base-image/service-available/:ssl-tools/assets/default-env new file mode 100644 index 0000000..96e5380 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/default-env @@ -0,0 +1,10 @@ +#!/bin/bash +SSL_HELPER_TOOL=${SSL_HELPER_TOOL:-"cfssl-helper"} + +SSL_HELPER_AUTO_RENEW=${SSL_HELPER_AUTO_RENEW:-false} +SSL_HELPER_AUTO_RENEW_CRON_EXP=${SSL_HELPER_AUTO_RENEW_CRON_EXP:-"0 0 * * *"} # every day at 00:00 +SSL_HELPER_AUTO_RENEW_SERVICES_IMPACTED=${SSL_HELPER_AUTO_RENEW_SERVICES_IMPACTED:-} +SSL_HELPER_AUTO_RENEW_FROM_FILES=${SSL_HELPER_AUTO_RENEW_FROM_FILES:-false} +SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE=${SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE:-} +SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE=${SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE:-} +SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE=${SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE:-} diff --git a/image/base-image/service-available/:ssl-tools/assets/jsonssl-default-env b/image/base-image/service-available/:ssl-tools/assets/jsonssl-default-env new file mode 100644 index 0000000..0aa00df --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/jsonssl-default-env @@ -0,0 +1,10 @@ +#!/bin/bash +JSONSSL_FILE_DEFAULT="${CONTAINER_SERVICE_DIR}/ssl-tools/assets/certs/certs.json" + +JSONSSL_FILE=${JSONSSL_FILE:-} # don't set default immediatly because we print a warning in jsonssl-helper +JSONSSL_HOSTNAME=${JSONSSL_HOSTNAME:-${HOSTNAME}} +JSONSSL_PROFILE=${JSONSSL_PROFILE:-} # traefik / traefik_up_to_v1_6 + +JSONSSL_GET_CA_CERT_CMD=${JSONSSL_GET_CA_CERT_CMD:-} +JSONSSL_GET_CERT_CMD=${JSONSSL_GET_CERT_CMD:-} +JSONSSL_GET_KEY_CMD=${JSONSSL_GET_KEY_CMD:-} diff --git a/image/base-image/service-available/:ssl-tools/assets/tool/cfssl-helper b/image/base-image/service-available/:ssl-tools/assets/tool/cfssl-helper new file mode 100755 index 0000000..9e14422 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/tool/cfssl-helper @@ -0,0 +1,238 @@ +#!/bin/bash +log-helper level eq trace && set -x + +# This tool helps to generate tls certificates with cfssl +# It takes cfssl configuration from environment variable. +# See cfssl-default-env file + +PREFIX=$1 +CERT_FILE=$2 +KEY_FILE=$3 +CA_FILE=$4 + +log-helper debug "cfssl-helper is launched, everybody on the floor!" + +# before 0.2.5 retro compatibility, will be removed. +mkdir -p "${CONTAINER_SERVICE_DIR}/:cfssl/assets/default-ca" +ln -sf "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/default-ca/default-ca.pem" "${CONTAINER_SERVICE_DIR}/:cfssl/assets/default-ca/default-ca.pem" + +if [ -z "${PREFIX}" ] || [ -z "${CERT_FILE}" ] || [ -z "${KEY_FILE}" ] || [ -z "${CA_FILE}" ]; then + log-helper error "Usage: cfssl-helper prefix cert_file key_file ca_file" + exit 1 +fi + +if [ ! -e "${CERT_FILE}" ] && [ ! -e "${KEY_FILE}" ]; then + + log-helper info "No certificate file and certificate key provided, generate:" + log-helper info "${CERT_FILE} and ${KEY_FILE}" + + LOG_LEVEL_PARAM="" + + case ${CONTAINER_LOG_LEVEL} in + 0 ) + LOG_LEVEL_PARAM="-loglevel 4";; + 1 ) + LOG_LEVEL_PARAM="-loglevel 3";; + 2 ) + LOG_LEVEL_PARAM="-loglevel 2";; + 3 ) + LOG_LEVEL_PARAM="-loglevel 1";; + 4 ) + LOG_LEVEL_PARAM="-loglevel 0";; + 5 ) + LOG_LEVEL_PARAM="-loglevel 0";; + esac + + # set env vars + PREFIX=${PREFIX^^} # uppercase + + # search for prefixed env var first + + # set prefix variable name + # example : PREFIX_CFSSL_REMOTE='MARIADB_CFSSL_REMOTE' + PREFIX_CFSSL_REMOTE=${PREFIX}_CFSSL_REMOTE + PREFIX_CFSSL_REMOTE_HTTPS_CA_CERT=${PREFIX}_CFSSL_REMOTE_HTTPS_CA_CERT + PREFIX_CFSSL_CA_CERT=${PREFIX}_CFSSL_CA_CERT + PREFIX_CFSSL_CA_KEY=${PREFIX}_CFSSL_CA_KEY + PREFIX_CFSSL_CSR=${PREFIX}_CFSSL_CSR + PREFIX_CFSSL_CSR_JSON=${PREFIX}_CFSSL_CSR_JSON + PREFIX_CFSSL_CONFIG=${PREFIX}_CFSSL_CONFIG + PREFIX_CFSSL_CONFIG_JSON=${PREFIX}_CFSSL_CONFIG_JSON + PREFIX_CFSSL_HOSTNAME=${PREFIX}_CFSSL_HOSTNAME + PREFIX_CFSSL_PROFILE=${PREFIX}_CFSSL_PROFILE + PREFIX_CFSSL_LABEL=${PREFIX}_CFSSL_LABEL + PREFIX_CFSSL_RETRY=${PREFIX}_CFSSL_RETRY + PREFIX_CFSSL_RETRY_DELAY=${PREFIX}_CFSSL_RETRY_DELAY + + # assign CFSSL_REMOTE=${!PREFIX_CFSSL_REMOTE} if value is not empty otherwise CFSSL_REMOTE=CFSSL_REMOTE + CFSSL_REMOTE=${!PREFIX_CFSSL_REMOTE:-$CFSSL_REMOTE} + CFSSL_REMOTE_HTTPS_CA_CERT=${!PREFIX_CFSSL_REMOTE_HTTPS_CA_CERT:-$CFSSL_REMOTE_HTTPS_CA_CERT} + CFSSL_CA_CERT=${!PREFIX_CFSSL_CA_CERT:-$CFSSL_CA_CERT} + CFSSL_CA_KEY=${!PREFIX_CFSSL_CA_KEY:-$CFSSL_CA_KEY} + CFSSL_CSR=${!PREFIX_CFSSL_CSR:-$CFSSL_CSR} + CFSSL_CSR_JSON=${!PREFIX_CFSSL_CSR_JSON:-$CFSSL_CSR_JSON} + CFSSL_CONFIG=${!PREFIX_CFSSL_CONFIG:-$CFSSL_CONFIG} + CFSSL_CONFIG_JSON=${!PREFIX_CFSSL_CONFIG_JSON:-$CFSSL_CONFIG_JSON} + CFSSL_HOSTNAME=${!PREFIX_CFSSL_HOSTNAME:-$CFSSL_HOSTNAME} + CFSSL_PROFILE=${!PREFIX_CFSSL_PROFILE:-$CFSSL_PROFILE} + CFSSL_LABEL=${!PREFIX_CFSSL_LABEL:-$CFSSL_LABEL} + CFSSL_RETRY=${!PREFIX_CFSSL_RETRY:-$CFSSL_RETRY} + CFSSL_RETRY_DELAY=${!PREFIX_CFSSL_RETRY_DELAY:-$CFSSL_RETRY_DELAY} + + source "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/cfssl-default-env" + + # set csr file + CSR_FILE="/tmp/csr-file" + if [ -n "${CFSSL_CSR_JSON}" ]; then + log-helper debug "use CFSSL_CSR_JSON value as csr file" + echo "${CFSSL_CSR_JSON}" > "${CSR_FILE}" + elif [ -n "${CFSSL_CSR}" ]; then + log-helper debug "use ${CFSSL_CSR} as csr file" + cp -f "${CFSSL_CSR}" "${CSR_FILE}" + + # it's the default csr + if [ "${CFSSL_CSR}" = "${CFSSL_DEFAULT_CSR}" ]; then + sed -i "s|{{ CFSSL_DEFAULT_CA_CSR_CN }}|${CFSSL_DEFAULT_CA_CSR_CN}|g" "${CSR_FILE}" + sed -i "s|{{ CFSSL_DEFAULT_CA_CSR_KEY_ALGO }}|${CFSSL_DEFAULT_CA_CSR_KEY_ALGO}|g" "${CSR_FILE}" + sed -i "s|{{ CFSSL_DEFAULT_CA_CSR_KEY_SIZE }}|${CFSSL_DEFAULT_CA_CSR_KEY_SIZE}|g" "${CSR_FILE}" + sed -i "s|{{ CFSSL_CERT_ORGANIZATION_UNIT }}|${CFSSL_CERT_ORGANIZATION_UNIT}|g" "${CSR_FILE}" + sed -i "s|{{ CFSSL_DEFAULT_CA_CSR_ORGANIZATION }}|${CFSSL_DEFAULT_CA_CSR_ORGANIZATION}|g" "${CSR_FILE}" + sed -i "s|{{ CFSSL_DEFAULT_CA_CSR_ORGANIZATION_UNIT }}|${CFSSL_DEFAULT_CA_CSR_ORGANIZATION_UNIT}|g" "${CSR_FILE}" + sed -i "s|{{ CFSSL_DEFAULT_CA_CSR_LOCATION }}|${CFSSL_DEFAULT_CA_CSR_LOCATION}|g" "${CSR_FILE}" + sed -i "s|{{ CFSSL_DEFAULT_CA_CSR_STATE }}|${CFSSL_DEFAULT_CA_CSR_STATE}|g" "${CSR_FILE}" + sed -i "s|{{ CFSSL_DEFAULT_CA_CSR_COUNTRY }}|${CFSSL_DEFAULT_CA_CSR_COUNTRY}|g" "${CSR_FILE}" + fi + else + log-helper error "error: no csr file provided" + log-helper error "CFSSL_CSR_JSON and CFSSL_CSR are empty" + exit 1 + fi + + # generate cert + CONFIG_FILE="/tmp/config-file" + CERT_NAME="cert" + + REMOTE_PARAM="" + CA_CERT_PARAM="" + CA_KEY_PARAM="" + CONFIG_PARAM="" + HOSTNAME_PARAM="" + PROFILE_PARAM="" + LABEL_PARAM="" + + if [ -n "${CFSSL_REMOTE}" ]; then + REMOTE_PARAM="-remote=${CFSSL_REMOTE}" + + # add remote https ca cert to known certificates if not empty + if [ -n "${CFSSL_REMOTE_HTTPS_CA_CERT}" ]; then + if [ -e "${CFSSL_REMOTE_HTTPS_CA_CERT}" ]; then + [[ ! -d "/etc/ssl/certs/" ]] && mkdir -p /etc/ssl/certs/ + cat "${CFSSL_REMOTE_HTTPS_CA_CERT}" >> /etc/ssl/certs/ca-certificates.crt + else + log-helper error "error: remote https ca cert file ${CFSSL_REMOTE_HTTPS_CA_CERT} not found" + fi + fi + + else + + # files path with : may cause issue with cfssl tools due to : + # ReadBytes - https://github.com/cloudflare/cfssl/blob/master/helpers/helpers.go#L573 + # : is used to split env from file path + # so we copy ca cert and key to tmp + if [ -n "${CFSSL_CA_CERT}" ]; then + + CFSSL_CA_CERT_FILE="/tmp/ca-cert-file" + cp -f "${CFSSL_CA_CERT}" "${CFSSL_CA_CERT_FILE}" + chmod 644 "${CFSSL_CA_CERT_FILE}" + + CA_CERT_PARAM="-ca ${CFSSL_CA_CERT_FILE}" + fi + + if [ -n "${CFSSL_CA_KEY}" ]; then + + CFSSL_CA_KEY_FILE="/tmp/ca-key-file" + cp -f "${CFSSL_CA_KEY}" "${CFSSL_CA_KEY_FILE}" + chmod 600 "${CFSSL_CA_CERT_FILE}" + + CA_KEY_PARAM="-ca-key ${CFSSL_CA_KEY_FILE}" + fi + + fi + + if [ -n "${CFSSL_CONFIG_JSON}" ]; then + log-helper debug "use CFSSL_CONFIG_JSON value as config file" + echo "${CFSSL_CONFIG_JSON}" > "${CONFIG_FILE}" + CONFIG_PARAM="-config ${CONFIG_FILE}" + + elif [ -n "${CFSSL_CONFIG}" ]; then + log-helper debug "use ${CFSSL_CONFIG} as config file" + cp -f "${CFSSL_CONFIG}" "${CONFIG_FILE}" + CONFIG_PARAM="-config ${CONFIG_FILE}" + fi + + if [ -n "$ADDITIONAL_HOSTNAMES" ]; then + log-helper debug "additional hostnames found" + CFSSL_HOSTNAME="${CFSSL_HOSTNAME},${ADDITIONAL_HOSTNAMES}" + fi + + [[ -n "${CFSSL_HOSTNAME}" ]] && HOSTNAME_PARAM="-hostname ${CFSSL_HOSTNAME}" + [[ -n "${CFSSL_PROFILE}" ]] && PROFILE_PARAM="-profile ${CFSSL_PROFILE}" + [[ -n "${CFSSL_LABEL}" ]] && LABEL_PARAM="-label ${CFSSL_LABEL}" + + retry=0 + while [ $retry -lt "${CFSSL_RETRY}" ]; do + log-helper debug "cfssl gencert ${LOG_LEVEL_PARAM} ${REMOTE_PARAM} ${CA_CERT_PARAM} ${CA_KEY_PARAM} ${CONFIG_PARAM} ${HOSTNAME_PARAM} ${PROFILE_PARAM} ${LABEL_PARAM} ${CSR_FILE} | cfssljson -bare /tmp/${CERT_NAME}" + eval cfssl gencert "${LOG_LEVEL_PARAM}" "${REMOTE_PARAM}" "${CA_CERT_PARAM}" "${CA_KEY_PARAM}" "${CONFIG_PARAM}" "${HOSTNAME_PARAM}" "${PROFILE_PARAM}" "${LABEL_PARAM}" "${CSR_FILE}" | cfssljson -bare "/tmp/${CERT_NAME}" && break + sleep "${CFSSL_RETRY_DELAY}" + ((retry++)) + done + + # move generated files + [[ ! -e "/tmp/${CERT_NAME}.pem" ]] && exit 1 + log-helper debug "move /tmp/${CERT_NAME}.pem to ${CERT_FILE}" + mv "/tmp/${CERT_NAME}.pem" "${CERT_FILE}" + + log-helper debug "move /tmp/${CERT_NAME}-key.pem to ${KEY_FILE}" + mv "/tmp/${CERT_NAME}-key.pem" "${KEY_FILE}" + + # if ca file don't exists + if [ ! -e "${CA_FILE}" ]; then + + if [ -n "${CFSSL_REMOTE}" ]; then + log-helper debug "Get CA certificate from ${CFSSL_REMOTE}" + + retry=0 + while [ $retry -lt "${CFSSL_RETRY}" ]; do + log-helper debug "cfssl info ${LOG_LEVEL_PARAM} ${REMOTE_PARAM} ${CONFIG_PARAM} ${PROFILE_PARAM} ${LABEL_PARAM}" + eval cfssl info "${LOG_LEVEL_PARAM}" "${REMOTE_PARAM}" "${CONFIG_PARAM}" "${PROFILE_PARAM}" "${LABEL_PARAM}" | sed -e "s/.*certificate\":\"\(.*-----\)\".*/\1/g" | sed 's/\\n/\n/g' > "${CA_FILE}" && log-helper debug "CA certificate returned save as ${CA_FILE}" && break + sleep "${CFSSL_RETRY_DELAY}" + ((retry++)) + done + + [[ ! -e "${CA_FILE}" ]] && exit 1 + + elif [ -n "${CFSSL_CA_CERT}" ]; then + log-helper info "Link ${CFSSL_CA_CERT} to ${CA_FILE}" + ln -sf "${CFSSL_CA_CERT}" "${CA_FILE}" + fi + + fi + + # delete tmp files + rm -f /tmp/${CERT_NAME}.csr ${CONFIG_FILE} "${CSR_FILE}" + [[ -e "${CFSSL_CA_CERT_FILE}" ]] && rm "${CFSSL_CA_CERT_FILE}" + [[ -e "${CFSSL_CA_KEY_FILE}" ]] && rm "${CFSSL_CA_KEY_FILE}" + + log-helper debug "done :)" + + elif [ ! -e "${KEY_FILE}" ]; then + log-helper error "Certificate file ${CERT_FILE} exists but not key file ${KEY_FILE}" + exit 1 + elif [ ! -e "${CERT_FILE}" ]; then + log-helper error "Key file ${KEY_FILE} exists but not certificate file ${CERT_FILE}" + exit 1 +else + log-helper debug "Files ${CERT_FILE} and ${KEY_FILE} exists, fix files permissions" + chmod 644 "${CERT_FILE}" + chmod 600 "${KEY_FILE}" +fi diff --git a/image/base-image/service-available/:ssl-tools/assets/tool/jsonssl-helper b/image/base-image/service-available/:ssl-tools/assets/tool/jsonssl-helper new file mode 100755 index 0000000..abd524a --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/tool/jsonssl-helper @@ -0,0 +1,122 @@ +#!/bin/bash +log-helper level eq trace && set -x + +# This tool helps get certificates from json files +# like kubernetes secrets or traefik acme.json +# It takes its configuration from environment variable. +# See json-default-env file + +PREFIX=$1 +CERT_FILE=$2 +KEY_FILE=$3 +CA_FILE=$4 + +log-helper debug "jsonssl-helper is launched, everybody on the floor!" + +if [ -z "${PREFIX}" ] || [ -z "${CERT_FILE}" ] || [ -z "${KEY_FILE}" ] || [ -z "${CA_FILE}" ]; then + log-helper error "Usage: jsonssl-helper prefix cert_file key_file ca_file" + exit 1 +fi + +if [ ! -e "${CERT_FILE}" ] && [ ! -e "${KEY_FILE}" ]; then + + # set env vars + PREFIX=${PREFIX^^} # uppercase + + # search for prefixed env var first + + # set prefix variable name + # example : PREFIX_JSONSSL_FILE='MARIADB_JSONSSL_FILE' + PREFIX_JSONSSL_FILE=${PREFIX}_JSONSSL_FILE + PREFIX_JSONSSL_HOSTNAME=${PREFIX}_JSONSSL_HOSTNAME + + PREFIX_JSONSSL_PROFILE=${PREFIX}_JSONSSL_PROFILE + PREFIX_JSONSSL_GET_CA_CERT_CMD=${PREFIX}_JSONSSL_GET_CA_CERT_CMD + PREFIX_JSONSSL_GET_CERT_CMD=${PREFIX}_JSONSSL_GET_CERT_CMD + PREFIX_JSONSSL_GET_KEY_CMD=${PREFIX}_JSONSSL_GET_KEY_CMD + + # assign JSONSSL_FILE=${!PREFIX_JSONSSL_FILE} if value is not empty otherwise JSONSSL_FILE=JSONSSL_FILE + JSONSSL_FILE=${!PREFIX_JSONSSL_FILE:-$JSONSSL_FILE} + JSONSSL_HOSTNAME=${!PREFIX_JSONSSL_HOSTNAME:-$JSONSSL_HOSTNAME} + + JSONSSL_PROFILE=${!PREFIX_JSONSSL_PROFILE:-$JSONSSL_PROFILE} + JSONSSL_GET_CA_CERT_CMD=${!PREFIX_JSONSSL_GET_CA_CERT_CMD:-$JSONSSL_GET_CA_CERT_CMD} + JSONSSL_GET_CERT_CMD=${!PREFIX_JSONSSL_GET_CERT_CMD:-$JSONSSL_GET_CERT_CMD} + JSONSSL_GET_KEY_CMD=${!PREFIX_JSONSSL_GET_KEY_CMD:-$JSONSSL_GET_KEY_CMD} + + source "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/jsonssl-default-env" + + if [ -z "${JSONSSL_FILE}" ]; then + log-helper info "Variable JSONSSL_FILE is empty, set to default location:" + log-helper info "JSONSSL_FILE=${JSONSSL_FILE_DEFAULT}" + JSONSSL_FILE=${JSONSSL_FILE_DEFAULT} + fi + + if [ ! -e "${JSONSSL_FILE}" ]; then + log-helper error "JSONSSL_FILE file '${JSONSSL_FILE}' not found" + exit 1 + fi + + # Json file profile, only traefik for now + if [ "${JSONSSL_PROFILE,,}" = "traefik" ]; then + # Let's Encrypt CA certificate is in cert file after the domain certificate. + # So we took what's after the first cert. + JSONSSL_GET_CA_CERT_CMD="awk '{if(found) print} /END CERTIFICATE/{found=1}' ${CERT_FILE}" + + JSONSSL_GET_CERT_CMD="cat ${JSONSSL_FILE} | jq -r '[.Certificates[]] | map(select(.Domain.Main == \"${JSONSSL_HOSTNAME}\")) | .[0].Certificate' | base64 -d" + JSONSSL_GET_KEY_CMD="cat ${JSONSSL_FILE} | jq -r '[.Certificates[]] | map(select(.Domain.Main == \"${JSONSSL_HOSTNAME}\")) | .[0].Key' | base64 -d" + elif [ "${JSONSSL_PROFILE,,}" = "traefik_up_to_v1_6" ]; then + # Let's Encrypt CA certificate is in cert file after the domain certificate. + # So we took what's after the first cert. + JSONSSL_GET_CA_CERT_CMD="awk '{if(found) print} /END CERTIFICATE/{found=1}' ${CERT_FILE}" + + JSONSSL_GET_CERT_CMD="cat ${JSONSSL_FILE} | jq -r '[.[\"DomainsCertificate\"].Certs[].Certificate] | map(select(.Domain == \"${JSONSSL_HOSTNAME}\")) | .[0].Certificate' | base64 -d" + JSONSSL_GET_KEY_CMD="cat ${JSONSSL_FILE} | jq -r '[.[\"DomainsCertificate\"].Certs[].Certificate] | map(select(.Domain == \"${JSONSSL_HOSTNAME}\")) | .[0].PrivateKey' | base64 -d" + fi + + log-helper debug "Run JSONSSL_GET_CERT_CMD: ${JSONSSL_GET_CERT_CMD}" + log-helper debug "put return in ${CERT_FILE}" + eval "${JSONSSL_GET_CERT_CMD}" > "${CERT_FILE}" + + if [ ! -s "$CERT_FILE" ]; then + log-helper error "Generated file '${CERT_FILE}' is empty" + log-helper error "Set loglevel to debug for more information" + exit 1 + fi + + log-helper debug "Run JSONSSL_GET_KEY_CMD: ${JSONSSL_GET_KEY_CMD}" + log-helper debug "put return in ${KEY_FILE}" + eval "$JSONSSL_GET_KEY_CMD" > "${KEY_FILE}" + + if [ ! -s "${KEY_FILE}" ]; then + log-helper error "Generated file '${KEY_FILE}' is empty" + log-helper error "Set loglevel to debug for more information" + exit 1 + fi + + # if CA cert doesn't exist + if [ ! -e "$CA_FILE" ]; then + log-helper debug "Run JSONSSL_GET_CA_CERT_CMD: ${JSONSSL_GET_CA_CERT_CMD}" + log-helper debug "put return in ${CA_FILE}" + eval "$JSONSSL_GET_CA_CERT_CMD" > "${CA_FILE}" + + if [ ! -s "$CA_FILE" ]; then + log-helper error "Generated file '${CA_FILE}' is empty" + log-helper error "Set loglevel to debug for more information" + exit 1 + fi + fi + + log-helper debug "done :)" + + elif [ ! -e "${KEY_FILE}" ]; then + log-helper error "Certificate file ${CERT_FILE} exists but not key file ${KEY_FILE}" + exit 1 + elif [ ! -e "${CERT_FILE}" ]; then + log-helper error "Key file ${KEY_FILE} exists but not certificate file ${CERT_FILE}" + exit 1 +else + log-helper debug "Files ${CERT_FILE} and ${KEY_FILE} exists, fix files permissions" + chmod 644 "${CERT_FILE}" + chmod 600 "${KEY_FILE}" +fi diff --git a/image/base-image/service-available/:ssl-tools/assets/tool/ssl-auto-renew b/image/base-image/service-available/:ssl-tools/assets/tool/ssl-auto-renew new file mode 100755 index 0000000..78d8f7d --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/tool/ssl-auto-renew @@ -0,0 +1,152 @@ +#!/bin/bash -e + +# This file aims to be called by a cron task +# and not directly. See ssl-helper. + +source /container/run/environment.sh + +SSL_HELPER_TOOL=$1 +PREFIX=$2 +CERT_FILE=$3 +KEY_FILE=$4 +CA_FILE=$5 +IMPACTED_SERVICES=$6 +JSONSSL_FILE=$7 +FROM_FILES=$8 +CERT_FROM_FILE=$9 +KEY_FROM_FILE=${10} +CA_CERT_FROM_FILE=${11} + +function stop_impacted_services() { + # Stop impacted services + if [ -n "${IMPACTED_SERVICES}" ]; then + log-helper info "Services to stop: ${IMPACTED_SERVICES}" + + impacted_services_table=("${IMPACTED_SERVICES}") + for service in "${impacted_services_table[@]}" + do + log-helper info "Stopping ${service}..." + sv stop "/container/run/process/${service}" + done + + log-helper info "All services are stopped" + fi +} + +function start_impacted_services() { + # restart impacted services + if [ -n "${IMPACTED_SERVICES}" ]; then + + impacted_services_table=("${IMPACTED_SERVICES}") + for service in "${impacted_services_table[@]}" + do + log-helper info "Starting ${service}..." + sv start "/container/run/process/${service}" + done + + log-helper info "All services are started" + fi +} + +# renew from container files +if [ "${FROM_FILES,,}" = "true" ]; then + + log-helper info "Check renew from files" + renew=false + + # File previous md5 + CERT_PREVIOUS_MD5=$(cat "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${CERT_FILE}.md5") || true + KEY_PREVIOUS_MD5=$(cat "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${KEY_FILE}.md5") || true + CA_CERT_PREVIOUS_MD5=$(cat "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${CA_FILE}.md5") || true + + # from file current md5 + FROM_CERT_MD5=$(md5sum "${CERT_FROM_FILE}" | awk '{ print $1 }') + FROM_KEY_MD5=$(md5sum "${KEY_FROM_FILE}" | awk '{ print $1 }') + FROM_CA_CERT_MD5=$(md5sum "${CA_CERT_FROM_FILE}" | awk '{ print $1 }') + + [[ "$CERT_PREVIOUS_MD5" != "$FROM_CERT_MD5" ]] && renew=true + [[ "$KEY_PREVIOUS_MD5" != "$FROM_KEY_MD5" ]] && renew=true + [[ "$CA_CERT_PREVIOUS_MD5" != "$FROM_CA_CERT_MD5" ]] && renew=true + + if ! $renew; then + log-helper info "Certificate files are identicals" + exit 0 + fi + + log-helper info "Certificate files are differents" + + stop_impacted_services + + if [ "${CERT_FROM_FILE}" != "${CERT_FILE}" ]; then + log-helper info "Copy ${CERT_FROM_FILE} to ${CERT_FILE}" + cp -f "${CERT_FROM_FILE}" "${CERT_FILE}" + fi + + if [ "${KEY_FROM_FILE}" != "${KEY_FILE}" ]; then + log-helper info "Copy ${KEY_FROM_FILE} to ${KEY_FILE}" + cp -f "${KEY_FROM_FILE}" "${KEY_FILE}" + fi + + if [ "${CA_CERT_FROM_FILE}" != "${CA_FILE}" ]; then + log-helper info "Copy ${CA_CERT_FROM_FILE} to ${CA_FILE}" + cp -f "${CA_CERT_FROM_FILE}" "${CA_FILE}" + fi + + log-helper info "Update file md5 with new values" + echo "${FROM_CERT_MD5}" > "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${CERT_FILE}.md5" + echo "${FROM_KEY_MD5}" > "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${KEY_FILE}.md5" + echo "${FROM_CA_CERT_MD5}" > "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${CA_FILE}.md5" + + start_impacted_services + + # renew with cfssl or jsonssl +else + log-helper info "Check renew for cfssl or jsonssl" + + cert_ok=false + ca_ok=false + + # the certificate will expired in the next day + if openssl x509 -checkend 259200 -noout -in "${CERT_FILE}"; then + log-helper info "The certificate '${CERT_FILE}' is ok for the next 3 days at least." + cert_ok=true + fi + + if openssl x509 -checkend 259200 -noout -in "${CA_FILE}"; then + log-helper info "The CA certificate '${CA_FILE}' is ok for the next 3 days at least." + ca_ok=true + fi + + if [ "${SSL_HELPER_TOOL}" = "jsonssl-helper" ]; then + log-helper info "Check if ${JSONSSL_FILE} has changed" + JSONSSL_FILE_PREVIOUS_MD5=$(cat "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${JSONSSL_FILE}.md5") || true + JSONSSL_FILE_MD5=$(md5sum "${JSONSSL_FILE}" | awk '{ print $1 }') + + [[ "${JSONSSL_FILE_PREVIOUS_MD5}" != "${JSONSSL_FILE_MD5}" ]] && cert_ok=false + fi + + if ${cert_ok} && ${ca_ok}; then + log-helper info "Nothing to do :)" + exit 0 + fi + + log-helper info "Auto-renew on the way!" + + stop_impacted_services + + log-helper info "Remove certificate files" + rm -f "${CERT_FILE}" "${KEY_FILE}" "${CA_FILE}" + + log-helper info "Regenerate certificate with ${SSL_HELPER_TOOL}" + ${SSL_HELPER_TOOL} "${PREFIX}" "${CERT_FILE}" "${KEY_FILE}" "${CA_FILE}" + + start_impacted_services + + if [ "${SSL_HELPER_TOOL}" = "jsonssl-helper" ]; then + log-helper info "Update file md5 with new values" + echo "${JSONSSL_FILE_MD5}" > "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${JSONSSL_FILE}.md5" + fi + +fi + +log-helper info "Auto-renew finished! Champagne!" diff --git a/image/base-image/service-available/:ssl-tools/assets/tool/ssl-helper b/image/base-image/service-available/:ssl-tools/assets/tool/ssl-helper new file mode 100755 index 0000000..8a5d717 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/assets/tool/ssl-helper @@ -0,0 +1,100 @@ +#!/bin/bash -e +log-helper level eq trace && set -x + +# This tool helps to generate tls certificates with cfssl +# or get certificates from a json file + +PREFIX=$1 +CERT_FILE=$2 +KEY_FILE=$3 +CA_FILE=$4 + +log-helper debug "Hi! I'm ssl-helper, what button should i press ?" + +# set env vars +PREFIX=${PREFIX^^} # uppercase + +PREFIX_SSL_HELPER_TOOL=${PREFIX}_SSL_HELPER_TOOL +PREFIX_SSL_HELPER_AUTO_RENEW=${PREFIX}_SSL_HELPER_AUTO_RENEW +PREFIX_SSL_HELPER_AUTO_RENEW_CRON_EXP=${PREFIX}_SSL_HELPER_AUTO_RENEW_CRON_EXP +PREFIX_SSL_HELPER_AUTO_RENEW_SERVICES_IMPACTED=${PREFIX}_SSL_HELPER_AUTO_RENEW_SERVICES_IMPACTED +PREFIX_SSL_HELPER_AUTO_RENEW_FROM_FILES=${PREFIX}_SSL_HELPER_AUTO_RENEW_FROM_FILES +PREFIX_SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE=${PREFIX}_SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE +PREFIX_SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE=${PREFIX}_SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE +PREFIX_SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE=${PREFIX}_SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE + +SSL_HELPER_TOOL=${!PREFIX_SSL_HELPER_TOOL:-$SSL_HELPER_TOOL} +SSL_HELPER_AUTO_RENEW=${!PREFIX_SSL_HELPER_AUTO_RENEW:-$SSL_HELPER_AUTO_RENEW} +SSL_HELPER_AUTO_RENEW_CRON_EXP=${!PREFIX_SSL_HELPER_AUTO_RENEW_CRON_EXP:-$SSL_HELPER_AUTO_RENEW_CRON_EXP} +SSL_HELPER_AUTO_RENEW_SERVICES_IMPACTED=${!PREFIX_SSL_HELPER_AUTO_RENEW_SERVICES_IMPACTED:-$SSL_HELPER_AUTO_RENEW_SERVICES_IMPACTED} +SSL_HELPER_AUTO_RENEW_FROM_FILES=${!PREFIX_SSL_HELPER_AUTO_RENEW_FROM_FILES:-$SSL_HELPER_AUTO_RENEW_FROM_FILES} +SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE=${!PREFIX_SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE:-$SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE} +SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE=${!PREFIX_SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE:-$SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE} +SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE=${!PREFIX_SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE:-$SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE} + +source "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/default-env" + +# call the certificate tool cfssl-helper (default) or jsonssl-helper +${SSL_HELPER_TOOL,,} "${PREFIX}" "${CERT_FILE}" "${KEY_FILE}" "${CA_FILE}" + +# auto-renew certificates just before it expired +# or if source files have changed +if [ "${SSL_HELPER_AUTO_RENEW,,}" = "true" ]; then + + # only for multiple process images (uses cron) + if [ ! -e "/container/multiple_process_stack_added" ]; then + log-helper error "auto-renew is available only with multiple process images" + exit 1 + fi + + # if SSL_HELPER_AUTO_RENEW_FROM_FILES=true check certificate source files + if [ "${SSL_HELPER_AUTO_RENEW_FROM_FILES,,}" = "true" ]; then + + [[ -z "${SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE}" ]] && SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE=${CERT_FILE} + [[ -z "${SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE}" ]] && SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE=${KEY_FILE} + [[ -z "${SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE}" ]] && SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE=${CA_FILE} + + if [ ! -e "${SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE}" ] || [ ! -e "${SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE}" ] || [ ! -e "${SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE}" ]; then + log-helper error "with SSL_HELPER_AUTO_RENEW_FROM_FILES=true the following files must exists:" + log-helper error "SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE=${SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE}" + log-helper error "SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE=${SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE}" + log-helper error "SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE=${SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE}" + exit 1 + fi + + mkdir -p "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5$(dirname "${CERT_FILE}")" + mkdir -p "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5$(dirname "${KEY_FILE}")" + mkdir -p "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5$(dirname "${CA_FILE}")" + + # calculate certificates files md5 + md5sum "${CERT_FILE}" | awk '{ print $1 }' > "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${CERT_FILE}.md5" + md5sum "${KEY_FILE}" | awk '{ print $1 }' > "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${KEY_FILE}.md5" + md5sum "${CA_FILE}" | awk '{ print $1 }' > "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${CA_FILE}.md5" + + fi + + if [ "${SSL_HELPER_TOOL,,}" = "jsonssl-helper" ]; then + + PREFIX_JSONSSL_FILE=${PREFIX}_JSONSSL_FILE + JSONSSL_FILE=${!PREFIX_JSONSSL_FILE:-$JSONSSL_FILE} + + source "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/jsonssl-default-env" + + if [ -z "${JSONSSL_FILE}" ]; then + JSONSSL_FILE=${JSONSSL_FILE_DEFAULT} + fi + + # calculate jsonssl file md5 + mkdir -p "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5$(dirname "${JSONSSL_FILE}")" + md5sum "${JSONSSL_FILE}" | awk '{ print $1 }' > "${CONTAINER_SERVICE_DIR}/:ssl-tools/assets/md5${JSONSSL_FILE}.md5" + + fi + + # add cron job + echo "${SSL_HELPER_AUTO_RENEW_CRON_EXP} root /usr/sbin/ssl-auto-renew ${SSL_HELPER_TOOL,,} ${PREFIX} ${CERT_FILE} ${KEY_FILE} ${CA_FILE} \"${SSL_HELPER_AUTO_RENEW_SERVICES_IMPACTED}\" \"${JSONSSL_FILE}\" \"${SSL_HELPER_AUTO_RENEW_FROM_FILES}\" \"${SSL_HELPER_AUTO_RENEW_CERT_FROM_FILE}\" \"${SSL_HELPER_AUTO_RENEW_KEY_FROM_FILE}\" \"${SSL_HELPER_AUTO_RENEW_CA_CERT_FROM_FILE}\" 2>&1 | /usr/bin/logger -t cron_ssl_auto_renew" > "/etc/cron.d/${PREFIX}" + chmod 600 "/etc/cron.d/${PREFIX}" + + # disable auto-renew if it was added + elif [ -e "/etc/cron.d/${PREFIX}" ]; then + rm -f "/etc/cron.d/${PREFIX}" +fi diff --git a/image/base-image/service-available/:ssl-tools/download.sh b/image/base-image/service-available/:ssl-tools/download.sh new file mode 100755 index 0000000..985b1bb --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/download.sh @@ -0,0 +1,69 @@ +#!/bin/bash -e + +UARCH=$(uname -m) +echo "Architecture is ${UARCH}" + +case "${UARCH}" in + + "x86_64") + HOST_ARCH="amd64" + ;; + + "arm64" | "aarch64") + HOST_ARCH="arm64" + ;; + + "armv7l" | "armv6l" | "armhf") + HOST_ARCH="arm" + ;; + + "i386") + HOST_ARCH="386" + ;; + + *) + echo "Architecture not supported. Exiting." + exit 1 + ;; +esac + +echo "Going to use ${HOST_ARCH} cfssl binaries" + +# download curl and ca-certificate from apt-get if needed +to_install=() + +if [ "$(dpkg-query -W -f='${Status}' curl 2>/dev/null | grep -c "ok installed")" -eq 0 ]; then + to_install+=("curl") +fi + +if [ "$(dpkg-query -W -f='${Status}' ca-certificates 2>/dev/null | grep -c "ok installed")" -eq 0 ]; then + to_install+=("ca-certificates") +fi + +if [ ${#to_install[@]} -ne 0 ]; then + LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${to_install[@]}" +fi + +LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssl jq + +# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=923479 +if [[ "${HOST_ARCH}" == 'arm' ]]; then + LC_ALL=C DEBIAN_FRONTEND=noninteractive c_rehash +fi + +echo "Download cfssl ..." +echo "curl -o /usr/sbin/cfssl -SL https://github.com/osixia/cfssl/releases/download/1.4.1/cfssl_linux-${HOST_ARCH}" +curl -o /usr/sbin/cfssl -SL "https://github.com/osixia/cfssl/releases/download/1.4.1/cfssl_linux-${HOST_ARCH}" +chmod 700 /usr/sbin/cfssl + +echo "Download cfssljson ..." +echo "curl -o /usr/sbin/cfssljson -SL https://github.com/osixia/cfssl/releases/download/1.4.1/cfssljson_linux-${HOST_ARCH}" +curl -o /usr/sbin/cfssljson -SL "https://github.com/osixia/cfssl/releases/download/1.4.1/cfssljson_linux-${HOST_ARCH}" +chmod 700 /usr/sbin/cfssljson + +echo "Project sources: https://github.com/cloudflare/cfssl" + +# remove tools installed to download cfssl +if [ ${#to_install[@]} -ne 0 ]; then + apt-get remove -y --purge --auto-remove "${to_install[@]}" +fi diff --git a/image/base-image/service-available/:ssl-tools/startup.sh b/image/base-image/service-available/:ssl-tools/startup.sh new file mode 100755 index 0000000..0189099 --- /dev/null +++ b/image/base-image/service-available/:ssl-tools/startup.sh @@ -0,0 +1,5 @@ +#!/bin/sh -e +log-helper level eq trace && set -x + +chmod 700 "${CONTAINER_SERVICE_DIR}"/:ssl-tools/assets/tool/* +ln -sf "${CONTAINER_SERVICE_DIR}"/:ssl-tools/assets/tool/* /usr/sbin diff --git a/image/base-image/service-available/:syslog-ng-core/assets/config/syslog-ng.conf b/image/base-image/service-available/:syslog-ng-core/assets/config/syslog-ng.conf new file mode 100644 index 0000000..839c118 --- /dev/null +++ b/image/base-image/service-available/:syslog-ng-core/assets/config/syslog-ng.conf @@ -0,0 +1,152 @@ +@version: 3.19 +@include "scl.conf" + +# Syslog-ng configuration file, compatible with default Debian syslogd +# installation. + +# First, set some global options. +options { chain_hostnames(off); flush_lines(0); use_dns(no); dns-cache(no); use_fqdn(no); + owner("root"); group("adm"); perm(0640); stats_freq(0); + bad_hostname("^gconfd$"); +}; + +######################## +# Sources +######################## +# This is the default behavior of sysklogd package +# Logs may come from unix stream, but not from another machine. +# +source s_src { + unix-dgram("/dev/log"); + internal(); +}; + +# If you wish to get logs from remote machine you should uncomment +# this and comment the above source line. +# +#source s_net { tcp(ip(127.0.0.1) port(1000)); }; + +######################## +# Destinations +######################## +# First some standard logfile +# +destination d_auth { file("/var/log/auth.log"); }; +destination d_cron { file("/var/log/cron.log"); }; +destination d_daemon { file("/var/log/daemon.log"); }; +destination d_kern { file("/var/log/kern.log"); }; +destination d_lpr { file("/var/log/lpr.log"); }; +destination d_mail { file("/var/log/mail.log"); }; +destination d_syslog { file("/var/log/syslog"); }; +destination d_user { file("/var/log/user.log"); }; +destination d_uucp { file("/var/log/uucp.log"); }; + +# This files are the log come from the mail subsystem. +# +destination d_mailinfo { file("/var/log/mail.info"); }; +destination d_mailwarn { file("/var/log/mail.warn"); }; +destination d_mailerr { file("/var/log/mail.err"); }; + +# Logging for INN news system +# +destination d_newscrit { file("/var/log/news/news.crit"); }; +destination d_newserr { file("/var/log/news/news.err"); }; +destination d_newsnotice { file("/var/log/news/news.notice"); }; + +# Some 'catch-all' logfiles. +# +destination d_debug { file("/var/log/debug"); }; +destination d_error { file("/var/log/error"); }; +destination d_messages { file("/var/log/messages"); }; + +# The named pipe /dev/xconsole is for the nsole' utility. To use it, +# you must invoke nsole' with the -file' option: +# +# $ xconsole -file /dev/xconsole [...] +# +destination d_xconsole { pipe("/dev/xconsole"); }; + +# Send the messages to an other host +# +#destination d_net { tcp("127.0.0.1" port(1000) log_fifo_size(1000)); }; + +# Debian only +destination d_ppp { file("/var/log/ppp.log"); }; + +# stdout for docker +destination d_stdout { ##SYSLOG_OUTPUT_MODE_DEV_STDOUT##("/dev/stdout"); }; + +######################## +# Filters +######################## +# Here's come the filter options. With this rules, we can set which +# message go where. + +filter f_dbg { level(debug); }; +filter f_info { level(info); }; +filter f_notice { level(notice); }; +filter f_warn { level(warn); }; +filter f_err { level(err); }; +filter f_crit { level(crit .. emerg); }; + +filter f_debug { level(debug) and not facility(auth, authpriv, news, mail); }; +filter f_error { level(err .. emerg) ; }; +filter f_messages { level(info,notice,warn) and + not facility(auth,authpriv,cron,daemon,mail,news); }; + +filter f_auth { facility(auth, authpriv) and not filter(f_debug); }; +filter f_cron { facility(cron) and not filter(f_debug); }; +filter f_daemon { facility(daemon) and not filter(f_debug); }; +filter f_kern { facility(kern) and not filter(f_debug); }; +filter f_lpr { facility(lpr) and not filter(f_debug); }; +filter f_local { facility(local0, local1, local3, local4, local5, + local6, local7) and not filter(f_debug); }; +filter f_mail { facility(mail) and not filter(f_debug); }; +filter f_news { facility(news) and not filter(f_debug); }; +filter f_syslog3 { not facility(auth, authpriv, mail) and not filter(f_debug); }; +filter f_user { facility(user) and not filter(f_debug); }; +filter f_uucp { facility(uucp) and not filter(f_debug); }; + +filter f_cnews { level(notice, err, crit) and facility(news); }; +filter f_cother { level(debug, info, notice, warn) or facility(daemon, mail); }; + +filter f_ppp { facility(local2) and not filter(f_debug); }; +filter f_console { level(warn .. emerg); }; + +######################## +# Log paths +######################## +log { source(s_src); filter(f_auth); destination(d_auth); }; +log { source(s_src); filter(f_cron); destination(d_cron); }; +log { source(s_src); filter(f_daemon); destination(d_daemon); }; +log { source(s_src); filter(f_kern); destination(d_kern); }; +log { source(s_src); filter(f_lpr); destination(d_lpr); }; +log { source(s_src); filter(f_syslog3); destination(d_syslog); destination(d_stdout); }; +log { source(s_src); filter(f_user); destination(d_user); }; +log { source(s_src); filter(f_uucp); destination(d_uucp); }; + +log { source(s_src); filter(f_mail); destination(d_mail); }; +#log { source(s_src); filter(f_mail); filter(f_info); destination(d_mailinfo); }; +#log { source(s_src); filter(f_mail); filter(f_warn); destination(d_mailwarn); }; +#log { source(s_src); filter(f_mail); filter(f_err); destination(d_mailerr); }; + +log { source(s_src); filter(f_news); filter(f_crit); destination(d_newscrit); }; +log { source(s_src); filter(f_news); filter(f_err); destination(d_newserr); }; +log { source(s_src); filter(f_news); filter(f_notice); destination(d_newsnotice); }; +#log { source(s_src); filter(f_cnews); destination(d_console_all); }; +#log { source(s_src); filter(f_cother); destination(d_console_all); }; + +#log { source(s_src); filter(f_ppp); destination(d_ppp); }; + +log { source(s_src); filter(f_debug); destination(d_debug); }; +log { source(s_src); filter(f_error); destination(d_error); }; +log { source(s_src); filter(f_messages); destination(d_messages); }; + +# All messages send to a remote site +# +#log { source(s_src); destination(d_net); }; + +### +# Include all config files in /etc/syslog-ng/conf.d/ +### +@include "/etc/syslog-ng/conf.d/*.conf" diff --git a/image/base-image/service-available/:syslog-ng-core/assets/config/syslog_ng_default b/image/base-image/service-available/:syslog-ng-core/assets/config/syslog_ng_default new file mode 100644 index 0000000..c9e7057 --- /dev/null +++ b/image/base-image/service-available/:syslog-ng-core/assets/config/syslog_ng_default @@ -0,0 +1,12 @@ +# If a variable is not set here, then the corresponding +# parameter will not be changed. +# If a variables is set, then every invocation of +# syslog-ng's init script will set them using dmesg. + +# log level of messages which should go to console +# see syslog(3) for details +# +#CONSOLE_LOG_LEVEL=1 + +# Command line options to syslog-ng +SYSLOGNG_OPTS="--no-caps" diff --git a/image/base-image/service-available/:syslog-ng-core/download.sh b/image/base-image/service-available/:syslog-ng-core/download.sh new file mode 100755 index 0000000..92bc3df --- /dev/null +++ b/image/base-image/service-available/:syslog-ng-core/download.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e + +# download syslog-ng-core from apt-get +LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends syslog-ng-core diff --git a/image/base-image/service-available/:syslog-ng-core/install.sh b/image/base-image/service-available/:syslog-ng-core/install.sh new file mode 100755 index 0000000..cc8a4a2 --- /dev/null +++ b/image/base-image/service-available/:syslog-ng-core/install.sh @@ -0,0 +1,8 @@ +#!/bin/sh -e + +mkdir -p /var/lib/syslog-ng +rm -f /etc/default/syslog-ng + +touch /var/log/syslog +chmod u=rw,g=r,o= /var/log/syslog +rm -f /etc/syslog-ng/syslog-ng.conf diff --git a/image/base-image/service-available/:syslog-ng-core/process.sh b/image/base-image/service-available/:syslog-ng-core/process.sh new file mode 100755 index 0000000..c842af4 --- /dev/null +++ b/image/base-image/service-available/:syslog-ng-core/process.sh @@ -0,0 +1,9 @@ +#!/bin/sh -e +log-helper level eq trace && set -x + +PIDFILE="/var/run/syslog-ng.pid" +SYSLOGNG_OPTS="" + +[ -r /etc/default/syslog-ng ] && . /etc/default/syslog-ng + +exec /usr/sbin/syslog-ng --pidfile "$PIDFILE" -F $SYSLOGNG_OPTS diff --git a/image/base-image/service-available/:syslog-ng-core/startup.sh b/image/base-image/service-available/:syslog-ng-core/startup.sh new file mode 100755 index 0000000..e0e1c5a --- /dev/null +++ b/image/base-image/service-available/:syslog-ng-core/startup.sh @@ -0,0 +1,22 @@ +#!/bin/sh -e +log-helper level eq trace && set -x + +ln -sf "${CONTAINER_SERVICE_DIR}/:syslog-ng-core/assets/config/syslog_ng_default" /etc/default/syslog-ng +ln -sf "${CONTAINER_SERVICE_DIR}/:syslog-ng-core/assets/config/syslog-ng.conf" /etc/syslog-ng/syslog-ng.conf + +# If /dev/log is either a named pipe or it was placed there accidentally, +# e.g. because of the issue documented at https://github.com/phusion/baseimage-docker/pull/25, +# then we remove it. +if [ ! -S /dev/log ]; then rm -f /dev/log; fi +if [ ! -S /var/lib/syslog-ng/syslog-ng.ctl ]; then rm -f /var/lib/syslog-ng/syslog-ng.ctl; fi + +# determine output mode on /dev/stdout because of the issue documented at https://github.com/phusion/baseimage-docker/issues/468 +if [ -p /dev/stdout ]; then + sed -i 's/##SYSLOG_OUTPUT_MODE_DEV_STDOUT##/pipe/' /etc/syslog-ng/syslog-ng.conf +else + sed -i 's/##SYSLOG_OUTPUT_MODE_DEV_STDOUT##/file/' /etc/syslog-ng/syslog-ng.conf +fi + +# If /var/log is writable by another user logrotate will fail +/bin/chown root:root /var/log +/bin/chmod 0755 /var/log diff --git a/image/base-image/tool/add-multiple-process-stack b/image/base-image/tool/add-multiple-process-stack new file mode 100755 index 0000000..131d8a7 --- /dev/null +++ b/image/base-image/tool/add-multiple-process-stack @@ -0,0 +1,4 @@ +#!/bin/sh -e +echo "Install the multiple process stack: runit, syslog-ng-core, logrotate and cron" +/container/tool/add-service-available :runit :syslog-ng-core :logrotate :cron +touch /container/multiple_process_stack_added diff --git a/image/base-image/tool/add-service-available b/image/base-image/tool/add-service-available new file mode 100755 index 0000000..081f7c3 --- /dev/null +++ b/image/base-image/tool/add-service-available @@ -0,0 +1,30 @@ +#!/bin/sh -e + +# Usage : +# RUN /container/tool/add-service-available [service1] [service2] ... + +SERVICE_DIR="/container/service" +SERVICE_AVAILABLE_DIR="/container/service-available" +DOWNLOAD_FILENAME="download.sh" + +for i in "$@" +do + + echo "add-service-available: ${i}" + if [ -d "${SERVICE_AVAILABLE_DIR}/${i}" ]; then + + if [ -f "${SERVICE_AVAILABLE_DIR}/${i}/${DOWNLOAD_FILENAME}" ]; then + echo "run ${SERVICE_AVAILABLE_DIR}/${i}/${DOWNLOAD_FILENAME}" + ${SERVICE_AVAILABLE_DIR}/"${i}"/"${DOWNLOAD_FILENAME}" + echo "remove ${SERVICE_AVAILABLE_DIR}/${i}/${DOWNLOAD_FILENAME}" + rm -f "${SERVICE_AVAILABLE_DIR}/${i}/${DOWNLOAD_FILENAME}" + fi + + echo "move ${SERVICE_AVAILABLE_DIR}/${i} to ${SERVICE_DIR}/${i}" + mv "${SERVICE_AVAILABLE_DIR}/${i}" "${SERVICE_DIR}/${i}" + + else + echo "service-available: ${i} not found in ${SERVICE_AVAILABLE_DIR}/${i}" + exit 1 + fi +done diff --git a/image/base-image/tool/complex-bash-env b/image/base-image/tool/complex-bash-env new file mode 100755 index 0000000..829bcd1 --- /dev/null +++ b/image/base-image/tool/complex-bash-env @@ -0,0 +1,91 @@ +#!/bin/bash -e + +call=$1 + +function iterate() { + local env_var_name=$1 + local env_var=${!env_var_name} + + if [ "$(complex-bash-env isTable "$env_var")" = true ]; then + complex-bash-env stripTablePrefix "${env_var}" + else + echo "${env_var_name}" + fi +} + +function isTable() { + local env_var=$1 + if [ "$(echo "${env_var}" | grep "#COMPLEX_BASH_ENV:TABLE:" -c )" -eq 1 ]; then + echo true + else + echo false + fi +} + +function isRow() { + local env_var=$1 + if [ "$(echo "${env_var}" | grep "#COMPLEX_BASH_ENV:ROW:" -c )" -eq 1 ]; then + echo true + else + echo false + fi +} + +function getRowKey() { + local env_var=$1 + local row_key_var_name + row_key_var_name=$(complex-bash-env getRowKeyVarName "$env_var") + echo "${!row_key_var_name}" +} + +function getRowValue() { + local env_var=$1 + local row_value_var_name + row_value_var_name=$(complex-bash-env getRowValueVarName "$env_var") + echo "${!row_value_var_name}" +} + +function getRowKeyVarName() { + local env_var=$1 + local row=($(complex-bash-env getRow "$env_var")) + echo "${row[0]}" +} + +function getRowValueVarName() { + local env_var=$1 + local row=($(complex-bash-env getRow "$env_var")) + echo "${row[1]}" +} + +function getRow() { + local env_var + env_var=$1 + if [ "$(complex-bash-env isRow "$env_var")" = true ]; then + local env_var + env_var=$(complex-bash-env stripRowPrefix "$env_var") + echo "${env_var}" + else + echo "$env_var is not a complex bash env row" + exit 1 + fi +} + +function stripTablePrefix() { + local env_var=$1 + stripPrefix "$env_var" "#COMPLEX_BASH_ENV:TABLE:" +} + +function stripRowPrefix() { + local env_var=$1 + stripPrefix "$env_var" "#COMPLEX_BASH_ENV:ROW:" +} + +function stripPrefix() { + local env_var=$1 + local prefix=$2 + local r=${env_var#$prefix} + echo "${r}" +} + +shift +$call "$@" diff --git a/image/base-image/tool/install-service b/image/base-image/tool/install-service new file mode 100755 index 0000000..b8a5782 --- /dev/null +++ b/image/base-image/tool/install-service @@ -0,0 +1,41 @@ +#!/usr/bin/python3 -u +import os, os.path, subprocess + +SERVICE_DIR = "/container/service" +INSTALL_FILENAME = "install.sh" +PROCESS_FILENAME = "process.sh" +nb_process = 0 + +print("install-service") +# Auto run global install script if available +if os.path.isfile(SERVICE_DIR + os.sep + INSTALL_FILENAME): + print(("run " + SERVICE_DIR + os.sep + INSTALL_FILENAME)) + subprocess.call([SERVICE_DIR + os.sep + INSTALL_FILENAME],shell=True) + + print(("remove " + SERVICE_DIR + os.sep + INSTALL_FILENAME + "\n")) + os.remove(SERVICE_DIR + os.sep + INSTALL_FILENAME) + +# Process install script of services in /container/service +for service in sorted(os.listdir(SERVICE_DIR)): + + if os.path.isfile(SERVICE_DIR + os.sep + service + os.sep + INSTALL_FILENAME): + print(("run " + SERVICE_DIR + os.sep + service + os.sep + INSTALL_FILENAME)) + subprocess.call([SERVICE_DIR + os.sep + service + os.sep + INSTALL_FILENAME],shell=True) + + print(("remove " + SERVICE_DIR + os.sep + service + os.sep + INSTALL_FILENAME)) + os.remove(SERVICE_DIR + os.sep + service + os.sep + INSTALL_FILENAME) + + if os.path.isfile(SERVICE_DIR + os.sep + service + os.sep + PROCESS_FILENAME): + nb_process += 1 + + +print((str(nb_process) + " process found.")) + +# Multiple process image +if nb_process > 1: + if not os.path.exists("/container/multiple_process_stack_added"): + print("This image has multiple process.") + subprocess.call(["apt-get update"],shell=True) + subprocess.call(["/container/tool/add-multiple-process-stack"],shell=True) + print("For better image build process consider adding:") + print("\"/container/tool/add-multiple-process-stack\" after an apt-get update in your Dockerfile.") diff --git a/image/base-image/tool/log-helper b/image/base-image/tool/log-helper new file mode 100755 index 0000000..ad1c527 --- /dev/null +++ b/image/base-image/tool/log-helper @@ -0,0 +1,121 @@ +#!/bin/bash -e + +# log helper base on environment variable CONTAINER_LOG_LEVEL +# CONTAINER_LOG_LEVEL environment variable is set by run tool based on --log-level argument (info by default) +# or you can set it directly with docker --env argument + +# Usage example: log-helper info CONTAINER_LOG_LEVEL is info or more +# the message "CONTAINER_LOG_LEVEL is info or more" will be printed only if log level is info, debug or trace + +LOG_LEVEL_NONE=0 +LOG_LEVEL_ERROR=1 +LOG_LEVEL_WARNING=2 +LOG_LEVEL_INFO=3 +LOG_LEVEL_DEBUG=4 +LOG_LEVEL_TRACE=5 + +# default log level if CONTAINER_LOG_LEVEL is not set -> info +log_level=${CONTAINER_LOG_LEVEL:-${LOG_LEVEL_INFO}} + +call=$1 # function to call (error, warning, info, debug, trace, level) +if [[ ! "$call" =~ ^(error|warning|info|debug|trace|level)$ ]]; then + echo "Error: Function $call not found" + echo "Allowed functions are: error, warning, info, debug, trace, level" + echo "usage example: log-helper info hello !" + exit 1 +fi + + +echo_msg="" # message to print if required log level is set +echo_param="" # echo command parameters + +function error() { + getEchoParams $@ + + if [ $log_level -ge 1 ]; then + echo $echo_param "$echo_msg" + fi +} + +function warning() { + getEchoParams $@ + + if [ $log_level -ge 2 ]; then + echo $echo_param "$echo_msg" + fi +} + +function info() { + getEchoParams $@ + + if [ $log_level -ge 3 ]; then + echo $echo_param "$echo_msg" + fi +} + +function debug() { + getEchoParams $@ + + if [ $log_level -ge 4 ]; then + echo $echo_param "$echo_msg" + fi +} + +function trace() { + getEchoParams $@ + + if [ $log_level -ge 5 ]; then + echo $echo_param "$echo_msg" + fi +} + +function getMsgFromStdin() { + if [ -z "$2" ]; then + echo_msg=$(cat) + fi +} + +function getEchoParams() { + + echo_msg="$@" + + if [[ "$1" =~ ^(-e|-n|-E)$ ]]; then + echo_param=$1 + echo_msg=${echo_msg#$1 } + fi + + # read from pipe if echo_msg is empty + [[ -n "$echo_msg" ]] || getMsgFromStdin +} + +function level() { + + local operator=$1 + local loglevel_str=$2 + local loglevel_str=${loglevel_str^^} # uppercase + + if [[ ! "$operator" =~ ^(eq|ne|gt|ge|lt|le)$ ]]; then + echo "Error: Operator $operator not allowed" + echo "Allowed operators are: eq, ne, gt, ge, lt, le" + echo "Help: http://www.tldp.org/LDP/abs/html/comparison-ops.html" + exit 1 + fi + + if [ -z "$loglevel_str" ]; then + echo "Error: No log level provided" + echo "Allowed log level are: none, error, warning, info, debug, trace" + echo "usage example: log-helper level eq info" + exit 1 + fi + + local log_level_var=LOG_LEVEL_$loglevel_str + + if [ $log_level -$operator ${!log_level_var} ]; then + exit 0 + else + exit 1 + fi +} + +shift +$call "$@" diff --git a/image/base-image/tool/run b/image/base-image/tool/run new file mode 100755 index 0000000..d254485 --- /dev/null +++ b/image/base-image/tool/run @@ -0,0 +1,930 @@ +#!/usr/bin/python3 -u +# -*- coding: utf-8 -*- + +import os, os.path, sys, stat, signal, errno, argparse, time, json, re, yaml, ast, socket, shutil, pwd, grp + +KILL_PROCESS_TIMEOUT = int(os.environ.get('KILL_PROCESS_TIMEOUT', 30)) +KILL_ALL_PROCESSES_TIMEOUT = int(os.environ.get('KILL_ALL_PROCESSES_TIMEOUT', 30)) + +LOG_LEVEL_NONE = 0 +LOG_LEVEL_ERROR = 1 +LOG_LEVEL_WARNING = 2 +LOG_LEVEL_INFO = 3 +LOG_LEVEL_DEBUG = 4 +LOG_LEVEL_TRACE = 5 + +SHENV_NAME_WHITELIST_REGEX = re.compile('\W') + +log_level = None + +environ_backup = dict(os.environ) +terminated_child_processes = {} + +IMPORT_STARTUP_FILENAME="startup.sh" +IMPORT_PROCESS_FILENAME="process.sh" +IMPORT_FINISH_FILENAME="finish.sh" + +IMPORT_ENVIRONMENT_DIR="/container/environment" +IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR="/container/environment/startup" + +ENV_FILES_YAML_EXTENSIONS = ('.yaml', '.startup.yaml') +ENV_FILES_JSON_EXTENSIONS = ('.json', '.startup.json') +ENV_FILES_STARTUP_EXTENSIONS = ('.startup.yaml', '.startup.json') + +IMPORT_SERVICE_DIR="/container/service" + +RUN_DIR="/container/run" +RUN_STATE_DIR = RUN_DIR + "/state" +RUN_ENVIRONMENT_DIR = RUN_DIR + "/environment" +RUN_ENVIRONMENT_FILE_EXPORT = RUN_DIR + "/environment.sh" +RUN_STARTUP_DIR = RUN_DIR + "/startup" +RUN_STARTUP_FINAL_FILE = RUN_DIR + "/startup.sh" +RUN_PROCESS_DIR = RUN_DIR + "/process" +RUN_SERVICE_DIR = RUN_DIR + "/service" + +ENVIRONMENT_LOG_LEVEL_KEY = 'CONTAINER_LOG_LEVEL' +ENVIRONMENT_SERVICE_DIR_KEY = 'CONTAINER_SERVICE_DIR' +ENVIRONMENT_STATE_DIR_KEY = 'CONTAINER_STATE_DIR' + +class AlarmException(Exception): + pass + +def error(message): + if log_level >= LOG_LEVEL_ERROR: + sys.stderr.write("*** %s\n" % message) + +def warning(message): + if log_level >= LOG_LEVEL_WARNING: + sys.stderr.write("*** %s\n" % message) + +def info(message): + if log_level >= LOG_LEVEL_INFO: + sys.stderr.write("*** %s\n" % message) + +def debug(message): + if log_level >= LOG_LEVEL_DEBUG: + sys.stderr.write("*** %s\n" % message) + +def trace(message): + if log_level >= LOG_LEVEL_TRACE: + sys.stderr.write("*** %s\n" % message) + +def debug_env_dump(): + debug("------------ Environment dump ------------") + for name, value in list(os.environ.items()): + debug(name + " = " + value) + debug("------------------------------------------") + +def ignore_signals_and_raise_keyboard_interrupt(signame): + signal.signal(signal.SIGTERM, signal.SIG_IGN) + signal.signal(signal.SIGINT, signal.SIG_IGN) + raise KeyboardInterrupt(signame) + +def raise_alarm_exception(): + raise AlarmException('Alarm') + +def listdir(path): + try: + result = os.stat(path) + except OSError: + return [] + if stat.S_ISDIR(result.st_mode): + return sorted(os.listdir(path)) + else: + return [] + +def is_exe(path): + try: + return os.path.isfile(path) and os.access(path, os.X_OK) + except OSError: + return False + +def xstr(s): + if s is None: + return '' + return str(s) + +def set_env_hostname_to_etc_hosts(): + try: + if "HOSTNAME" in os.environ: + socket_hostname = socket.gethostname() + + if os.environ["HOSTNAME"] != socket_hostname: + ip_address = socket.gethostbyname(socket_hostname) + with open("/etc/hosts", "a") as myfile: + myfile.write(ip_address+" "+os.environ["HOSTNAME"]+"\n") + except: + trace("set_env_hostname_to_etc_hosts: failed at some point...") + +def python_dict_to_bash_envvar(name, python_dict): + + for value in python_dict: + python_to_bash_envvar(name+"_KEY", value) + python_to_bash_envvar(name+"_VALUE", python_dict.get(value)) + + values = "#COMPLEX_BASH_ENV:ROW: "+name+"_KEY "+name+"_VALUE" + os.environ[name] = xstr(values) + trace("python2bash : set : " + name + " = "+ os.environ[name]) + +def python_list_to_bash_envvar(name, python_list): + + values="#COMPLEX_BASH_ENV:TABLE:" + + i=1 + for value in python_list: + child_name = name + "_ROW_" + str(i) + values += " " + child_name + python_to_bash_envvar(child_name, value) + i = i +1 + + os.environ[name] = xstr(values) + trace("python2bash : set : " + name + " = "+ os.environ[name]) + +def python_to_bash_envvar(name, value): + + try: + value = ast.literal_eval(value) + except: + pass + + if isinstance(value, list): + python_list_to_bash_envvar(name,value) + + elif isinstance(value, dict): + python_dict_to_bash_envvar(name,value) + + else: + os.environ[name] = xstr(value) + trace("python2bash : set : " + name + " = "+ os.environ[name]) + +def decode_python_envvars(): + _environ = dict(os.environ) + for name, value in list(_environ.items()): + if value.startswith("#PYTHON2BASH:") : + value = value.replace("#PYTHON2BASH:","",1) + python_to_bash_envvar(name, value) + +def decode_json_envvars(): + _environ = dict(os.environ) + for name, value in list(_environ.items()): + if value.startswith("#JSON2BASH:") : + value = value.replace("#JSON2BASH:","",1) + try: + value = json.loads(value) + python_to_bash_envvar(name,value) + except: + os.environ[name] = xstr(value) + warning("failed to parse : " + xstr(value)) + trace("set : " + name + " = "+ os.environ[name]) + +def decode_envvars(): + decode_json_envvars() + decode_python_envvars() + +def generic_import_envvars(path, override_existing_environment): + if not os.path.exists(path): + trace("generic_import_envvars "+ path+ " don't exists") + return + new_env = {} + for envfile in listdir(path): + filePath = path + os.sep + envfile + if os.path.isfile(filePath) and "." not in envfile: + name = os.path.basename(envfile) + with open(filePath, "r") as f: + # Text files often end with a trailing newline, which we + # don't want to include in the env variable value. See + # https://github.com/phusion/baseimage-docker/pull/49 + value = re.sub('\n\Z', '', f.read()) + new_env[name] = value + trace("import " + name + " from " + filePath + " --- ") + + for name, value in list(new_env.items()): + if override_existing_environment or name not in os.environ: + os.environ[name] = value + trace("set : " + name + " = "+ os.environ[name]) + else: + debug("ignore : " + name + " = " + xstr(value) + " (keep " + name + " = " + os.environ[name] + " )") + +def import_run_envvars(): + clear_environ() + generic_import_envvars(RUN_ENVIRONMENT_DIR, True) + +def import_envvars(): + generic_import_envvars(IMPORT_ENVIRONMENT_DIR, False) + generic_import_envvars(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR, False) + +def export_run_envvars(to_dir = True): + if to_dir and not os.path.exists(RUN_ENVIRONMENT_DIR): + warning("export_run_envvars: "+RUN_ENVIRONMENT_DIR+" don't exists") + return + shell_dump = "" + for name, value in list(os.environ.items()): + if name in ['USER', 'GROUP', 'UID', 'GID', 'SHELL']: + continue + if to_dir: + with open(RUN_ENVIRONMENT_DIR + os.sep + name, "w") as f: + f.write(value) + trace("export " + name + " to " + RUN_ENVIRONMENT_DIR + os.sep + name + " --- ") + shell_dump += "export " + sanitize_shenvname(name) + "=" + shquote(value) + "\n" + + with open(RUN_ENVIRONMENT_FILE_EXPORT, "w") as f: + f.write(shell_dump) + trace("export "+RUN_ENVIRONMENT_FILE_EXPORT+" --- ") + +def create_run_envvars(): + set_dir_env() + set_log_level_env() + import_envvars() + import_env_files() + decode_envvars() + export_run_envvars() + +def clear_run_envvars(): + try: + shutil.rmtree(RUN_ENVIRONMENT_DIR) + os.makedirs(RUN_ENVIRONMENT_DIR) + os.chmod(RUN_ENVIRONMENT_DIR, 700) + except: + warning("clear_run_envvars: failed at some point...") + +def print_env_files_order(file_extensions): + + if not os.path.exists(IMPORT_ENVIRONMENT_DIR): + warning("print_env_files_order "+IMPORT_ENVIRONMENT_DIR+" don't exists") + return + + to_print = 'Caution: previously defined variables will not be overriden.\n' + + file_found = False + for subdir, dirs, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)): + for file in files: + filepath = subdir + os.sep + file + if filepath.endswith(file_extensions): + file_found = True + filepath = subdir + os.sep + file + to_print += filepath + '\n' + + if file_found: + if log_level < LOG_LEVEL_DEBUG: + to_print+='\nTo see how this files are processed and environment variables values,\n' + to_print+='run this container with \'--loglevel debug\'' + + info('Environment files will be proccessed in this order : \n' + to_print) + +def import_env_files(): + + if not os.path.exists(IMPORT_ENVIRONMENT_DIR): + warning("import_env_files: "+IMPORT_ENVIRONMENT_DIR+" don't exists") + return + + file_extensions = ENV_FILES_YAML_EXTENSIONS + ENV_FILES_JSON_EXTENSIONS + print_env_files_order(file_extensions) + + for subdir, dirs, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)): + for file in files: + if file.endswith(file_extensions): + filepath = subdir + os.sep + file + + try: + with open(filepath, "r") as f: + + debug("--- process file : " + filepath+ " ---") + + if file.endswith(ENV_FILES_YAML_EXTENSIONS): + env_vars = yaml.load(f) + + elif file.endswith(ENV_FILES_JSON_EXTENSIONS): + env_vars = json.load(f) + + for name, value in list(env_vars.items()): + if not name in os.environ: + if isinstance(value, list) or isinstance(value, dict): + os.environ[name] = '#PYTHON2BASH:' + xstr(value) + else: + os.environ[name] = xstr(value) + trace("set : " + name + " = "+ os.environ[name]) + else: + debug("ignore : " + name + " = " + xstr(value) + " (keep " + name + " = " + os.environ[name] + " )") + except: + warning('failed to parse: ' + filepath) + +def remove_startup_env_files(): + + if os.path.isdir(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR): + try: + shutil.rmtree(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR) + except: + warning("remove_startup_env_files: failed to remove "+IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR) + + if not os.path.exists(IMPORT_ENVIRONMENT_DIR): + warning("remove_startup_env_files: "+IMPORT_ENVIRONMENT_DIR+" don't exists") + return + + for subdir, dirs, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)): + for file in files: + filepath = subdir + os.sep + file + if filepath.endswith(ENV_FILES_STARTUP_EXTENSIONS): + try: + os.remove(filepath) + info("Remove file "+filepath) + except: + warning("remove_startup_env_files: failed to remove "+filepath) + +def restore_environ(): + clear_environ() + trace("--- Restore initial environment ---") + os.environ.update(environ_backup) + +def clear_environ(): + trace("--- Clear existing environment ---") + os.environ.clear() + +def set_startup_scripts_env(): + info("Set environment for startup files") + clear_run_envvars() # clear previous environment + create_run_envvars() # create run envvars with all env files + +def set_process_env(keep_startup_env = False): + info("Set environment for container process") + if not keep_startup_env: + remove_startup_env_files() + clear_run_envvars() + + restore_environ() + create_run_envvars() # recreate env var without startup env files + +def setup_run_directories(args): + + directories = (RUN_PROCESS_DIR, RUN_STARTUP_DIR, RUN_STATE_DIR, RUN_ENVIRONMENT_DIR) + for directory in directories: + if not os.path.exists(directory): + os.makedirs(directory) + + if directory == RUN_ENVIRONMENT_DIR: + os.chmod(directory, 700) + + if not os.path.exists(RUN_ENVIRONMENT_FILE_EXPORT): + open(RUN_ENVIRONMENT_FILE_EXPORT, 'a').close() + os.chmod(RUN_ENVIRONMENT_FILE_EXPORT, 640) + uid = pwd.getpwnam("root").pw_uid + gid = grp.getgrnam("docker_env").gr_gid + os.chown(RUN_ENVIRONMENT_FILE_EXPORT, uid, gid) + + if state_is_first_start(): + + if args.copy_service: + copy_service_to_run_dir() + + set_dir_env() + + base_path = os.environ[ENVIRONMENT_SERVICE_DIR_KEY] + nb_service = len(listdir(base_path)) + + if nb_service > 0 : + info("Search service in " + ENVIRONMENT_SERVICE_DIR_KEY + " = "+base_path+" :") + for d in listdir(base_path): + d_path = base_path + os.sep + d + if os.path.isdir(d_path): + if is_exe(d_path + os.sep + IMPORT_STARTUP_FILENAME): + info('link ' + d_path + os.sep + IMPORT_STARTUP_FILENAME + ' to ' + RUN_STARTUP_DIR + os.sep + d) + try: + os.symlink(d_path + os.sep + IMPORT_STARTUP_FILENAME, RUN_STARTUP_DIR + os.sep + d) + except OSError as detail: + warning('failed to link ' + d_path + os.sep + IMPORT_STARTUP_FILENAME + ' to ' + RUN_STARTUP_DIR + os.sep + d + ': ' + xstr(detail)) + + if is_exe(d_path + os.sep + IMPORT_PROCESS_FILENAME): + info('link ' + d_path + os.sep + IMPORT_PROCESS_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'run') + + if not os.path.exists(RUN_PROCESS_DIR + os.sep + d): + os.makedirs(RUN_PROCESS_DIR + os.sep + d) + else: + warning('directory ' + RUN_PROCESS_DIR + os.sep + d + ' already exists') + + try: + os.symlink(d_path + os.sep + IMPORT_PROCESS_FILENAME, RUN_PROCESS_DIR + os.sep + d + os.sep + 'run') + except OSError as detail: + warning('failed to link ' + d_path + os.sep + IMPORT_PROCESS_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'run : ' + xstr(detail)) + + if not args.skip_finish_files and is_exe(d_path + os.sep + IMPORT_FINISH_FILENAME): + info('link ' + d_path + os.sep + IMPORT_FINISH_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish') + + if not os.path.exists(RUN_PROCESS_DIR + os.sep + d): + os.makedirs(RUN_PROCESS_DIR + os.sep + d) + + try: + os.symlink(d_path + os.sep + IMPORT_FINISH_FILENAME, RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish') + except OSError as detail: + warning('failed to link ' + d_path + os.sep + IMPORT_FINISH_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish : ' + xstr(detail)) + +def set_dir_env(): + if state_is_service_copied_to_run_dir(): + os.environ[ENVIRONMENT_SERVICE_DIR_KEY] = RUN_SERVICE_DIR + else: + os.environ[ENVIRONMENT_SERVICE_DIR_KEY] = IMPORT_SERVICE_DIR + trace("set : " + ENVIRONMENT_SERVICE_DIR_KEY + " = " + os.environ[ENVIRONMENT_SERVICE_DIR_KEY]) + + os.environ[ENVIRONMENT_STATE_DIR_KEY] = RUN_STATE_DIR + trace("set : " + ENVIRONMENT_STATE_DIR_KEY + " = " + os.environ[ENVIRONMENT_STATE_DIR_KEY]) + +def set_log_level_env(): + os.environ[ENVIRONMENT_LOG_LEVEL_KEY] = xstr(log_level) + trace("set : "+ENVIRONMENT_LOG_LEVEL_KEY+" = " + os.environ[ENVIRONMENT_LOG_LEVEL_KEY]) + +def copy_service_to_run_dir(): + + if os.path.exists(RUN_SERVICE_DIR): + warning("Copy "+IMPORT_SERVICE_DIR+" to "+RUN_SERVICE_DIR + " ignored") + warning(RUN_SERVICE_DIR + " already exists") + return + + info("Copy "+IMPORT_SERVICE_DIR+" to "+RUN_SERVICE_DIR) + + try: + shutil.copytree(IMPORT_SERVICE_DIR, RUN_SERVICE_DIR) + except shutil.Error as e: + warning(e) + + state_set_service_copied_to_run_dir() + +def state_set_service_copied_to_run_dir(): + open(RUN_STATE_DIR+"/service-copied-to-run-dir", 'a').close() + +def state_is_service_copied_to_run_dir(): + return os.path.exists(RUN_STATE_DIR+'/service-copied-to-run-dir') + +def state_set_first_startup_done(): + open(RUN_STATE_DIR+"/first-startup-done", 'a').close() + +def state_is_first_start(): + return os.path.exists(RUN_STATE_DIR+'/first-startup-done') == False + +def state_set_startup_done(): + open(RUN_STATE_DIR+"/startup-done", 'a').close() + +def state_reset_startup_done(): + try: + os.remove(RUN_STATE_DIR+"/startup-done") + except OSError: + pass + +def is_multiple_process_container(): + return len(listdir(RUN_PROCESS_DIR)) > 1 + +def is_single_process_container(): + return len(listdir(RUN_PROCESS_DIR)) == 1 + +def get_container_process(): + for p in listdir(RUN_PROCESS_DIR): + return RUN_PROCESS_DIR + os.sep + p + os.sep + 'run' + +def is_runit_installed(): + return os.path.exists('/usr/bin/sv') + +_find_unsafe = re.compile(r'[^\w@%+=:,./-]').search + +def shquote(s): + """Return a shell-escaped version of the string *s*.""" + if not s: + return "''" + if _find_unsafe(s) is None: + return s + + # use single quotes, and put single quotes into double quotes + # the string $'b is then quoted as '$'"'"'b' + return "'" + s.replace("'", "'\"'\"'") + "'" + +def sanitize_shenvname(s): + return re.sub(SHENV_NAME_WHITELIST_REGEX, "_", s) + +# Waits for the child process with the given PID, while at the same time +# reaping any other child processes that have exited (e.g. adopted child +# processes that have terminated). +def waitpid_reap_other_children(pid): + global terminated_child_processes + + status = terminated_child_processes.get(pid) + if status: + # A previous call to waitpid_reap_other_children(), + # with an argument not equal to the current argument, + # already waited for this process. Return the status + # that was obtained back then. + del terminated_child_processes[pid] + return status + + done = False + status = None + while not done: + try: + # https://github.com/phusion/baseimage-docker/issues/151#issuecomment-92660569 + this_pid, status = os.waitpid(pid, os.WNOHANG) + if this_pid == 0: + this_pid, status = os.waitpid(-1, 0) + if this_pid == pid: + done = True + else: + # Save status for later. + terminated_child_processes[this_pid] = status + except OSError as e: + if e.errno == errno.ECHILD or e.errno == errno.ESRCH: + return None + else: + raise + return status + +def stop_child_process(name, pid, signo = signal.SIGTERM, time_limit = KILL_PROCESS_TIMEOUT): + info("Shutting down %s (PID %d)..." % (name, pid)) + try: + os.kill(pid, signo) + except OSError: + pass + signal.alarm(time_limit) + try: + try: + waitpid_reap_other_children(pid) + except OSError: + pass + except AlarmException: + warning("%s (PID %d) did not shut down in time. Forcing it to exit." % (name, pid)) + try: + os.kill(pid, signal.SIGKILL) + except OSError: + pass + try: + waitpid_reap_other_children(pid) + except OSError: + pass + finally: + signal.alarm(0) + +def run_command_killable(command): + status = None + debug_env_dump() + pid = os.spawnvp(os.P_NOWAIT, command[0], command) + try: + status = waitpid_reap_other_children(pid) + except BaseException: + warning("An error occurred. Aborting.") + stop_child_process(command[0], pid) + raise + if status != 0: + if status is None: + error("%s exited with unknown status\n" % command[0]) + else: + error("%s failed with status %d\n" % (command[0], os.WEXITSTATUS(status))) + sys.exit(1) + +def run_command_killable_and_import_run_envvars(command): + run_command_killable(command) + import_run_envvars() + export_run_envvars(False) + +def kill_all_processes(time_limit): + info("Killing all processes...") + try: + os.kill(-1, signal.SIGTERM) + except OSError: + pass + signal.alarm(time_limit) + try: + # Wait until no more child processes exist. + done = False + while not done: + try: + os.waitpid(-1, 0) + except OSError as e: + if e.errno == errno.ECHILD: + done = True + else: + raise + except AlarmException: + warning("Not all processes have exited in time. Forcing them to exit.") + try: + os.kill(-1, signal.SIGKILL) + except OSError: + pass + finally: + signal.alarm(0) + +def container_had_startup_script(): + return (len(listdir(RUN_STARTUP_DIR)) > 0 or is_exe(RUN_STARTUP_FINAL_FILE)) + +def run_startup_files(args): + + # Run /container/run/startup/* + for name in listdir(RUN_STARTUP_DIR): + filename = RUN_STARTUP_DIR + os.sep + name + if is_exe(filename): + info("Running %s..." % filename) + run_command_killable_and_import_run_envvars([filename]) + + # Run /container/run/startup.sh. + if is_exe(RUN_STARTUP_FINAL_FILE): + info("Running "+RUN_STARTUP_FINAL_FILE+"...") + run_command_killable_and_import_run_envvars([RUN_STARTUP_FINAL_FILE]) + +def wait_for_process_or_interrupt(pid): + status = waitpid_reap_other_children(pid) + return (True, status) + +def run_process(args, background_process_name, background_process_command): + background_process_pid = run_background_process(background_process_name,background_process_command) + background_process_exited = False + exit_status = None + + if len(args.main_command) == 0: + background_process_exited, exit_status = wait_background_process(background_process_name, background_process_pid) + else: + exit_status = run_foreground_process(args.main_command) + + return background_process_pid, background_process_exited, exit_status + +def run_background_process(name, command): + info("Running "+ name +"...") + pid = os.spawnvp(os.P_NOWAIT, command[0], command) + debug("%s started as PID %d" % (name, pid)) + return pid + +def wait_background_process(name, pid): + exit_code = None + exit_status = None + process_exited = False + + process_exited, exit_code = wait_for_process_or_interrupt(pid) + if process_exited: + if exit_code is None: + info(name + " exited with unknown status") + exit_status = 1 + else: + exit_status = os.WEXITSTATUS(exit_code) + info("%s exited with status %d" % (name, exit_status)) + return (process_exited, exit_status) + +def run_foreground_process(command): + exit_code = None + exit_status = None + + info("Running %s..." % " ".join(command)) + pid = os.spawnvp(os.P_NOWAIT, command[0], command) + try: + exit_code = waitpid_reap_other_children(pid) + if exit_code is None: + info("%s exited with unknown status." % command[0]) + exit_status = 1 + else: + exit_status = os.WEXITSTATUS(exit_code) + info("%s exited with status %d." % (command[0], exit_status)) + except KeyboardInterrupt: + stop_child_process(command[0], pid) + raise + except BaseException: + warning("An error occurred. Aborting.") + stop_child_process(command[0], pid) + raise + + return exit_status + +def shutdown_runit_services(): + debug("Begin shutting down runit services...") + os.system("/usr/bin/sv -w %d force-stop %s/* > /dev/null" % (KILL_PROCESS_TIMEOUT, RUN_PROCESS_DIR)) + +def wait_for_runit_services(): + debug("Waiting for runit services to exit...") + done = False + while not done: + done = os.system("/usr/bin/sv status "+RUN_PROCESS_DIR+"/* | grep -q '^run:'") != 0 + if not done: + time.sleep(0.1) + shutdown_runit_services() + +def run_multiple_process_container(args): + if not is_runit_installed(): + error("Error: runit is not installed and this is a multiple process container.") + return + + background_process_exited=False + background_process_pid=None + + try: + runit_command=["/usr/bin/runsvdir", "-P", RUN_PROCESS_DIR] + background_process_pid, background_process_exited, exit_status = run_process(args, "runit daemon", runit_command) + + sys.exit(exit_status) + finally: + shutdown_runit_services() + if not background_process_exited: + stop_child_process("runit daemon", background_process_pid) + wait_for_runit_services() + +def run_single_process_container(args): + background_process_exited=False + background_process_pid=None + + try: + container_process=get_container_process(); + background_process_pid, background_process_exited, exit_status = run_process(args, container_process, [container_process]) + + sys.exit(exit_status) + finally: + if not background_process_exited: + stop_child_process(container_process, background_process_pid) + +def run_no_process_container(args): + if len(args.main_command) == 0: + args.main_command=['bash'] # run bash by default + + exit_status = run_foreground_process(args.main_command) + sys.exit(exit_status) + +def run_finish_files(): + + # iterate process dir to find finish files + for name in listdir(RUN_PROCESS_DIR): + filename = RUN_PROCESS_DIR + os.sep + name + os.sep + "finish" + if is_exe(filename): + info("Running %s..." % filename) + run_command_killable_and_import_run_envvars([filename]) + +def wait_states(states): + for state in states: + filename = RUN_STATE_DIR + os.sep + state + info("Wait state: " + state) + + while not os.path.exists(filename): + time.sleep(0.1) + debug("Check file " + filename) + pass + debug("Check file " + filename + " [Ok]") + +def run_cmds(args, when): + debug("Run commands before " + when + "...") + if len(args.cmds) > 0: + + for cmd in args.cmds: + if (len(cmd) > 1 and cmd[1] == when) or (len(cmd) == 1 and when == "startup"): + info("Running '"+cmd[0]+"'...") + run_command_killable_and_import_run_envvars(cmd[0].split()) + +def main(args): + + info(ENVIRONMENT_LOG_LEVEL_KEY + " = " + xstr(log_level) + " (" + log_level_switcher_inv.get(log_level) + ")") + state_reset_startup_done() + + if args.set_env_hostname_to_etc_hosts: + set_env_hostname_to_etc_hosts() + + wait_states(args.wait_states) + setup_run_directories(args) + + if not args.skip_env_files: + set_startup_scripts_env() + + run_cmds(args,"startup") + + if not args.skip_startup_files and container_had_startup_script(): + run_startup_files(args) + + state_set_startup_done() + state_set_first_startup_done() + + if not args.skip_env_files: + set_process_env(args.keep_startup_env) + + run_cmds(args,"process") + + debug_env_dump() + + if is_single_process_container() and not args.skip_process_files: + run_single_process_container(args) + + elif is_multiple_process_container() and not args.skip_process_files: + run_multiple_process_container(args) + + else: + run_no_process_container(args) + +# Parse options. +parser = argparse.ArgumentParser(description = 'Initialize the system.', epilog='Osixia! Light Baseimage: https://github.com/osixia/docker-light-baseimage') +parser.add_argument('main_command', metavar = 'MAIN_COMMAND', type = str, nargs = '*', + help = 'The main command to run, leave empty to only run container process.') +parser.add_argument('-e', '--skip-env-files', dest = 'skip_env_files', + action = 'store_const', const = True, default = False, + help = 'Skip getting environment values from environment file(s).') +parser.add_argument('-s', '--skip-startup-files', dest = 'skip_startup_files', + action = 'store_const', const = True, default = False, + help = 'Skip running '+RUN_STARTUP_DIR+'/* and '+RUN_STARTUP_FINAL_FILE + ' file(s).') +parser.add_argument('-p', '--skip-process-files', dest = 'skip_process_files', + action = 'store_const', const = True, default = False, + help = 'Skip running container process file(s).') +parser.add_argument('-f', '--skip-finish-files', dest = 'skip_finish_files', + action = 'store_const', const = True, default = False, + help = 'Skip running container finish file(s).') +parser.add_argument('-o', '--run-only', type=str, choices=["startup","process","finish"], dest = 'run_only', default = None, + help = 'Run only this file type and ignore others.') +parser.add_argument('-c', '--cmd', metavar=('COMMAND', 'WHEN={startup,process,finish}'), dest = 'cmds', type = str, + action = 'append', default = [], nargs = "+", + help = 'Run this command before WHEN file(s). Default before startup file(s).') +parser.add_argument('-k', '--no-kill-all-on-exit', dest = 'kill_all_on_exit', + action = 'store_const', const = False, default = True, + help = 'Don\'t kill all processes on the system upon exiting.') +parser.add_argument('--wait-state', metavar = 'FILENAME', dest = 'wait_states', type = str, + action = 'append', default=[], + help = 'Wait until the container state file exists in '+RUN_STATE_DIR+' directory before starting. Usefull when 2 containers share '+RUN_DIR+' directory via volume.') +parser.add_argument('--wait-first-startup', dest = 'wait_first_startup', + action = 'store_const', const = True, default = False, + help = 'Wait until the first startup is done before starting. Usefull when 2 containers share '+RUN_DIR+' directory via volume.') +parser.add_argument('--keep-startup-env', dest = 'keep_startup_env', + action = 'store_const', const = True, default = False, + help = 'Don\'t remove ' + xstr(ENV_FILES_STARTUP_EXTENSIONS) + ' environment files after startup scripts.') +parser.add_argument('--copy-service', dest = 'copy_service', + action = 'store_const', const = True, default = False, + help = 'Copy '+IMPORT_SERVICE_DIR+' to '+RUN_SERVICE_DIR+'. Help to fix docker mounted files problems.') +parser.add_argument('--dont-touch-etc-hosts', dest = 'set_env_hostname_to_etc_hosts', + action = 'store_const', const = False, default = True, + help = 'Don\'t add in /etc/hosts a line with the container ip and $HOSTNAME environment variable value.') +parser.add_argument('--keepalive', dest = 'keepalive', + action = 'store_const', const = True, default = False, + help = 'Keep alive container if all startup files and process exited without error.') +parser.add_argument('--keepalive-force', dest = 'keepalive_force', + action = 'store_const', const = True, default = False, + help = 'Keep alive container in all circonstancies.') +parser.add_argument('-l', '--loglevel', type=str, choices=["none","error","warning","info","debug","trace"], dest = 'log_level', default = "info", + help = 'Log level (default: info)') + +args = parser.parse_args() + +log_level_switcher = {"none": LOG_LEVEL_NONE,"error": LOG_LEVEL_ERROR,"warning": LOG_LEVEL_WARNING,"info": LOG_LEVEL_INFO,"debug": LOG_LEVEL_DEBUG, "trace": LOG_LEVEL_TRACE} +log_level_switcher_inv = {LOG_LEVEL_NONE: "none",LOG_LEVEL_ERROR:"error",LOG_LEVEL_WARNING:"warning",LOG_LEVEL_INFO:"info",LOG_LEVEL_DEBUG:"debug",LOG_LEVEL_TRACE:"trace"} +log_level = log_level_switcher.get(args.log_level) + +# Run only arg +if args.run_only != None: + if args.run_only == "startup" and args.skip_startup_files: + error("Error: When '--run-only startup' is set '--skip-startup-files' can't be set.") + sys.exit(1) + elif args.run_only == "process" and args.skip_startup_files: + error("Error: When '--run-only process' is set '--skip-process-files' can't be set.") + sys.exit(1) + elif args.run_only == "finish" and args.skip_startup_files: + error("Error: When '--run-only finish' is set '--skip-finish-files' can't be set.") + sys.exit(1) + + if args.run_only == "startup": + args.skip_process_files = True + args.skip_finish_files = True + elif args.run_only == "process": + args.skip_startup_files = True + args.skip_finish_files = True + elif args.run_only == "finish": + args.skip_startup_files = True + args.skip_process_files = True + +# wait for startup args +if args.wait_first_startup: + args.wait_states.insert(0, 'first-startup-done') + +# Run main function. +signal.signal(signal.SIGTERM, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGTERM')) +signal.signal(signal.SIGINT, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGINT')) +signal.signal(signal.SIGALRM, lambda signum, frame: raise_alarm_exception()) + +exit_code = 0 + +try: + main(args) + +except SystemExit as err: + exit_code = err.code + if args.keepalive and err.code == 0: + try: + info("All process have exited without error, keep container alive...") + while True: + time.sleep(60) + pass + except: + info("Keep alive process ended.") + +except KeyboardInterrupt: + warning("Init system aborted.") + exit(2) + +finally: + + run_cmds(args,"finish") + + # for multiple process images finish script are run by runit + if not args.skip_finish_files and not is_multiple_process_container(): + run_finish_files() + + if args.keepalive_force: + try: + info("All process have exited, keep container alive...") + while True: + time.sleep(60) + pass + except: + info("Keep alive process ended.") + + if args.kill_all_on_exit: + kill_all_processes(KILL_ALL_PROCESSES_TIMEOUT) + + exit(exit_code) diff --git a/image/base-image/tool/setuser b/image/base-image/tool/setuser new file mode 100755 index 0000000..06d7430 --- /dev/null +++ b/image/base-image/tool/setuser @@ -0,0 +1,64 @@ +#!/usr/bin/python3 + +''' +Copyright (c) 2013-2015 Phusion Holding B.V. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +''' + +import sys +import os +import pwd + + +def abort(message): + sys.stderr.write("setuser: %s\n" % message) + sys.exit(1) + + +def main(): + ''' + A simple alternative to sudo that executes a command as a user by setting + the user ID and user parameters to those described by the system and then + using execvp(3) to execute the command without the necessity of a TTY + ''' + + username = sys.argv[1] + try: + user = pwd.getpwnam(username) + except KeyError: + abort("user %s not found" % username) + os.initgroups(username, user.pw_gid) + os.setgid(user.pw_gid) + os.setuid(user.pw_uid) + os.environ['USER'] = username + os.environ['HOME'] = user.pw_dir + os.environ['UID'] = str(user.pw_uid) + try: + os.execvp(sys.argv[2], sys.argv[2:]) + except OSError as e: + abort("cannot execute %s: %s" % (sys.argv[2], str(e))) + +if __name__ == '__main__': + + if len(sys.argv) < 3: + sys.stderr.write("Usage: /sbin/setuser USERNAME COMMAND [args..]\n") + sys.exit(1) + + main() diff --git a/image/base-image/tool/wait-process b/image/base-image/tool/wait-process new file mode 100755 index 0000000..8ac03f7 --- /dev/null +++ b/image/base-image/tool/wait-process @@ -0,0 +1,16 @@ +#!/bin/sh -e + +# wait startup to finish +while ! test -f /container/run/state/startup-done +do + sleep 0.5 +done + +for process in "$@" +do + # wait service + while ! pgrep -c "${process}" > /dev/null + do + sleep 0.5 + done +done
