From 217c67a43c3bea596591e0bc755075e00814243e Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 29 Jul 2018 19:45:40 +0100 Subject: [PATCH 01/98] ISSUE 173: Adds version 2, CentOS-7 7.4.1708 x86_64 - MySQL 5.7 Community Server. --- CHANGELOG.md | 185 +----------------- Dockerfile | 22 ++- README-short.txt | 2 +- README.md | 47 +++-- environment.mk | 4 +- .../systemd/system/centos-ssh-mysql@.service | 2 +- src/opt/scmi/environment.sh | 4 +- src/usr/bin/healthcheck | 4 +- src/usr/sbin/mysqld-bootstrap | 38 +++- src/usr/sbin/mysqld-wrapper | 10 +- test/shpec/operation_shpec.sh | 118 +++++++---- 11 files changed, 165 insertions(+), 271 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e89f9c9..b7b9d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,188 +1,11 @@ # Change Log -## centos-6 +## centos-7-mysql57-community -Summary of release changes for Version 1. +Summary of release changes for Version 2. -CentOS-6 6.9 x86_64 - MySQL 5.1. +CentOS-7 7.4.1708 x86_64 - MySQL 5.7 Community Server. -### 1.8.4 - 2018-05-13 - -- Updates source image to [1.8.4 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.8.4). -- Adds feature to set `MYSQL_ROOT_PASSWORD` via a file path. e.g. Docker Swarm secrets. -- Adds feature to set `MYSQL_USER_PASSWORD` via a file path. e.g. Docker Swarm secrets. - -### 1.8.3 - 2018-01-23 - -- Fixes issue with unusable healthcheck error messages. -- Fixes issue with healthcheck failure when `MYSQL_ROOT_PASSWORD` is set. - -### 1.8.2 - 2018-01-15 - -- Updates source image to [1.8.3 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.8.3). -- Adds a `.dockerignore` file. -- Adds minor correction to scmi default configuration file. -- Adds generic ready state test function. -- Adds increased database initialisation timeout; from 30 to 60 seconds. - -### 1.8.1 - 2017-09-16 - -- Updates source image to [1.8.2 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.8.2). -- Fixes path error in log output for custom initialisation SQL. - -### 1.8.0 - 2017-07-27 - -- Fixes issue with local readonly variables being writable. -- Removes undocumented `MYSQL_DATA_DIR_DEFAULT` variable. -- Removes undocumented `FORCE_MYSQL_INSTALL` variable. -- Removes requirement for gawk in scmi and systemd unit. -- Removes scmi; it's maintained [upstream](https://github.com/jdeathe/centos-ssh/blob/centos-6/src/usr/sbin/scmi). -- Replaces deprecated Dockerfile `MAINTAINER` with a `LABEL`. -- Updates source image to [1.8.1 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.8.1). -- Adds a `src` directory for the image root files. -- Adds `STARTUP_TIME` variable for the `logs-delayed` Makefile target. -- Adds use of `/var/lock/subsys/` (subsystem lock directory) for bootstrap lock files. -- Adds test case output with improved readability. -- Adds healthcheck. -- Adds `MYSQL_AUTOSTART_MYSQLD_WRAPPER` and `MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP` optionally disable process startup. -- Adds updated README images showing docker logs output for the initialisation SQL template and MySQL Details. -- Fixes issue with README example import of the Sakila MySQL example database. - -### 1.7.3 - 2017-05-12 - -- Updates upstream source to [1.7.6 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.7.6). -- Updates MySQL package to `mysql-server-5.1.73-8.el6_8`. -- Adds a change log (`CHANGELOG.md`). -- Adds support for semantic version numbered tags. -- Adds minor code style changes to the Makefile for readability. -- Adds support for running `shpec` functional tests with `make test`. - -### 1.7.2 - 2016-10-02 - -- Adds Makefile help target with usage instructions. -- Splits up the Makefile targets into internal and public types. -- Adds correct `scmi` path in usage instructions. -- Changes `PACKAGE_PATH` to `DIST_PATH` in line with the Makefile environment include. Not currently used by `scmi` but changing for consistency. -- Changes `DOCKER_CONTAINER_PARAMETERS_APPEND` to `DOCKER_CONTAINER_OPTS` for usability. This is a potentially breaking change that could affect systemd service configurations if using the Environment variable in a drop-in customisation. However, if using the systemd template unit-files it should be pinned to a specific version tag. The Makefile should only be used for development/testing and usage in `scmi` is internal only as the `--setopt` parameter is used to build up the optional container parameters. -- Removes X-Fleet section from template unit-file. - -### 1.7.1 - 2016-09-15 - -- Adds scmi and systemd configuration files to image. -- Adds option to disable publishing of 3306 in make/scmi and systemd templates. -- Updated README with some minor corrections and changes for consistency. -- Updates the install/uninstall metadata labels to use the correct path to the scmi script on the image to allow the atomic install/uninstall feature to work as expected. -- Updates README examples to use the 1.7.1 release version. -- Adds new `mysqld-wrapper` script which is more easily maintained than using inline code in the supervisord configuration. -- Adds general consistency improvements to the `mysqld-bootstrap` script. - -### 1.7.0 - 2016-09-06 - -- Updates upstream source to [1.7.0 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.7.0). -- Adds `scmi` and metadata for atomic install/uninstall usage. -- Removes deprecated run.sh and build.sh helper scripts. These have been replaced with the make targets `make` (or `make build`) and `make install start`. -- Removes support for and documentation on configuration volumes. These can still be implemented by making use of the `DOCKER_CONTAINER_PARAMETERS_APPEND` environment variable or using the `scmi` option `--setopt` to append additional docker parameters to the default docker create template. -- Changes systemd template unit-file environment variable for `DOCKER_IMAGE_PACKAGE_PATH` now defaults to the path `/var/opt/scmi/packages` instead of `/var/services-packages` however this can be reverted back using the `scmi` option `--env='DOCKER_IMAGE_PACKAGE_PATH="/var/services-packages"'` if necessary. -- Changes the recommended data volume name for mapping to the container path: `/var/lib/mysql` from `volume-data.mysql.pool-1.1.1` to `mysql.pool-1.1.1.data-mysql`. This makes it easier to sort output of `docker volume ls`. - -### 1.6.0 - 2016-09-05 - -- Updates upstream source image to [1.6.0 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.6.0) (i.e. CentOS-6.8). -- Improves readability of Dockerfile. -- Updates Makefile to fix issue running `make dist` without first creating the `PACKAGE_PATH`, (./packages/jdeathe), directory. -- Adds `DOCKER_CONTAINER_PARAMETERS_APPEND` to the Makefile create template. -- Adds an improvements to the optional etcd register template unit-file used in systemd installations. -- Adds `DOCKER_USER` to the systemd template unit-file environment variables and removes the docker username from the `DOCKER_IMAGE_NAME` for consistency. - -### 1.5.0 - 2016-07-09 - -- Updates upstream source image to [1.5.3 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.5.3). -- Updates mysql-server package to version 5.1.73-7.el6. -- Changes supervisord configuration and `mysqld-bootstrap` script. -- Splits out the docker helper functions used by the `build.sh` and `run.sh` scripts. -- Changes to start mysqld process after `mysqld-boostrap` completes successfully. -- Adds make files for docker build/run jobs. -- Adds updated systemd unit file template with optional etcd register companion service. -- Adds change to skip networking during `mysql-boostrap` initialisation. -- Adds `skip-name-resolve` to the default MySQL configuration to mitigate resolver issues causing database authentication failures. -- Adds support for multiline initialisation SQL. -- Adds more informative output to logs from `mysql-bootstrap`. -- Changes the SQL used to generate database,user,grants to be outside of the configuration file. -- Adds `MYSQL_ROOT_PASSWORD_HASHED` to allow for the use of a hashed root password. -- Adds `MYSQL_USER_PASSWORD_HASHED` to allow for the use of a hashed user password. -- Adds feature to redact password values from log output if operator supplied. - -### 1.4.2 - 2016-01-17 - -- Updates documentation with revised steps on how to implement the optional configuration "data" volume. Also revised for the application data volume. -- Updates the Systemd installation and definition scripts - no longer require the busybox image and made the installation easier to visualise by tailing the unit logs. Removes the necessity to create and populate the configuration data volume directory mount points. -- Removes the `run.sh` feature to automatically mount the configuration volume on the docker host using a full path and attempt to populate the directory locally. This was problematic since the path on the Docker host might not exist and the feature to automatically create paths when adding a volume mount is deprecated. Using `docker cp` to upload a directory to the configuration volume is the preferred approach. -- Updates `run.conf` such that only values are in the configuration file and added `VOLUME_CONFIG_ENABLED` to allow the "optional" configuration volume to be enabled if required instead of using it by default. Most essential settings can be implemented via the use of environment variables now. Also addes the variable `VOLUME_DATA_ENABLED` to allow the optional use of a named data container instead of defining the volume within the running application container. -- Addes `VOLUME_CONFIG_NAMED` and `VOLUME_DATA_NAMED` to `run.conf` to allow the operator to use named volumes and, if set to `true` the values are is used for the `docker_host_path` such that the volume is defined as: `-v volume_name:/container_path`. The recommended approach is to not define a host path or named volume if using a separate configuration/data container so that Docker manages the naming by only setting the container path: `-v /container_path`. -- Addes new `run.conf` variables `DOCKER_HOST_PORT_SSH` and `DOCKER_HOST_PORT_MYSQL` to allow the operator to easily change the values from those set in the `run.sh` helper script. - -### 1.4.1 - 2016-01-10 - -- Updates upstream source image to [1.4.1 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.4.1). -- Adds more consistent syntax across BASH scripts. -- Fixes an issue that prevented the `run.sh` from running correctly. -- Addes code comment example for how to use the `run.sh` script to run a command in a container at docker run time; This is helpful for debugging containers that don't stay running. -- Addes initialisation testing of database access for the `MYSQL_USER` user against the `MYSQL_USER_DATABASE` database if values are set. -- Updates README file with more recent image of the docker logs output. -- Adds examples of how to run the mysql shell and how to import data. - -### 1.4.0 - 2015-12-09 - -- Updates to CentOS 6.7. - -### 1.3.1 - 2015-12-09 - -- Updates upstream source image to [1.3.1 tag](https://github.com/jdeathe/centos-ssh/releases/tag/1.3.1). -- Adds delay to allow the custom initialisation SQL to run at startup. -- Adds better verification of completion of the initialisation process in the mysql-bootstrap script. -- Adds feature to allow first-load configuration with environment variables. Now the database name, user and password can be set on first run. -- Updates the docker network helper functions and moves them out of the configuration file. -- Updates the systemd install script to use helper functions + updates the service definition file to use etcd2. - -### 1.3.0 - 2015-08-03 - -- Change to build from a specified tag instead of branch. -- Change build to use specific package versions, add versionlock package and lock packages. -- Change location of the SSH configuration file to a subdirectory. -- Adds support for running and building on Mac Docker hosts (when using boot2docker). -- Adds a 2 second delay to supervisor's MySQL startup script to allow mysql-bootstrap to run first. -- Adds some improvements to the mysql-bootstrap script. - - Run the initial table installation in the background so the the initialisation SQL can be generated in the foreground. - - Removes the MySQL shutdown since this should not longer be necessary. - - Improves the readability by using heredoc syntax for the multiline text instead of lots of echo calls. - - The permissions on the MySQL data directory are used determine the service users UID/GID this was added for support on Mac hosts with either boot2docker or Kitematic. - - Moves the socket file out of the MySQL data directory. -- Adds some improvements to the run.sh helper script. - - Makes the configuration volume the same for both SSH and non-SSH containers. - - Defines the docker run command once and set up the port requirements for SSH if necessary. -- Fixes an issue with deprecated option warnings in the MySQL configuration. - -### 1.2.2 - 2015-07-27 - -- Fixes an issue with the init scripts/SQL not running due to Supervisord starting the mysqld process between database creation and running the init scripts. -- Adds configuration change to force a default UTF8 character set for the connection and results. - -### 1.2.1 - 2015-05-25 - -- Updates the systemd service file to reference the correct tag version. -- Fixes a few spelling errors in the README file. - -### 1.2.0 - 2015-05-03 - -*Note:* This should have been tagged as 1.1.0. - -- Updates CentOS to 6.6. -- Adds MIT License. - -### 1.0.1 - 2014-09-15 - -- Removes the SSH port mapping from the systemd unit file since SSH is not enabled by default. - -### 1.0.0 - 2014-09-14 +### 2.0.0 - Unreleased - Initial release. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7a9bb64..918c9a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,29 @@ # ============================================================================= # jdeathe/centos-ssh-mysql # -# CentOS-6, MySQL 5.1 +# CentOS-7, MySQL 5.7 Community Server # # ============================================================================= -FROM jdeathe/centos-ssh:1.8.4 +FROM jdeathe/centos-ssh:2.3.2 # ----------------------------------------------------------------------------- # Install MySQL # ----------------------------------------------------------------------------- -RUN rpm --rebuilddb \ +RUN { \ + echo '[mysql57-community]'; \ + echo 'name=MySQL 5.7 Community Server'; \ + echo 'baseurl=http://repo.mysql.com/yum/mysql-5.7-community/el/7/$basearch/'; \ + echo 'gpgcheck=1'; \ + echo 'enabled=1'; \ + echo 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql'; \ + } > /etc/yum.repos.d/mysql-community.repo \ + && rpm --import \ + https://repo.mysql.com/RPM-GPG-KEY-mysql \ && yum -y install \ --setopt=tsflags=nodocs \ --disableplugin=fastestmirror \ - mysql-server-5.1.73-8.el6_8 \ + mysql-community-server \ + psmisc \ && yum versionlock add \ mysql* \ && rm -rf /var/cache/yum/* \ @@ -73,7 +83,7 @@ ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true \ # ----------------------------------------------------------------------------- # Set image metadata # ----------------------------------------------------------------------------- -ARG RELEASE_VERSION="1.8.4" +ARG RELEASE_VERSION="2.0.0" LABEL \ maintainer="James Deathe " \ install="docker run \ @@ -102,7 +112,7 @@ jdeathe/centos-ssh-mysql:${RELEASE_VERSION} \ org.deathe.license="MIT" \ org.deathe.vendor="jdeathe" \ org.deathe.url="https://github.com/jdeathe/centos-ssh-mysql" \ - org.deathe.description="CentOS-6 6.9 x86_64 - MySQL 5.1." + org.deathe.description="CentOS-7 7.4.1708 x86_64 - MySQL 5.7 Community Server." HEALTHCHECK \ --interval=1s \ diff --git a/README-short.txt b/README-short.txt index 16f9149..312b3b7 100644 --- a/README-short.txt +++ b/README-short.txt @@ -1 +1 @@ -CentOS-6 6.9 x86_64 - MySQL. \ No newline at end of file +CentOS-7 7.4.1708 x86_64 - MySQL Community Server. \ No newline at end of file diff --git a/README.md b/README.md index c4b39ac..5fd3e02 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,19 @@ centos-ssh-mysql ================ -Docker Image of CentOS-6 6.9 x86_64, MySQL 5.1. +Docker Image including: +- CentOS-6 6.9 x86_64, MySQL 5.1. +- CentOS-7 7.4.1708 x86_64, MySQL 5.7 Community Server. Includes Automated password generation and an option for custom initialisation SQL. Supports custom configuration via environment variables. ## Overview & links -The latest CentOS-6 based release can be pulled from the centos-6 Docker tag. It is recommended to select a specific release tag - the convention is `centos-6-1.8.4` or `1.8.4` for the [1.8.4](https://github.com/jdeathe/centos-ssh-mysql/tree/1.8.4) release tag. +The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-community` Docker tag. It is recommended to select a specific release tag - the convention is `centos-7-mysql57-community-2.0.0` or `2.0.0` for the [2.0.0](https://github.com/jdeathe/centos-ssh-mysql/tree/2.0.0) release tag. ### Tags and respective `Dockerfile` links +- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.0.0`, `2.0.0` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) - `centos-6`, `centos-6-1.8.4`, `1.8.4` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) The Dockerfile can be used to build a base image that is the bases for several other docker images. @@ -29,7 +32,7 @@ SSH is not required in order to access a terminal for the running container. The $ docker exec -it {container-name-or-id} bash ``` -For cases where access to docker exec is not possible the preferred method is to use Command Keys and the nsenter command. See [command-keys.md](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/command-keys.md) for details on how to set this up. +For cases where access to docker exec is not possible the preferred method is to use Command Keys and the nsenter command. See [command-keys.md](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/command-keys.md) for details on how to set this up. ## Quick Example @@ -40,7 +43,7 @@ $ docker run -d \ --name mysql.pool-1.1.1 \ -p 3306:3306 \ -v /var/lib/mysql \ - jdeathe/centos-ssh-mysql:centos-6 + jdeathe/centos-ssh-mysql:2.0.0 ``` Now you can verify it is initialised and running successfully by inspecting the container's logs. @@ -51,9 +54,9 @@ $ docker logs mysql.pool-1.1.1 On the first run, there will be additional output showing the initialisation SQL template and, before mysqld-bootstrap completes, the MySQL Details which shows the configured database, if applicable, and any associated user credentials. -![Docker Logs - MySQL Initialisation SQL Template](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-6/images/docker-logs-mysqld-bootstrap-initialisation-sql.png) +![Docker Logs - MySQL Initialisation SQL Template](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-7-mysql57-community/images/docker-logs-mysqld-bootstrap-initialisation-sql.png) -![Docker Logs - MySQL Details](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-6/images/docker-logs-mysqld-bootstrap-details.png) +![Docker Logs - MySQL Details](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-7-mysql57-community/images/docker-logs-mysqld-bootstrap-details.png) The MySQL table data is persistent across container restarts by setting the MySQL data directory `/var/lib/mysql` as a data volume. We didn't specify a name or docker_host path so Docker will give it a unique name and store it in `/var/lib/docker/volumes/`; to find out where the data is stored on the Docker host you can use `docker inspect`. @@ -96,7 +99,7 @@ $ docker exec mysql.pool-1.1.1 \ ### Running -To run the a docker container from this image you can use the standard docker commands. Alternatively, you can use the embedded (Service Container Manager Interface) [scmi](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/usr/sbin/scmi) that is included in the image since `1.7.1` or, if you have a checkout of the [source repository](https://github.com/jdeathe/centos-ssh-mysql), and have make installed the Makefile provides targets to build, install, start, stop etc. where environment variables can be used to configure the container options and set custom docker run parameters. +To run the a docker container from this image you can use the standard docker commands. Alternatively, you can use the embedded (Service Container Manager Interface) [scmi](https://github.com/jdeathe/centos-ssh/blob/centos-7/src/usr/sbin/scmi) that is included in the image since `1.7.1` or, if you have a checkout of the [source repository](https://github.com/jdeathe/centos-ssh-mysql), and have make installed the Makefile provides targets to build, install, start, stop etc. where environment variables can be used to configure the container options and set custom docker run parameters. #### SCMI Installation Examples @@ -111,10 +114,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:1.8.4 \ + jdeathe/centos-ssh-mysql:2.0.0 \ /usr/sbin/scmi install \ --chroot=/media/root \ - --tag=1.8.4 \ + --tag=2.0.0 \ --name=mysql.pool-1.1.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -128,10 +131,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:1.8.4 \ + jdeathe/centos-ssh-mysql:2.0.0 \ /usr/sbin/scmi uninstall \ --chroot=/media/root \ - --tag=1.8.4 \ + --tag=2.0.0 \ --name=mysql.pool-1.1.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -145,10 +148,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:1.8.4 \ + jdeathe/centos-ssh-mysql:2.0.0 \ /usr/sbin/scmi install \ --chroot=/media/root \ - --tag=1.8.4 \ + --tag=2.0.0 \ --name=mysql.pool-1.1.1 \ --manager=systemd \ --register \ @@ -161,6 +164,8 @@ $ docker run \ ##### SCMI Fleet Support +**_Deprecation Notice:_** The fleet project is no longer maintained. The fleet `--manager` option has been deprecated in `scmi`. + If your docker host has systemd, fleetd (and optionally etcd) installed then `scmi` provides a method to schedule the container to run on the cluster. This provides some additional features for managing a group of instances on a [fleet](https://github.com/coreos/fleet) cluster and has the option to use an etcd backed service registry. To use the fleet method of installation use the `-m` or `--manager` option of `scmi` and to include the optional etcd register companion unit use the `--register` option. ##### SCMI Image Information @@ -173,7 +178,7 @@ To see detailed information about the image run `scmi` with the `--info` option. $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:1.8.4 + jdeathe/centos-ssh-mysql:2.0.0 ) --info" ``` @@ -183,7 +188,7 @@ To perform an installation using the docker name `mysql.pool-1.2.1` simply use t $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:1.8.4 + jdeathe/centos-ssh-mysql:2.0.0 ) --name=mysql.pool-1.2.1" ``` @@ -193,7 +198,7 @@ To uninstall use the *same command* that was used to install but with the `unins $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.uninstall}}" \ - jdeathe/centos-ssh-mysql:1.8.4 + jdeathe/centos-ssh-mysql:2.0.0 ) --name=mysql.pool-1.2.1" ``` @@ -206,7 +211,7 @@ To see detailed information about the image run `scmi` with the `--info` option. ``` $ sudo -E atomic install \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:1.8.4 \ + jdeathe/centos-ssh-mysql:2.0.0 \ --info ``` @@ -215,14 +220,14 @@ To perform an installation using the docker name `mysql.pool-1.3.1` simply use t ``` $ sudo -E atomic install \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:1.8.4 + jdeathe/centos-ssh-mysql:2.0.0 ``` Alternatively, you could use the `scmi` options `--name` or `-n` for naming the container. ``` $ sudo -E atomic install \ - jdeathe/centos-ssh-mysql:1.8.4 \ + jdeathe/centos-ssh-mysql:2.0.0 \ --name mysql.pool-1.3.1 ``` @@ -231,7 +236,7 @@ To uninstall use the *same command* that was used to install but with the `unins ``` $ sudo -E atomic uninstall \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:1.8.4 + jdeathe/centos-ssh-mysql:2.0.0 ``` #### Using environment variables @@ -250,7 +255,7 @@ $ docker run \ --env "MYSQL_USER_PASSWORD=" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume mysql.pool-1.1.1.data-mysql:/var/lib/mysql \ - jdeathe/centos-ssh-mysql:centos-6 + jdeathe/centos-ssh-mysql:2.0.0 ``` The environmental variable `MYSQL_SUBNET` is optional but can be used to generate users with access to databases outside the `localhost`, (the default for the root user). In the example, the subnet definition `0.0.0.0/0.0.0.0` allows connections from any network which is equivalent to the wildcard symbol, `%`, in MySQL GRANT definitions. diff --git a/environment.mk b/environment.mk index 8c2b599..6f79fe1 100644 --- a/environment.mk +++ b/environment.mk @@ -6,8 +6,8 @@ DOCKER_IMAGE_NAME := centos-ssh-mysql SHPEC_ROOT := test/shpec # Tag validation patterns -DOCKER_IMAGE_TAG_PATTERN := ^(latest|centos-6|((1|centos-6-1)\.[0-9]+\.[0-9]+))$ -DOCKER_IMAGE_RELEASE_TAG_PATTERN := ^(1|centos-6-1)\.[0-9]+\.[0-9]+$ +DOCKER_IMAGE_TAG_PATTERN := ^(latest|centos-6|centos-7-mysql57-community|(([1-2]|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+))$ +DOCKER_IMAGE_RELEASE_TAG_PATTERN := ^(1|2|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+$ # ----------------------------------------------------------------------------- # Variables diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index b088bbe..b1ef923 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -52,7 +52,7 @@ Environment="DOCKER_USER=jdeathe" Environment="DOCKER_IMAGE_NAME=centos-ssh-mysql" Environment="DOCKER_CONTAINER_OPTS=" Environment="DOCKER_IMAGE_PACKAGE_PATH=/var/opt/scmi/packages" -Environment="DOCKER_IMAGE_TAG=1.8.4" +Environment="DOCKER_IMAGE_TAG=2.0.0" Environment="DOCKER_PORT_MAP_TCP_3306=3306" Environment="MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true" Environment="MYSQL_AUTOSTART_MYSQLD_WRAPPER=true" diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index 6454763..37716ee 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -5,8 +5,8 @@ readonly DOCKER_USER=jdeathe readonly DOCKER_IMAGE_NAME=centos-ssh-mysql # Tag validation patterns -readonly DOCKER_IMAGE_TAG_PATTERN='^(latest|centos-6|((1|centos-6-1)\.[0-9]+\.[0-9]+))$' -readonly DOCKER_IMAGE_RELEASE_TAG_PATTERN='^(1|centos-6-1)\.[0-9]+\.[0-9]+$' +readonly DOCKER_IMAGE_TAG_PATTERN='^(latest|centos-6|centos-7-mysql57-community|(([1-2]|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+))$' +readonly DOCKER_IMAGE_RELEASE_TAG_PATTERN='^(1|2|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+$' # ----------------------------------------------------------------------------- # Variables diff --git a/src/usr/bin/healthcheck b/src/usr/bin/healthcheck index 912c7c2..7e94cf4 100755 --- a/src/usr/bin/healthcheck +++ b/src/usr/bin/healthcheck @@ -185,6 +185,8 @@ function __usage () function main () { + readonly MYSQLD=/usr/sbin/mysqld + local -r REDACTED_VALUE="********" local -r STATUS_PATH="/var/lib/healthcheck/status" local QUITE=false @@ -240,7 +242,7 @@ function main () # mysqld-wrapper if [[ ${MYSQL_AUTOSTART_MYSQLD_WRAPPER} == true ]]; then if ! ps axo command \ - | grep -qE '^/usr/libexec/mysqld ' + | grep -qE "^${MYSQLD} " then exit 1 fi diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 0f858dc..d583c26 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -9,6 +9,9 @@ TIMER_START="$( source /etc/mysqld-bootstrap.conf +readonly MYSQLD=/usr/sbin/mysqld +readonly PID_PATH=/run/mysqld/mysqld.pid + function get_mysql_user_host () { local CLIENT_SUBNET="${1:-127.0.0.1}" @@ -193,11 +196,14 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then fi echo "Initalising MySQL data directory." - mysql_install_db \ - --force \ + ${MYSQLD} \ + --datadir="${OPTS_MYSQL_DATA_DIR}" \ + --initialize-insecure \ + --pid-file="${PID_PATH}" \ --skip-name-resolve \ --skip-networking \ --tmpdir="${OPTS_MYSQL_DATA_DIR}" \ + --user=mysql \ & PIDS[0]=${!} @@ -295,8 +301,21 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then # Wait for the MySQL system table installation to complete [[ -n ${PIDS[0]} ]] && wait ${PIDS[0]} + if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ + && [[ ! -e ${OPTS_MYSQL_DATA_DIR}/server-key.pem ]] + then + echo "Generating MySQL certificates." + /usr/bin/mysql_ssl_rsa_setup \ + --datadir="${OPTS_MYSQL_DATA_DIR}" \ + --uid=mysql \ + &> /dev/null \ + & + fi + PIDS[1]=${!} + echo "Initialising MySQL." - mysqld_safe \ + ${MYSQLD} \ + --pid-file="${PID_PATH}" \ --skip-networking \ --init-file=/tmp/mysql-init \ & @@ -314,7 +333,7 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]]; then mysql \ --password="${OPTS_MYSQL_ROOT_PASSWORD}" \ - -e "SET PASSWORD = '${OPTS_MYSQL_ROOT_PASSWORD}'" + -e "UPDATE mysql.user SET authentication_string = '${OPTS_MYSQL_ROOT_PASSWORD}' WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES;" fi break @@ -353,14 +372,17 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then /tmp/mysql-init{,-template} fi + # Wait for the MySQL certificate setup to complete + [[ -n ${PIDS[1]} ]] && wait ${PIDS[1]} + if [[ -n ${MYSQL_ROOT_PASSWORD} ]]; then - OPTS_MYSQL_ROOT_PASSWORD="${REDACTED_VALUE}" - MYSQL_ROOT_PASSWORD="${REDACTED_VALUE}" + OPTS_MYSQL_ROOT_PASSWORD="${REDACTED_VALUE}" + MYSQL_ROOT_PASSWORD="${REDACTED_VALUE}" fi if [[ -n ${MYSQL_USER_PASSWORD} ]]; then - OPTS_MYSQL_USER_PASSWORD="${REDACTED_VALUE}" - MYSQL_USER_PASSWORD="${REDACTED_VALUE}" + OPTS_MYSQL_USER_PASSWORD="${REDACTED_VALUE}" + MYSQL_USER_PASSWORD="${REDACTED_VALUE}" fi # Local root user details diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index bef99fd..1b6ae7d 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -2,11 +2,10 @@ source /etc/mysqld-bootstrap.conf -readonly MYSQLD_SAFE=/usr/bin/mysqld_safe +readonly MYSQLD=/usr/sbin/mysqld readonly NICE=/bin/nice readonly NICENESS="${MYSQL_NICENESS:-10}" -readonly PID_PATH=/var/run/mysqld/mysqld.pid -readonly PID_PROXY=/usr/bin/pidproxy +readonly PID_PATH=/run/mysqld/mysqld.pid while true; do sleep 0.1 @@ -15,6 +14,5 @@ done exec ${NICE} \ -n ${NICENESS} \ - ${PID_PROXY} \ - ${PID_PATH} \ - ${MYSQLD_SAFE} + ${MYSQLD} \ + --pid-file=${PID_PATH} diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 0291f51..a983e24 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -272,10 +272,10 @@ function test_basic_operations () if ! __is_container_ready \ mysql.pool-1.1.1 \ ${STARTUP_TIME} \ - "/usr/libexec/mysqld " \ + "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /var/run/mysqld/mysqld.pid ]]" + && [[ -s /run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -301,7 +301,7 @@ function test_basic_operations () mysql.pool-1.1.1 \ mysql \ --batch \ - --password=${mysql_root_password} \ + --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user;" @@ -311,7 +311,11 @@ function test_basic_operations () "${select_users}" \ "$( printf -- \ - '%s\t%s' \ + '%s\t%s\n%s\t%s\n%s\t%s' \ + 'mysql.session' \ + 'localhost' \ + 'mysql.sys' \ + 'localhost' \ 'root' \ 'localhost' )" @@ -325,7 +329,7 @@ function test_basic_operations () mysql.pool-1.1.1 \ mysql \ --batch \ - --password=${mysql_root_password} \ + --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SHOW DATABASES;" @@ -335,9 +339,11 @@ function test_basic_operations () "${show_databases}" \ "$( printf -- \ - '%s\n%s' \ + '%s\n%s\n%s\n%s' \ 'information_schema' \ - 'mysql' + 'mysql' \ + 'performance_schema' \ + 'sys' )" end @@ -371,10 +377,10 @@ function test_basic_operations () if ! __is_container_ready \ mysql.pool-1.1.1 \ ${STARTUP_TIME} \ - "/usr/libexec/mysqld " \ + "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /var/run/mysqld/mysqld.pid ]]" + && [[ -s /run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -422,10 +428,10 @@ function test_basic_operations () if ! __is_container_ready \ mysql.pool-1.1.1 \ ${STARTUP_TIME} \ - "/usr/libexec/mysqld " \ + "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /var/run/mysqld/mysqld.pid ]]" + && [[ -s /run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -445,9 +451,13 @@ function test_basic_operations () "${select_users}" \ "$( printf -- \ - '%s\t%s\n%s\t%s' \ + '%s\t%s\n%s\t%s\n%s\t%s\n%s\t%s' \ 'my-user' \ 'localhost' \ + 'mysql.session' \ + 'localhost' \ + 'mysql.sys' \ + 'localhost' \ 'root' \ 'localhost' )" @@ -478,7 +488,7 @@ function test_basic_operations () assert __shpec_matcher_egrep \ "${show_grants}" \ - "^GRANT USAGE ON \*\.\* TO 'my-user'@'localhost' IDENTIFIED BY PASSWORD '[\*A-Z0-9]+'$" + "^GRANT USAGE ON \*\.\* TO 'my-user'@'localhost'$" end end @@ -564,10 +574,10 @@ function test_custom_configuration () if ! __is_container_ready \ mysql.pool-1.1.2 \ ${STARTUP_TIME} \ - "/usr/libexec/mysqld " \ + "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /var/run/mysqld/mysqld.pid ]]" + && [[ -s /run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -620,7 +630,7 @@ function test_custom_configuration () mysql.pool-1.1.2 \ mysql \ --batch \ - --password=${mysql_root_password} \ + --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" @@ -630,9 +640,13 @@ function test_custom_configuration () "${select_users}" \ "$( printf -- \ - '%s\t%s\n%s\t%s' \ + '%s\t%s\n%s\t%s\n%s\t%s\n%s\t%s' \ 'app-user' \ '172.172.40.0/255.255.255.0' \ + 'mysql.session' \ + 'localhost' \ + 'mysql.sys' \ + 'localhost' \ 'root' \ 'localhost' )" @@ -679,13 +693,13 @@ function test_custom_configuration () --name mysql.pool-1.1.2 \ --network-alias mysql.pool-1.1.2 \ --network ${private_network_1} \ - --env "MYSQL_ROOT_PASSWORD=/var/run/secrets/mysql_root_password" \ + --env "MYSQL_ROOT_PASSWORD=/run/secrets/mysql_root_password" \ --env "MYSQL_SUBNET=172.172.40.0/255.255.255.0" \ --env "MYSQL_USER=app-user" \ - --env "MYSQL_USER_PASSWORD=/var/run/secrets/mysql_user_password" \ + --env "MYSQL_USER_PASSWORD=/run/secrets/mysql_user_password" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume ${data_volume_1}:/var/lib/mysql \ - --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/var/run/secrets:ro \ + --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/run/secrets:ro \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -697,10 +711,10 @@ function test_custom_configuration () if ! __is_container_ready \ mysql.pool-1.1.2 \ ${STARTUP_TIME} \ - "/usr/libexec/mysqld " \ + "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /var/run/mysqld/mysqld.pid ]]" + && [[ -s /run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -712,7 +726,7 @@ function test_custom_configuration () mysql.pool-1.1.2 \ mysql \ --batch \ - --password=${mysql_root_password} \ + --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" @@ -722,9 +736,13 @@ function test_custom_configuration () "${select_users}" \ "$( printf -- \ - '%s\t%s\n%s\t%s' \ + '%s\t%s\n%s\t%s\n%s\t%s\n%s\t%s' \ 'app-user' \ '172.172.40.0/255.255.255.0' \ + 'mysql.session' \ + 'localhost' \ + 'mysql.sys' \ + 'localhost' \ 'root' \ 'localhost' )" @@ -771,15 +789,15 @@ function test_custom_configuration () --name mysql.pool-1.1.2 \ --network-alias mysql.pool-1.1.2 \ --network ${private_network_1} \ - --env "MYSQL_ROOT_PASSWORD=/var/run/secrets/mysql_root_password_hashed" \ + --env "MYSQL_ROOT_PASSWORD=/run/secrets/mysql_root_password_hashed" \ --env "MYSQL_ROOT_PASSWORD_HASHED=true" \ --env "MYSQL_SUBNET=172.172.40.0/255.255.255.0" \ --env "MYSQL_USER=app-user" \ - --env "MYSQL_USER_PASSWORD=/var/run/secrets/mysql_user_password_hashed" \ + --env "MYSQL_USER_PASSWORD=/run/secrets/mysql_user_password_hashed" \ --env "MYSQL_USER_PASSWORD_HASHED=true" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume ${data_volume_1}:/var/lib/mysql \ - --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/var/run/secrets:ro \ + --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/run/secrets:ro \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -791,10 +809,10 @@ function test_custom_configuration () if ! __is_container_ready \ mysql.pool-1.1.2 \ ${STARTUP_TIME} \ - "/usr/libexec/mysqld " \ + "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /var/run/mysqld/mysqld.pid ]]" + && [[ -s /run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -806,7 +824,7 @@ function test_custom_configuration () mysql.pool-1.1.2 \ mysql \ --batch \ - --password=${mysql_root_password} \ + --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" @@ -816,9 +834,13 @@ function test_custom_configuration () "${select_users}" \ "$( printf -- \ - '%s\t%s\n%s\t%s' \ + '%s\t%s\n%s\t%s\n%s\t%s\n%s\t%s' \ 'app-user' \ '172.172.40.0/255.255.255.0' \ + 'mysql.session' \ + 'localhost' \ + 'mysql.sys' \ + 'localhost' \ 'root' \ 'localhost' )" @@ -908,10 +930,10 @@ function test_custom_configuration () if ! __is_container_ready \ mysql.pool-1.1.4 \ ${STARTUP_TIME} \ - "/usr/libexec/mysqld " \ + "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /var/run/mysqld/mysqld.pid ]]" + && [[ -s /run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -923,7 +945,7 @@ function test_custom_configuration () mysql.pool-1.1.4 \ mysql \ --batch \ - --password=${mysql_root_password} \ + --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" @@ -933,9 +955,13 @@ function test_custom_configuration () "${select_users}" \ "$( printf -- \ - '%s\t%s\n%s\t%s' \ + '%s\t%s\n%s\t%s\n%s\t%s\n%s\t%s' \ 'app2-user' \ '%' \ + 'mysql.session' \ + 'localhost' \ + 'mysql.sys' \ + 'localhost' \ 'root' \ 'localhost' )" @@ -1006,7 +1032,7 @@ function test_custom_configuration () if ! __is_container_ready \ mysql.pool-1.1.1 \ ${STARTUP_TIME} \ - "/usr/libexec/mysqld " + "/usr/sbin/mysqld " then exit 1 fi @@ -1031,11 +1057,19 @@ function test_custom_configuration () jdeathe/centos-ssh-mysql:latest \ &> /dev/null - sleep ${STARTUP_TIME} + if ! __is_container_ready \ + mysql.pool-1.1.1 \ + ${STARTUP_TIME} \ + "/usr/bin/python /usr/bin/supervisord " \ + "[[ -e /var/lib/mysql/ibdata1 ]] \ + && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]]" + then + exit 1 + fi it "Can disable mysqld-wrapper." docker top mysql.pool-1.1.1 \ - | grep -qE '/usr/libexec/mysqld ' + | grep -qE '/usr/sbin/mysqld ' assert equal \ "${?}" \ @@ -1111,12 +1145,12 @@ function test_healthcheck () docker exec -t \ mysql.pool-1.1.1 \ bash -c "mv \ - /usr/libexec/mysqld \ - /usr/libexec/mysqld2" \ + /usr/sbin/mysqld \ + /usr/sbin/mysqld2" \ && docker exec -t \ mysql.pool-1.1.1 \ - bash -c "if [[ -n \$(pgrep -f '^/usr/libexec/mysqld ') ]]; then \ - kill -9 -\$(ps axo pgid,command | grep -P '/bin/sh /usr/bin/mysqld_safe$' | awk '{ print \$1; }') + bash -c "if [[ -n \$(pgrep -f '^/usr/sbin/mysqld ') ]]; then \ + kill -9 -\$(ps axo pgid,command | grep -P '/usr/sbin/mysqld --pid-file=/run/mysqld/mysqld.pid$' | awk '{ print \$1; }') fi" sleep $( From a98bc030ccf10a62e5f7ff5d793d8289acb847b3 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 30 Jul 2018 08:56:05 +0100 Subject: [PATCH 02/98] ISSUE 176: Adds specific package versions to build. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 918c9a1..2455608 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,8 @@ RUN { \ && yum -y install \ --setopt=tsflags=nodocs \ --disableplugin=fastestmirror \ - mysql-community-server \ - psmisc \ + mysql-community-server-5.7.23-1.el7 \ + psmisc-22.20-15.el7 \ && yum versionlock add \ mysql* \ && rm -rf /var/cache/yum/* \ From 3e3d18117921367489adf664a2c4e29987be923b Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 3 Aug 2018 09:45:31 +0100 Subject: [PATCH 03/98] ISSUE 179: Updates my.cnf for 5.7. --- src/etc/services-config/mysql/my.cnf | 1 - src/etc/services-config/mysql/my.cnf.default | 21 ++++++++++++++++++-- src/usr/sbin/mysqld-bootstrap | 5 ++--- src/usr/sbin/mysqld-wrapper | 2 +- test/shpec/operation_shpec.sh | 16 +++++++-------- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/etc/services-config/mysql/my.cnf b/src/etc/services-config/mysql/my.cnf index e23eb7c..a19cdcf 100644 --- a/src/etc/services-config/mysql/my.cnf +++ b/src/etc/services-config/mysql/my.cnf @@ -19,6 +19,5 @@ collation-server=utf8_unicode_ci skip-name-resolve -[mysqld_safe] log-error=/var/log/mysqld.log pid-file=/var/run/mysqld/mysqld.pid \ No newline at end of file diff --git a/src/etc/services-config/mysql/my.cnf.default b/src/etc/services-config/mysql/my.cnf.default index 3d8afdb..637e69e 100644 --- a/src/etc/services-config/mysql/my.cnf.default +++ b/src/etc/services-config/mysql/my.cnf.default @@ -1,10 +1,27 @@ +# For advice on how to change settings please see +# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html + [mysqld] +# +# Remove leading # and set to the amount of RAM for the most important data +# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. +# innodb_buffer_pool_size = 128M +# +# Remove leading # to turn on a very important data integrity option: logging +# changes to the binary log between backups. +# log_bin +# +# Remove leading # to set options mainly useful for reporting servers. +# The server defaults are faster for transactions and fast SELECTs. +# Adjust sizes as needed, experiment to find the optimal values. +# join_buffer_size = 128M +# sort_buffer_size = 2M +# read_rnd_buffer_size = 2M datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock -user=mysql + # Disabling symbolic-links is recommended to prevent assorted security risks symbolic-links=0 -[mysqld_safe] log-error=/var/log/mysqld.log pid-file=/var/run/mysqld/mysqld.pid \ No newline at end of file diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index d583c26..7f71587 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -9,9 +9,6 @@ TIMER_START="$( source /etc/mysqld-bootstrap.conf -readonly MYSQLD=/usr/sbin/mysqld -readonly PID_PATH=/run/mysqld/mysqld.pid - function get_mysql_user_host () { local CLIENT_SUBNET="${1:-127.0.0.1}" @@ -100,7 +97,9 @@ OPTS_MYSQL_DATA_DIR="$( # MySQL initialisation is a one-shot process. if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then + readonly MYSQLD=/usr/sbin/mysqld readonly PASSWORD_LENGTH=16 + readonly PID_PATH=/var/run/mysqld/mysqld.pid readonly REDACTED_VALUE="********" # Get passwords from file if applicable diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index 1b6ae7d..5adfdc2 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -5,7 +5,7 @@ source /etc/mysqld-bootstrap.conf readonly MYSQLD=/usr/sbin/mysqld readonly NICE=/bin/nice readonly NICENESS="${MYSQL_NICENESS:-10}" -readonly PID_PATH=/run/mysqld/mysqld.pid +readonly PID_PATH=/var/run/mysqld/mysqld.pid while true; do sleep 0.1 diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index a983e24..4498707 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -275,7 +275,7 @@ function test_basic_operations () "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /run/mysqld/mysqld.pid ]]" + && [[ -s /var/run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -380,7 +380,7 @@ function test_basic_operations () "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /run/mysqld/mysqld.pid ]]" + && [[ -s /var/run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -431,7 +431,7 @@ function test_basic_operations () "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /run/mysqld/mysqld.pid ]]" + && [[ -s /var/run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -577,7 +577,7 @@ function test_custom_configuration () "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /run/mysqld/mysqld.pid ]]" + && [[ -s /var/run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -714,7 +714,7 @@ function test_custom_configuration () "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /run/mysqld/mysqld.pid ]]" + && [[ -s /var/run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -812,7 +812,7 @@ function test_custom_configuration () "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /run/mysqld/mysqld.pid ]]" + && [[ -s /var/run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -933,7 +933,7 @@ function test_custom_configuration () "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ - && [[ -s /run/mysqld/mysqld.pid ]]" + && [[ -s /var/run/mysqld/mysqld.pid ]]" then exit 1 fi @@ -1150,7 +1150,7 @@ function test_healthcheck () && docker exec -t \ mysql.pool-1.1.1 \ bash -c "if [[ -n \$(pgrep -f '^/usr/sbin/mysqld ') ]]; then \ - kill -9 -\$(ps axo pgid,command | grep -P '/usr/sbin/mysqld --pid-file=/run/mysqld/mysqld.pid$' | awk '{ print \$1; }') + kill -9 -\$(ps axo pgid,command | grep -P '/usr/sbin/mysqld --pid-file=/var/run/mysqld/mysqld.pid$' | awk '{ print \$1; }') fi" sleep $( From 40d9300301f6899e4769010a23a393d1e147f362 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 3 Aug 2018 09:52:51 +0100 Subject: [PATCH 04/98] ISSUE 179: Remove commented lines from default configuration. --- src/etc/services-config/mysql/my.cnf | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/etc/services-config/mysql/my.cnf b/src/etc/services-config/mysql/my.cnf index a19cdcf..6e1f746 100644 --- a/src/etc/services-config/mysql/my.cnf +++ b/src/etc/services-config/mysql/my.cnf @@ -5,19 +5,13 @@ default-character-set=utf8 [mysqld] datadir=/var/lib/mysql socket=/var/run/mysqld/mysql.sock +pid-file=/var/run/mysqld/mysqld.pid +log-error=/var/log/mysqld.log user=mysql -# Disabling symbolic-links is recommended to prevent assorted security risks + symbolic-links=0 +skip-name-resolve=1 skip-character-set-client-handshake=1 character-set-server=utf8 -collation-server=utf8_unicode_ci - -#log-queries-not-using-indexes=1 -#slow-query-log=1 -#slow-query-log-file=/var/lib/mysql/mysql-slow.log - -skip-name-resolve - -log-error=/var/log/mysqld.log -pid-file=/var/run/mysqld/mysqld.pid \ No newline at end of file +collation-server=utf8_unicode_ci \ No newline at end of file From ff9334aa1c60b6bf9c6eabf0921064ab5f3f9a36 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 3 Aug 2018 10:00:37 +0100 Subject: [PATCH 05/98] ISSUE 179: Increases startup time from 7 to 10 seconds. --- environment.mk | 2 +- src/opt/scmi/environment.sh | 2 +- src/opt/scmi/service-unit.sh | 2 +- test/shpec/operation_shpec.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/environment.mk b/environment.mk index 6f79fe1..0b44757 100644 --- a/environment.mk +++ b/environment.mk @@ -28,7 +28,7 @@ NO_CACHE ?= false DIST_PATH ?= ./dist # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME ?= 7 +STARTUP_TIME ?= 10 # ----------------------------------------------------------------------------- # Application container configuration diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index 37716ee..9b23cd9 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -27,7 +27,7 @@ NO_CACHE="${NO_CACHE:-false}" DIST_PATH="${DIST_PATH:-./dist}" # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME="${STARTUP_TIME:-7}" +STARTUP_TIME="${STARTUP_TIME:-10}" # ETCD register service settings REGISTER_ETCD_PARAMETERS="${REGISTER_ETCD_PARAMETERS:-}" diff --git a/src/opt/scmi/service-unit.sh b/src/opt/scmi/service-unit.sh index 57e1a50..d73c22e 100644 --- a/src/opt/scmi/service-unit.sh +++ b/src/opt/scmi/service-unit.sh @@ -25,4 +25,4 @@ readonly SERVICE_UNIT_REGISTER_ENVIRONMENT_KEYS=" # ----------------------------------------------------------------------------- # Variables # ----------------------------------------------------------------------------- -SERVICE_UNIT_INSTALL_TIMEOUT=${SERVICE_UNIT_INSTALL_TIMEOUT:-10} +SERVICE_UNIT_INSTALL_TIMEOUT=${SERVICE_UNIT_INSTALL_TIMEOUT:-13} diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 4498707..25db3e4 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -1,4 +1,4 @@ -readonly STARTUP_TIME=7 +readonly STARTUP_TIME=10 readonly TEST_DIRECTORY="test" # These should ideally be a static value but hosts might be using this port so From 2ecd6296fd4ec73a7d2888949ab14b7ff8408fe3 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Tue, 7 Aug 2018 18:30:45 +0100 Subject: [PATCH 06/98] Release changes for 1.8.5 / 2.0.0. --- CHANGELOG.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7b9d46..c52b150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,6 @@ Summary of release changes for Version 2. CentOS-7 7.4.1708 x86_64 - MySQL 5.7 Community Server. -### 2.0.0 - Unreleased +### 2.0.0 - 2018-08-07 - Initial release. \ No newline at end of file diff --git a/README.md b/README.md index 5fd3e02..be6681b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-commu ### Tags and respective `Dockerfile` links - `centos-7-mysql57-community`, `centos-7-mysql57-community-2.0.0`, `2.0.0` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, `centos-6-1.8.4`, `1.8.4` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- `centos-6`, `centos-6-1.8.5`, `1.8.5` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) The Dockerfile can be used to build a base image that is the bases for several other docker images. From 09aeb2e840f4f8bd8cbb2c4f913b4f20909a1e51 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Wed, 22 Aug 2018 22:44:37 +0100 Subject: [PATCH 07/98] ISSUE 186: Updates source image to 2.4.0. --- CHANGELOG.md | 6 +++++- Dockerfile | 4 ++-- README-short.txt | 2 +- README.md | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c52b150..19d67f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ Summary of release changes for Version 2. -CentOS-7 7.4.1708 x86_64 - MySQL 5.7 Community Server. +CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. + +### 2.1.0 - Unreleased + +- Updates source image to [2.4.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.4.0). ### 2.0.0 - 2018-08-07 diff --git a/Dockerfile b/Dockerfile index 2455608..27fed98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # CentOS-7, MySQL 5.7 Community Server # # ============================================================================= -FROM jdeathe/centos-ssh:2.3.2 +FROM jdeathe/centos-ssh:2.4.0 # ----------------------------------------------------------------------------- # Install MySQL @@ -112,7 +112,7 @@ jdeathe/centos-ssh-mysql:${RELEASE_VERSION} \ org.deathe.license="MIT" \ org.deathe.vendor="jdeathe" \ org.deathe.url="https://github.com/jdeathe/centos-ssh-mysql" \ - org.deathe.description="CentOS-7 7.4.1708 x86_64 - MySQL 5.7 Community Server." + org.deathe.description="CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server." HEALTHCHECK \ --interval=1s \ diff --git a/README-short.txt b/README-short.txt index 312b3b7..fc53ce3 100644 --- a/README-short.txt +++ b/README-short.txt @@ -1 +1 @@ -CentOS-7 7.4.1708 x86_64 - MySQL Community Server. \ No newline at end of file +CentOS-7 7.5.1804 x86_64 - MySQL Community Server. \ No newline at end of file diff --git a/README.md b/README.md index be6681b..426c127 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ centos-ssh-mysql Docker Image including: - CentOS-6 6.9 x86_64, MySQL 5.1. -- CentOS-7 7.4.1708 x86_64, MySQL 5.7 Community Server. +- CentOS-7 7.5.1804 x86_64, MySQL 5.7 Community Server. Includes Automated password generation and an option for custom initialisation SQL. Supports custom configuration via environment variables. From befc8414cc9131589c577c023692acea22b987ca Mon Sep 17 00:00:00 2001 From: James Deathe Date: Wed, 22 Aug 2018 22:54:31 +0100 Subject: [PATCH 08/98] --amend --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 426c127..4b56639 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ centos-ssh-mysql ================ Docker Image including: -- CentOS-6 6.9 x86_64, MySQL 5.1. +- CentOS-6 6.10 x86_64, MySQL 5.1. - CentOS-7 7.5.1804 x86_64, MySQL 5.7 Community Server. Includes Automated password generation and an option for custom initialisation SQL. Supports custom configuration via environment variables. From aa9943e9067224c471a0ea72d91f8acc639b836d Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 23 Aug 2018 15:16:22 +0100 Subject: [PATCH 09/98] Release changes for 1.9.0 / 2.1.0. --- CHANGELOG.md | 2 +- Dockerfile | 2 +- README.md | 36 +++++++++---------- .../systemd/system/centos-ssh-mysql@.service | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19d67f0..d8e6a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Summary of release changes for Version 2. CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. -### 2.1.0 - Unreleased +### 2.1.0 - 2018-08-23 - Updates source image to [2.4.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.4.0). diff --git a/Dockerfile b/Dockerfile index 27fed98..0bc85d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,7 @@ ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true \ # ----------------------------------------------------------------------------- # Set image metadata # ----------------------------------------------------------------------------- -ARG RELEASE_VERSION="2.0.0" +ARG RELEASE_VERSION="2.1.0" LABEL \ maintainer="James Deathe " \ install="docker run \ diff --git a/README.md b/README.md index 4b56639..1cba74c 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ Includes Automated password generation and an option for custom initialisation S ## Overview & links -The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-community` Docker tag. It is recommended to select a specific release tag - the convention is `centos-7-mysql57-community-2.0.0` or `2.0.0` for the [2.0.0](https://github.com/jdeathe/centos-ssh-mysql/tree/2.0.0) release tag. +The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-community` Docker tag. It is recommended to select a specific release tag - the convention is `centos-7-mysql57-community-2.1.0` or `2.1.0` for the [2.1.0](https://github.com/jdeathe/centos-ssh-mysql/tree/2.1.0) release tag. ### Tags and respective `Dockerfile` links -- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.0.0`, `2.0.0` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, `centos-6-1.8.5`, `1.8.5` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.1.0`, `2.1.0` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- `centos-6`, `centos-6-1.9.0`, `1.9.0` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) The Dockerfile can be used to build a base image that is the bases for several other docker images. @@ -43,7 +43,7 @@ $ docker run -d \ --name mysql.pool-1.1.1 \ -p 3306:3306 \ -v /var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.0.0 + jdeathe/centos-ssh-mysql:2.1.0 ``` Now you can verify it is initialised and running successfully by inspecting the container's logs. @@ -114,10 +114,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.0.0 \ + jdeathe/centos-ssh-mysql:2.1.0 \ /usr/sbin/scmi install \ --chroot=/media/root \ - --tag=2.0.0 \ + --tag=2.1.0 \ --name=mysql.pool-1.1.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -131,10 +131,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.0.0 \ + jdeathe/centos-ssh-mysql:2.1.0 \ /usr/sbin/scmi uninstall \ --chroot=/media/root \ - --tag=2.0.0 \ + --tag=2.1.0 \ --name=mysql.pool-1.1.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -148,10 +148,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.0.0 \ + jdeathe/centos-ssh-mysql:2.1.0 \ /usr/sbin/scmi install \ --chroot=/media/root \ - --tag=2.0.0 \ + --tag=2.1.0 \ --name=mysql.pool-1.1.1 \ --manager=systemd \ --register \ @@ -178,7 +178,7 @@ To see detailed information about the image run `scmi` with the `--info` option. $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:2.0.0 + jdeathe/centos-ssh-mysql:2.1.0 ) --info" ``` @@ -188,7 +188,7 @@ To perform an installation using the docker name `mysql.pool-1.2.1` simply use t $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:2.0.0 + jdeathe/centos-ssh-mysql:2.1.0 ) --name=mysql.pool-1.2.1" ``` @@ -198,7 +198,7 @@ To uninstall use the *same command* that was used to install but with the `unins $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.uninstall}}" \ - jdeathe/centos-ssh-mysql:2.0.0 + jdeathe/centos-ssh-mysql:2.1.0 ) --name=mysql.pool-1.2.1" ``` @@ -211,7 +211,7 @@ To see detailed information about the image run `scmi` with the `--info` option. ``` $ sudo -E atomic install \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:2.0.0 \ + jdeathe/centos-ssh-mysql:2.1.0 \ --info ``` @@ -220,14 +220,14 @@ To perform an installation using the docker name `mysql.pool-1.3.1` simply use t ``` $ sudo -E atomic install \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:2.0.0 + jdeathe/centos-ssh-mysql:2.1.0 ``` Alternatively, you could use the `scmi` options `--name` or `-n` for naming the container. ``` $ sudo -E atomic install \ - jdeathe/centos-ssh-mysql:2.0.0 \ + jdeathe/centos-ssh-mysql:2.1.0 \ --name mysql.pool-1.3.1 ``` @@ -236,7 +236,7 @@ To uninstall use the *same command* that was used to install but with the `unins ``` $ sudo -E atomic uninstall \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:2.0.0 + jdeathe/centos-ssh-mysql:2.1.0 ``` #### Using environment variables @@ -255,7 +255,7 @@ $ docker run \ --env "MYSQL_USER_PASSWORD=" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume mysql.pool-1.1.1.data-mysql:/var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.0.0 + jdeathe/centos-ssh-mysql:2.1.0 ``` The environmental variable `MYSQL_SUBNET` is optional but can be used to generate users with access to databases outside the `localhost`, (the default for the root user). In the example, the subnet definition `0.0.0.0/0.0.0.0` allows connections from any network which is equivalent to the wildcard symbol, `%`, in MySQL GRANT definitions. diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index b1ef923..31ae6ba 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -52,7 +52,7 @@ Environment="DOCKER_USER=jdeathe" Environment="DOCKER_IMAGE_NAME=centos-ssh-mysql" Environment="DOCKER_CONTAINER_OPTS=" Environment="DOCKER_IMAGE_PACKAGE_PATH=/var/opt/scmi/packages" -Environment="DOCKER_IMAGE_TAG=2.0.0" +Environment="DOCKER_IMAGE_TAG=2.1.0" Environment="DOCKER_PORT_MAP_TCP_3306=3306" Environment="MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true" Environment="MYSQL_AUTOSTART_MYSQLD_WRAPPER=true" From 7e2af834c753170065bf923d33b42c2f3e5ede61 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 17 Nov 2018 13:21:17 +0000 Subject: [PATCH 10/98] #193: Udates source image to 2.4.1. --- CHANGELOG.md | 4 ++++ Dockerfile | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e6a94..30f25f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Summary of release changes for Version 2. CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. +### 2.1.1 - Unreleased + +- Updates source image to [2.4.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.4.1). + ### 2.1.0 - 2018-08-23 - Updates source image to [2.4.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.4.0). diff --git a/Dockerfile b/Dockerfile index 0bc85d1..e7dc700 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # CentOS-7, MySQL 5.7 Community Server # # ============================================================================= -FROM jdeathe/centos-ssh:2.4.0 +FROM jdeathe/centos-ssh:2.4.1 # ----------------------------------------------------------------------------- # Install MySQL From 8791b46ea1eb0e0605e9fdd4fbaeb6ee21fa45cf Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 17 Nov 2018 14:25:27 +0000 Subject: [PATCH 11/98] #191: Adds missing error messages to internal healthcheck. --- CHANGELOG.md | 1 + src/usr/bin/healthcheck | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f25f7..2cd2d1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. ### 2.1.1 - Unreleased - Updates source image to [2.4.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.4.1). +- Adds missing error messages to internal healthcheck. ### 2.1.0 - 2018-08-23 diff --git a/src/usr/bin/healthcheck b/src/usr/bin/healthcheck index 7e94cf4..3ce5d8e 100755 --- a/src/usr/bin/healthcheck +++ b/src/usr/bin/healthcheck @@ -235,6 +235,9 @@ function main () if [[ -e /var/lock/subsys/mysqld-bootstrap ]] \ || ! __is_mysql_data_directory_populated then + __print_message \ + "error" \ + "Bootstrap failed to complete." exit 1 fi fi @@ -244,6 +247,9 @@ function main () if ! ps axo command \ | grep -qE "^${MYSQLD} " then + __print_message \ + "error" \ + "Service process not running." exit 1 fi From ae9b2a46fd9bc04fa6721722a8a1ee79f858e170 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 18 Nov 2018 16:35:31 +0000 Subject: [PATCH 12/98] Release chagnes for 1.9.1/2.1.1 --- CHANGELOG.md | 2 +- Dockerfile | 2 +- README.md | 36 +++++++++---------- .../systemd/system/centos-ssh-mysql@.service | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd2d1e..0fb40b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Summary of release changes for Version 2. CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. -### 2.1.1 - Unreleased +### 2.1.1 - 2018-11-18 - Updates source image to [2.4.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.4.1). - Adds missing error messages to internal healthcheck. diff --git a/Dockerfile b/Dockerfile index e7dc700..d7e5611 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,7 @@ ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true \ # ----------------------------------------------------------------------------- # Set image metadata # ----------------------------------------------------------------------------- -ARG RELEASE_VERSION="2.1.0" +ARG RELEASE_VERSION="2.1.1" LABEL \ maintainer="James Deathe " \ install="docker run \ diff --git a/README.md b/README.md index 1cba74c..3167f83 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ Includes Automated password generation and an option for custom initialisation S ## Overview & links -The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-community` Docker tag. It is recommended to select a specific release tag - the convention is `centos-7-mysql57-community-2.1.0` or `2.1.0` for the [2.1.0](https://github.com/jdeathe/centos-ssh-mysql/tree/2.1.0) release tag. +The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-community` Docker tag. It is recommended to select a specific release tag - the convention is `centos-7-mysql57-community-2.1.1` or `2.1.1` for the [2.1.1](https://github.com/jdeathe/centos-ssh-mysql/tree/2.1.1) release tag. ### Tags and respective `Dockerfile` links -- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.1.0`, `2.1.0` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, `centos-6-1.9.0`, `1.9.0` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.1.1`, `2.1.1` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- `centos-6`, `centos-6-1.9.1`, `1.9.1` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) The Dockerfile can be used to build a base image that is the bases for several other docker images. @@ -43,7 +43,7 @@ $ docker run -d \ --name mysql.pool-1.1.1 \ -p 3306:3306 \ -v /var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.1.0 + jdeathe/centos-ssh-mysql:2.1.1 ``` Now you can verify it is initialised and running successfully by inspecting the container's logs. @@ -114,10 +114,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.1.0 \ + jdeathe/centos-ssh-mysql:2.1.1 \ /usr/sbin/scmi install \ --chroot=/media/root \ - --tag=2.1.0 \ + --tag=2.1.1 \ --name=mysql.pool-1.1.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -131,10 +131,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.1.0 \ + jdeathe/centos-ssh-mysql:2.1.1 \ /usr/sbin/scmi uninstall \ --chroot=/media/root \ - --tag=2.1.0 \ + --tag=2.1.1 \ --name=mysql.pool-1.1.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -148,10 +148,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.1.0 \ + jdeathe/centos-ssh-mysql:2.1.1 \ /usr/sbin/scmi install \ --chroot=/media/root \ - --tag=2.1.0 \ + --tag=2.1.1 \ --name=mysql.pool-1.1.1 \ --manager=systemd \ --register \ @@ -178,7 +178,7 @@ To see detailed information about the image run `scmi` with the `--info` option. $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:2.1.0 + jdeathe/centos-ssh-mysql:2.1.1 ) --info" ``` @@ -188,7 +188,7 @@ To perform an installation using the docker name `mysql.pool-1.2.1` simply use t $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:2.1.0 + jdeathe/centos-ssh-mysql:2.1.1 ) --name=mysql.pool-1.2.1" ``` @@ -198,7 +198,7 @@ To uninstall use the *same command* that was used to install but with the `unins $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.uninstall}}" \ - jdeathe/centos-ssh-mysql:2.1.0 + jdeathe/centos-ssh-mysql:2.1.1 ) --name=mysql.pool-1.2.1" ``` @@ -211,7 +211,7 @@ To see detailed information about the image run `scmi` with the `--info` option. ``` $ sudo -E atomic install \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:2.1.0 \ + jdeathe/centos-ssh-mysql:2.1.1 \ --info ``` @@ -220,14 +220,14 @@ To perform an installation using the docker name `mysql.pool-1.3.1` simply use t ``` $ sudo -E atomic install \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:2.1.0 + jdeathe/centos-ssh-mysql:2.1.1 ``` Alternatively, you could use the `scmi` options `--name` or `-n` for naming the container. ``` $ sudo -E atomic install \ - jdeathe/centos-ssh-mysql:2.1.0 \ + jdeathe/centos-ssh-mysql:2.1.1 \ --name mysql.pool-1.3.1 ``` @@ -236,7 +236,7 @@ To uninstall use the *same command* that was used to install but with the `unins ``` $ sudo -E atomic uninstall \ -n mysql.pool-1.3.1 \ - jdeathe/centos-ssh-mysql:2.1.0 + jdeathe/centos-ssh-mysql:2.1.1 ``` #### Using environment variables @@ -255,7 +255,7 @@ $ docker run \ --env "MYSQL_USER_PASSWORD=" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume mysql.pool-1.1.1.data-mysql:/var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.1.0 + jdeathe/centos-ssh-mysql:2.1.1 ``` The environmental variable `MYSQL_SUBNET` is optional but can be used to generate users with access to databases outside the `localhost`, (the default for the root user). In the example, the subnet definition `0.0.0.0/0.0.0.0` allows connections from any network which is equivalent to the wildcard symbol, `%`, in MySQL GRANT definitions. diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index 31ae6ba..2db2461 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -52,7 +52,7 @@ Environment="DOCKER_USER=jdeathe" Environment="DOCKER_IMAGE_NAME=centos-ssh-mysql" Environment="DOCKER_CONTAINER_OPTS=" Environment="DOCKER_IMAGE_PACKAGE_PATH=/var/opt/scmi/packages" -Environment="DOCKER_IMAGE_TAG=2.1.0" +Environment="DOCKER_IMAGE_TAG=2.1.1" Environment="DOCKER_PORT_MAP_TCP_3306=3306" Environment="MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true" Environment="MYSQL_AUTOSTART_MYSQLD_WRAPPER=true" From 6e410b311bc96eca06b4c60dc75b598679c0a67e Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Feb 2019 20:03:04 +0000 Subject: [PATCH 13/98] #203: Updates upstream source image to 2.5.0. --- CHANGELOG.md | 4 ++++ Dockerfile | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb40b5..128f28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Summary of release changes for Version 2. CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. +### 2.2.0 - Unreleased + +- Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). + ### 2.1.1 - 2018-11-18 - Updates source image to [2.4.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.4.1). diff --git a/Dockerfile b/Dockerfile index d7e5611..ddcd210 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # CentOS-7, MySQL 5.7 Community Server # # ============================================================================= -FROM jdeathe/centos-ssh:2.4.1 +FROM jdeathe/centos-ssh:2.5.0 # ----------------------------------------------------------------------------- # Install MySQL From 9c6a14bd66a9f75b1467b783cbb6cfdfeeaacb70 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Feb 2019 20:24:04 +0000 Subject: [PATCH 14/98] #203: Updates and restructures Dockerfile. --- CHANGELOG.md | 1 + Dockerfile | 68 ++++++++++++++++++++++++++-------------------------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 128f28f..a62c3ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. ### 2.2.0 - Unreleased - Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). +- Updates and restructures Dockerfile. ### 2.1.1 - 2018-11-18 diff --git a/Dockerfile b/Dockerfile index ddcd210..e6ffd79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,18 @@ -# ============================================================================= -# jdeathe/centos-ssh-mysql -# -# CentOS-7, MySQL 5.7 Community Server -# -# ============================================================================= FROM jdeathe/centos-ssh:2.5.0 -# ----------------------------------------------------------------------------- -# Install MySQL -# ----------------------------------------------------------------------------- -RUN { \ - echo '[mysql57-community]'; \ - echo 'name=MySQL 5.7 Community Server'; \ - echo 'baseurl=http://repo.mysql.com/yum/mysql-5.7-community/el/7/$basearch/'; \ - echo 'gpgcheck=1'; \ - echo 'enabled=1'; \ - echo 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql'; \ +ARG RELEASE_VERSION="2.1.1" + +# ------------------------------------------------------------------------------ +# Base install of required packages +# ------------------------------------------------------------------------------ +RUN { printf -- \ + '[%s]\nname=%s\nbaseurl=%s\ngpgcheck=%s\nenabled=%s\ngpgkey=%s\n' \ + 'mysql57-community' \ + 'MySQL 5.7 Community Server' \ + 'http://repo.mysql.com/yum/mysql-5.7-community/el/7/$basearch/' \ + '1' \ + '1' \ + 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql'; \ } > /etc/yum.repos.d/mysql-community.repo \ && rpm --import \ https://repo.mysql.com/RPM-GPG-KEY-mysql \ @@ -29,15 +26,9 @@ RUN { \ && rm -rf /var/cache/yum/* \ && yum clean all -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Copy files into place -# ----------------------------------------------------------------------------- -ADD src/usr/bin \ - /usr/bin/ -ADD src/usr/sbin \ - /usr/sbin/ -ADD src/opt/scmi \ - /opt/scmi/ +# ------------------------------------------------------------------------------ ADD src/etc/systemd/system \ /etc/systemd/system/ ADD src/etc/services-config/mysql/my.cnf \ @@ -45,7 +36,17 @@ ADD src/etc/services-config/mysql/my.cnf \ /etc/services-config/mysql/ ADD src/etc/services-config/supervisor/supervisord.d \ /etc/services-config/supervisor/supervisord.d/ +ADD src/opt/scmi \ + /opt/scmi/ +ADD src/usr/bin \ + /usr/bin/ +ADD src/usr/sbin \ + /usr/sbin/ +# ------------------------------------------------------------------------------ +# Provisioning +# - Set permissions +# ------------------------------------------------------------------------------ RUN ln -sf \ /etc/services-config/mysql/my.cnf \ /etc/my.cnf \ @@ -68,22 +69,21 @@ EXPOSE 3306 # ----------------------------------------------------------------------------- # Set default environment variables # ----------------------------------------------------------------------------- -ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true \ - MYSQL_AUTOSTART_MYSQLD_WRAPPER=true \ +ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="true" \ + MYSQL_AUTOSTART_MYSQLD_WRAPPER="true" \ MYSQL_ROOT_PASSWORD="" \ - MYSQL_ROOT_PASSWORD_HASHED=false \ + MYSQL_ROOT_PASSWORD_HASHED="false" \ MYSQL_SUBNET="127.0.0.1" \ MYSQL_USER="" \ MYSQL_USER_DATABASE="" \ MYSQL_USER_PASSWORD="" \ - MYSQL_USER_PASSWORD_HASHED=false \ - SSH_AUTOSTART_SSHD=false \ - SSH_AUTOSTART_SSHD_BOOTSTRAP=false + MYSQL_USER_PASSWORD_HASHED="false" \ + SSH_AUTOSTART_SSHD="false" \ + SSH_AUTOSTART_SSHD_BOOTSTRAP="false" -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Set image metadata -# ----------------------------------------------------------------------------- -ARG RELEASE_VERSION="2.1.1" +# ------------------------------------------------------------------------------ LABEL \ maintainer="James Deathe " \ install="docker run \ @@ -120,4 +120,4 @@ HEALTHCHECK \ --retries=10 \ CMD ["/usr/bin/healthcheck"] -CMD ["/usr/bin/supervisord", "--configuration=/etc/supervisord.conf"] \ No newline at end of file +CMD ["/usr/bin/supervisord", "--configuration=/etc/supervisord.conf"] From 8e02a5a38d88a922cb7a01117241b1d0a621df3e Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Feb 2019 21:37:25 +0000 Subject: [PATCH 15/98] #203: Removes use of /etc/services-config paths. --- CHANGELOG.md | 1 + Dockerfile | 33 +++++-------------- src/etc/{services-config/mysql => }/my.cnf | 0 .../mysql => }/mysqld-bootstrap.conf | 0 src/etc/services-config/mysql/my.cnf.default | 27 --------------- .../supervisord.d/mysqld-bootstrap.conf | 0 .../supervisord.d/mysqld-wrapper.conf | 0 7 files changed, 9 insertions(+), 52 deletions(-) rename src/etc/{services-config/mysql => }/my.cnf (100%) rename src/etc/{services-config/mysql => }/mysqld-bootstrap.conf (100%) delete mode 100644 src/etc/services-config/mysql/my.cnf.default rename src/etc/{services-config/supervisor => }/supervisord.d/mysqld-bootstrap.conf (100%) rename src/etc/{services-config/supervisor => }/supervisord.d/mysqld-wrapper.conf (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a62c3ef..95ae80d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). - Updates and restructures Dockerfile. +- Removes use of `/etc/services-config` paths. ### 2.1.1 - 2018-11-18 diff --git a/Dockerfile b/Dockerfile index e6ffd79..af3bc66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,38 +29,21 @@ RUN { printf -- \ # ------------------------------------------------------------------------------ # Copy files into place # ------------------------------------------------------------------------------ -ADD src/etc/systemd/system \ - /etc/systemd/system/ -ADD src/etc/services-config/mysql/my.cnf \ - src/etc/services-config/mysql/mysqld-bootstrap.conf \ - /etc/services-config/mysql/ -ADD src/etc/services-config/supervisor/supervisord.d \ - /etc/services-config/supervisor/supervisord.d/ +ADD src/etc \ + /etc/ ADD src/opt/scmi \ /opt/scmi/ -ADD src/usr/bin \ - /usr/bin/ -ADD src/usr/sbin \ - /usr/sbin/ +ADD src/usr \ + /usr/ # ------------------------------------------------------------------------------ # Provisioning # - Set permissions # ------------------------------------------------------------------------------ -RUN ln -sf \ - /etc/services-config/mysql/my.cnf \ - /etc/my.cnf \ - && ln -sf \ - /etc/services-config/mysql/mysqld-bootstrap.conf \ - /etc/mysqld-bootstrap.conf \ - && ln -sf \ - /etc/services-config/supervisor/supervisord.d/mysqld-bootstrap.conf \ - /etc/supervisord.d/mysqld-bootstrap.conf \ - && ln -sf \ - /etc/services-config/supervisor/supervisord.d/mysqld-wrapper.conf \ - /etc/supervisord.d/mysqld-wrapper.conf \ - && chmod 600 \ - /etc/services-config/mysql/{my.cnf,mysqld-bootstrap.conf} \ +RUN chmod 600 \ + /etc/{my.cnf,mysqld-bootstrap.conf} \ + && chmod 644 \ + /etc/supervisord.d/mysqld-{bootstrap,wrapper}.conf \ && chmod 700 \ /usr/{bin/healthcheck,sbin/mysqld-{bootstrap,wrapper}} diff --git a/src/etc/services-config/mysql/my.cnf b/src/etc/my.cnf similarity index 100% rename from src/etc/services-config/mysql/my.cnf rename to src/etc/my.cnf diff --git a/src/etc/services-config/mysql/mysqld-bootstrap.conf b/src/etc/mysqld-bootstrap.conf similarity index 100% rename from src/etc/services-config/mysql/mysqld-bootstrap.conf rename to src/etc/mysqld-bootstrap.conf diff --git a/src/etc/services-config/mysql/my.cnf.default b/src/etc/services-config/mysql/my.cnf.default deleted file mode 100644 index 637e69e..0000000 --- a/src/etc/services-config/mysql/my.cnf.default +++ /dev/null @@ -1,27 +0,0 @@ -# For advice on how to change settings please see -# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html - -[mysqld] -# -# Remove leading # and set to the amount of RAM for the most important data -# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. -# innodb_buffer_pool_size = 128M -# -# Remove leading # to turn on a very important data integrity option: logging -# changes to the binary log between backups. -# log_bin -# -# Remove leading # to set options mainly useful for reporting servers. -# The server defaults are faster for transactions and fast SELECTs. -# Adjust sizes as needed, experiment to find the optimal values. -# join_buffer_size = 128M -# sort_buffer_size = 2M -# read_rnd_buffer_size = 2M -datadir=/var/lib/mysql -socket=/var/lib/mysql/mysql.sock - -# Disabling symbolic-links is recommended to prevent assorted security risks -symbolic-links=0 - -log-error=/var/log/mysqld.log -pid-file=/var/run/mysqld/mysqld.pid \ No newline at end of file diff --git a/src/etc/services-config/supervisor/supervisord.d/mysqld-bootstrap.conf b/src/etc/supervisord.d/mysqld-bootstrap.conf similarity index 100% rename from src/etc/services-config/supervisor/supervisord.d/mysqld-bootstrap.conf rename to src/etc/supervisord.d/mysqld-bootstrap.conf diff --git a/src/etc/services-config/supervisor/supervisord.d/mysqld-wrapper.conf b/src/etc/supervisord.d/mysqld-wrapper.conf similarity index 100% rename from src/etc/services-config/supervisor/supervisord.d/mysqld-wrapper.conf rename to src/etc/supervisord.d/mysqld-wrapper.conf From 2e1fe1cd2bfd89f7b3ee5cc7ec1b9c7db68ee1f9 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Feb 2019 21:49:08 +0000 Subject: [PATCH 16/98] #203: Removes code from configuration file. --- CHANGELOG.md | 1 + src/etc/mysqld-bootstrap.conf | 17 ++++++----------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ae80d..9658f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). - Updates and restructures Dockerfile. - Removes use of `/etc/services-config` paths. +- Removes code from configuration file `/etc/mysqld-bootstrap.conf`. ### 2.1.1 - 2018-11-18 diff --git a/src/etc/mysqld-bootstrap.conf b/src/etc/mysqld-bootstrap.conf index bd168cc..1ca0561 100644 --- a/src/etc/mysqld-bootstrap.conf +++ b/src/etc/mysqld-bootstrap.conf @@ -1,11 +1,9 @@ -#!/usr/bin/env bash - # Set the time to wait for the MySQL initialisation process before exiting. -MYSQL_INIT_LIMIT=${MYSQL_INIT_LIMIT:-60} +MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Application container configuration -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP:-true}" MYSQL_AUTOSTART_MYSQLD_WRAPPER="${MYSQL_AUTOSTART_MYSQLD_WRAPPER:-true}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" @@ -16,10 +14,7 @@ MYSQL_USER_DATABASE="${MYSQL_USER_DATABASE:-}" MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-}" MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Custom SQL run once during initialisation of the database tables -# ----------------------------------------------------------------------------- -printf \ - -v CUSTOM_MYSQL_INIT_SQL \ - -- "-- Custom Initialisation SQL can be included in %s" \ - '/etc/mysqld-bootstrap.conf' +# ------------------------------------------------------------------------------ +CUSTOM_MYSQL_INIT_SQL="-- Custom Initialisation SQL can be included in /etc/mysqld-bootstrap.conf" From 9f78a7167a21c27e491edaa115bd8f53ddc32bab Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Feb 2019 22:34:45 +0000 Subject: [PATCH 17/98] #203: Removes X-Fleet section from etcd register template unit-file. --- CHANGELOG.md | 1 + src/etc/systemd/system/centos-ssh-mysql.register@.service | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9658f07..abf51c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Updates and restructures Dockerfile. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. +- Removes X-Fleet section from etcd register template unit-file. ### 2.1.1 - 2018-11-18 diff --git a/src/etc/systemd/system/centos-ssh-mysql.register@.service b/src/etc/systemd/system/centos-ssh-mysql.register@.service index 7420d8f..e299611 100644 --- a/src/etc/systemd/system/centos-ssh-mysql.register@.service +++ b/src/etc/systemd/system/centos-ssh-mysql.register@.service @@ -114,6 +114,3 @@ ExecStop=/bin/bash -c \ [Install] DefaultInstance=1.1 RequiredBy={{SERVICE_UNIT_NAME}}@%i.service - -[X-Fleet] -MachineOf={{SERVICE_UNIT_NAME}}@%i.service From 87f49bd6c06461c0343df043b74af6cf32a7262b Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Feb 2019 22:38:23 +0000 Subject: [PATCH 18/98] #203: Removes hard-coded tag from systemd service unit template. --- CHANGELOG.md | 1 + Dockerfile | 6 +++++- src/etc/systemd/system/centos-ssh-mysql@.service | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf51c4..370d6e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). - Updates and restructures Dockerfile. +- Adds placeholder replacement of `RELEASE_VERSION` docker argument to systemd service unit template. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. diff --git a/Dockerfile b/Dockerfile index af3bc66..1c557df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,9 +38,13 @@ ADD src/usr \ # ------------------------------------------------------------------------------ # Provisioning +# - Replace placeholders with values in systemd service unit template # - Set permissions # ------------------------------------------------------------------------------ -RUN chmod 600 \ +RUN sed -i \ + -e "s~{{RELEASE_VERSION}}~${RELEASE_VERSION}~g" \ + /etc/systemd/system/centos-ssh-mysql@.service \ + && chmod 600 \ /etc/{my.cnf,mysqld-bootstrap.conf} \ && chmod 644 \ /etc/supervisord.d/mysqld-{bootstrap,wrapper}.conf \ diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index 2db2461..b3ef901 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -52,7 +52,7 @@ Environment="DOCKER_USER=jdeathe" Environment="DOCKER_IMAGE_NAME=centos-ssh-mysql" Environment="DOCKER_CONTAINER_OPTS=" Environment="DOCKER_IMAGE_PACKAGE_PATH=/var/opt/scmi/packages" -Environment="DOCKER_IMAGE_TAG=2.1.1" +Environment="DOCKER_IMAGE_TAG={{RELEASE_VERSION}}" Environment="DOCKER_PORT_MAP_TCP_3306=3306" Environment="MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true" Environment="MYSQL_AUTOSTART_MYSQLD_WRAPPER=true" From 62c6f9fff312c81547615391a4c648b47b9ad99e Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Feb 2019 23:02:31 +0000 Subject: [PATCH 19/98] ISSUE #203: Adds more reliable method for testing health status. --- CHANGELOG.md | 1 + test/health_status | 264 ++++++++++++++++++++++++++++++++++ test/shpec/operation_shpec.sh | 58 +++++--- 3 files changed, 303 insertions(+), 20 deletions(-) create mode 100755 test/health_status diff --git a/CHANGELOG.md b/CHANGELOG.md index 370d6e5..703a83c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). - Updates and restructures Dockerfile. - Adds placeholder replacement of `RELEASE_VERSION` docker argument to systemd service unit template. +- Adds consideration for event lag into test cases for unhealthy health_status events. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. diff --git a/test/health_status b/test/health_status new file mode 100755 index 0000000..c26a7b9 --- /dev/null +++ b/test/health_status @@ -0,0 +1,264 @@ +#!/usr/bin/env bash + +function __cleanup () +{ + local -r fifo_path="${1:-}" + local -r pid="${2:-0}" + + if [[ -p ${fifo_path} ]] + then + rm -f "${fifo_path}" + fi + + if (( ${pid} <= 1 )) + then + return 0 + fi + + kill \ + -15 \ + -- ${pid} \ + > /dev/null \ + 2>&1 +} + +function __print_status () +{ + local -r character_positive='✓' + local -r character_negative='✗' + local colour_negative='\033[1;31m' + local colour_positive='\033[1;32m' + local colour_reset='\033[0m' + local type="${1:-}" + local message="${2:-${type}}" + + if [[ ${option_quiet} == true ]] + then + return 0 + fi + + # Allow for uncolourised output + if [[ ${option_monochrome} == true ]] + then + unset \ + colour_negative \ + colour_notice \ + colour_positive \ + colour_reset + fi + + case "${type}" in + healthy|starting) + printf -- \ + '%b%s%b %s\n' \ + "${colour_positive}" \ + "${character_positive}" \ + "${colour_reset}" \ + "${message}" + ;; + timeout|unhealthy) + printf -- \ + '%b%s%b %s\n' \ + "${colour_negative}" \ + "${character_negative}" \ + "${colour_reset}" \ + "${message}" \ + >&2 + ;; + esac +} + +function __usage() +{ + cat <<-EOF + Usage: $(basename ${0}) -c [OPTIONS] + $(basename ${0}) --container= [OPTIONS] + $(basename ${0}) [-h|--help] + + Gets health_status events and returns the status. + + Options: + -c, --container=NAME Container name or id. + -h, --help Show this help and exit. + --monochrome Output colour is suppressed. + -q, --quiet Display less message output except for errors. + --since=TIMESTAMP Unix timestamp from which to limit events. + Defaults to start time. + -t, --timeout=SECONDS Timeout value in seconds. Defaults to 10. + Set to 0 for no timeout. + EOF + + exit 1 +} + +function health_status () +{ + local -r fifo_path="$( + mktemp -u + )" + local -r pattern_healthy='^health_status: healthy$' + local -r pattern_starting='^health_status: starting$' + local -r pattern_timeout='^[0-9]+$' + local -r pattern_timestamp='^[0-9]{10}$' + local -r pattern_unhealthy='^health_status: unhealthy$' + + option_monochrome=false + option_quiet=false + local container + local events_command + local health_status + local pid + local since + local since_timestamp="$( + date +%s + )" + local timeout=10 + local until_timestamp + local until + + # Parse install options + while [[ "${#}" -gt 0 ]] + do + case "${1}" in + -h|--help) + __usage + break + ;; + --monochrome) + option_monochrome=true + shift 1 + ;; + -c) + if [[ -z ${2:-} ]] + then + __usage + fi + container="${2:-}" + shift 2 + ;; + --container=*) + container="${1#*=}" + shift 1 + ;; + -q|--quiet) + option_quiet=true + shift 1 + ;; + --since=*) + since_timestamp="${1#*=}" + shift 1 + ;; + -t) + if [[ -z ${2:-} ]] + then + __usage + fi + timeout="${2:-}" + shift 2 + ;; + --timeout=*) + timeout="${1#*=}" + shift 1 + ;; + *) + __usage + ;; + esac + done + + if [[ -z ${container} ]] + then + __usage + fi + + if ! [[ ${timeout} =~ ${pattern_timeout} ]] + then + printf -- \ + '[ERROR] Invalid --time value.\n' \ + >&2 + __usage + fi + + # Set end time limit + until="" + if (( timeout > 0 )) + then + until_timestamp="$(( + $( + date +%s + ) + + ${timeout} + ))" + + until="$( + printf -- \ + '--until %s' \ + "${until_timestamp}" + )" + fi + + # Fail if operator attempts start time limit before end limit. + if ! [[ ${since_timestamp} =~ ${pattern_timestamp} ]] \ + || (( since_timestamp > until_timestamp )) + then + printf -- \ + '[ERROR] Invalid --since value.\n' \ + >&2 + __usage + fi + + # Set start time limit + since="$( + printf -- \ + '--since %s' \ + "${since_timestamp}" + )" + + trap \ + "__cleanup \"${fifo_path}\"" \ + INT TERM EXIT + + mkfifo \ + -m 0600 \ + "${fifo_path}" + + docker events \ + --format '{{.Status}}' \ + --filter "event=health_status" \ + --filter "container=${container}" \ + ${since} \ + ${until} \ + > "${fifo_path}" \ + & + pid="${!}" + disown + + trap \ + "__cleanup \"${fifo_path}\" \"${pid}\"" \ + INT TERM EXIT + + while IFS= read -r health_status || [[ -n ${health_status} ]] + do + if [[ ${health_status} =~ ${pattern_starting} ]] + then + __print_status "starting" + fi + + if [[ ${health_status} =~ ${pattern_healthy} ]] + then + __print_status "healthy" + return 0 + fi + + if [[ ${health_status} =~ ${pattern_unhealthy} ]] + then + __print_status "unhealthy" + return 1 + fi + done < "${fifo_path}" + + __print_status "timeout" + return 1 +} + +health_status "${@}" diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 25db3e4..0dbd4fa 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -1088,9 +1088,11 @@ function test_custom_configuration () function test_healthcheck () { + local -r event_lag_seconds=2 local -r interval_seconds=1 local -r retries=10 - local health_status="" + local events_since_timestamp + local health_status trap "__terminate_container mysql.pool-1.1.1 &> /dev/null; \ __destroy; \ @@ -1109,6 +1111,10 @@ function test_healthcheck () jdeathe/centos-ssh-mysql:latest \ &> /dev/null + events_since_timestamp="$( + date +%s + )" + it "Returns a valid status on starting." health_status="$( docker inspect \ @@ -1121,23 +1127,27 @@ function test_healthcheck () "\"(starting|healthy|unhealthy)\"" end - sleep $( - awk \ - -v interval_seconds="${interval_seconds}" \ - -v startup_time="${STARTUP_TIME}" \ - 'BEGIN { print 1 + interval_seconds + startup_time; }' - ) - it "Returns healthy after startup." + events_timeout="$( + awk \ + -v event_lag="${event_lag_seconds}" \ + -v interval="${interval_seconds}" \ + -v startup_time="${STARTUP_TIME}" \ + 'BEGIN { print event_lag + startup_time + interval; }' + )" + health_status="$( - docker inspect \ - --format='{{json .State.Health.Status}}' \ - mysql.pool-1.1.1 + test/health_status \ + --container=mysql.pool-1.1.1 \ + --since="${events_since_timestamp}" \ + --timeout="${events_timeout}" \ + --monochrome \ + 2>&1 )" assert equal \ "${health_status}" \ - "\"healthy\"" + "✓ healthy" end it "Returns unhealthy on failure." @@ -1153,22 +1163,30 @@ function test_healthcheck () kill -9 -\$(ps axo pgid,command | grep -P '/usr/sbin/mysqld --pid-file=/var/run/mysqld/mysqld.pid$' | awk '{ print \$1; }') fi" - sleep $( + events_since_timestamp="$( + date +%s + )" + + events_timeout="$( awk \ - -v interval_seconds="${interval_seconds}" \ + -v event_lag="${event_lag_seconds}" \ + -v interval="${interval_seconds}" \ -v retries="${retries}" \ - 'BEGIN { print 1 + interval_seconds * retries; }' - ) + 'BEGIN { print event_lag + (interval * retries); }' + )" health_status="$( - docker inspect \ - --format='{{json .State.Health.Status}}' \ - mysql.pool-1.1.1 + test/health_status \ + --container=mysql.pool-1.1.1 \ + --since="$(( ${event_lag_seconds} + ${events_since_timestamp} ))" \ + --timeout="${events_timeout}" \ + --monochrome \ + 2>&1 )" assert equal \ "${health_status}" \ - "\"unhealthy\"" + "✗ unhealthy" end __terminate_container \ From 7b8899883867a81feec758f07cfaa94646912fdf Mon Sep 17 00:00:00 2001 From: James Deathe Date: Tue, 19 Feb 2019 20:07:26 +0000 Subject: [PATCH 20/98] #203: Updates container naming conventions. --- CHANGELOG.md | 6 + Makefile | 799 +++++++++++++----- README.md | 50 +- command-keys.md | 22 +- default.mk | 42 +- environment.mk | 16 +- .../system/centos-ssh-mysql.register@.service | 100 ++- .../systemd/system/centos-ssh-mysql@.service | 118 ++- src/opt/scmi/default.sh | 34 +- src/opt/scmi/environment.sh | 16 +- src/opt/scmi/service-unit.sh | 10 +- src/usr/bin/healthcheck | 70 +- test/shpec/operation_shpec.sh | 186 ++-- 13 files changed, 1035 insertions(+), 434 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 703a83c..00bd84a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,17 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). - Updates and restructures Dockerfile. +- Updates container naming conventions and readability of `Makefile`. +- Fixes issue with unexpected published port in run templates when `DOCKER_PORT_MAP_TCP_3306` is set to an empty string or 0. - Adds placeholder replacement of `RELEASE_VERSION` docker argument to systemd service unit template. - Adds consideration for event lag into test cases for unhealthy health_status events. +- Adds port incrementation to Makefile's run template for container names with an instance suffix. +- Adds supervisord check to healthcheck script and removes unnecessary source script. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. +- Removes the unused group element from the default container name. +- Removes the node element from the default container name. ### 2.1.1 - 2018-11-18 diff --git a/Makefile b/Makefile index c599184..5ebb343 100644 --- a/Makefile +++ b/Makefile @@ -14,22 +14,22 @@ Targets: clean Clean up build artifacts. create Execute the create container template. dist Pull a release version from the registry and save a - package suitable for offline distribution. Image is + package suitable for offline distribution. Image is saved as a tar archive, compressed with xz. distclean Clean up distribution artifacts. exec COMMAND [ARG...] Run command in a the running container. help Show this help. - install Terminate running container and run the docker + install Terminate running container and run the docker create template. images Show container's image details. - load Loads from the distribution package. Requires + load Loads from the distribution package. Requires DOCKER_IMAGE_TAG variable. logs Display log output from the running container. logs-delayed Display log output from the running container after - backing off for STARTUP_TIME seconds. This can be + backing off for STARTUP_TIME seconds. This can be necessary when chaining make targets together. pause Pause the running container. - pull Pull the release image from the registry. Requires + pull Pull the release image from the registry. Requires the DOCKER_IMAGE_TAG variable. ps Display the details of the container process. restart Restarts the container. @@ -43,26 +43,25 @@ Targets: unpause Unpause the container when in a paused state. Variables: - - DOCKER_CONTAINER_OPTS Set optional docker parameters to append that will + - DOCKER_CONTAINER_OPTS Set optional docker parameters to append that will be appended to the create and run templates. - DOCKER_IMAGE_TAG Defines the image tag name. - DOCKER_NAME Container name. The required format is as follows - where and are required numeric - values and group is optional. - {|.[group]}.. - - DOCKER_PORT_MAP_TCP_* The port map variable is used to define the initial - port mapping to use for the docker host value where + where and are numeric values. + [.[.]] + - DOCKER_PORT_MAP_TCP_* The port map variable is used to define the initial + port mapping to use for the docker host value where "*" corresponds to an exposed port on the container. - Setting this to an empty string or 0 will result in - an automatically assigned port and setting to NULL + Setting this to an empty string or 0 will result in + an automatically assigned port and setting to NULL will prevent the port from being published. - - DOCKER_RESTART_POLICY Defines the container restart policy. - - DIST_PATH Ouput directory path - where the release package + - DOCKER_RESTART_POLICY Defines the container restart policy. + - DIST_PATH Ouput directory path - where the release package artifacts are placed. - - NO_CACHE When true, no cache will be used while running the + - NO_CACHE When true, no cache will be used while running the build target. - - STARTUP_TIME Defines the number of seconds expected to complete - the startup process, including the bootstrap where + - STARTUP_TIME Defines the number of seconds expected to complete + the startup process, including the bootstrap where applicable. endef @@ -128,6 +127,23 @@ get-docker-info := $(shell \ $(docker) info \ ) +define get-docker-image-id +$$(if [[ -n $$($(docker) images -q \ + $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(1) \ + ) ]]; \ +then \ + printf -- '%s\n' \ + "$$($(docker) images -q \ + $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(1) \ + )"; \ +else \ + printf -- '%s\n' \ + "$$($(docker) images -q \ + docker.io/$(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(1) \ + )"; \ +fi) +endef + .PHONY: \ _prerequisites \ _require-docker-container \ @@ -182,77 +198,179 @@ ifeq ($(get-docker-info),) endif _require-docker-container: - @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)This operation requires the $(DOCKER_NAME) docker container."; \ - echo "$(PREFIX_SUB_STEP)Try installing it with: make install"; \ + @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; \ + then \ + printf -- '%sThis operation requires the %s container.\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "$(DOCKER_NAME)" \ + >&2; \ + printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(DOCKER_NAME)" \ + "install" \ + >&2; \ exit 1; \ fi _require-docker-container-not: - @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)This operation requires the $(DOCKER_NAME) docker container be removed (or renamed)."; \ - echo "$(PREFIX_SUB_STEP)Try removing it with: make rm"; \ + @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; \ + then \ + printf -- '%sThis operation requires the %s container %s.\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "$(DOCKER_NAME)" \ + "be removed or renamed" \ + >&2; \ + printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(DOCKER_NAME)" \ + "rm" \ + >&2; \ exit 1; \ fi _require-docker-container-not-status-paused: - @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=paused") ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)This operation requires the $(DOCKER_NAME) docker container to be unpaused."; \ - echo "$(PREFIX_SUB_STEP)Try unpausing it with: make unpause"; \ + @ if [[ -n $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=paused" \ + ) ]]; \ + then \ + printf -- '%sThis operation requires the %s container %s.\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "$(DOCKER_NAME)" \ + "to be unpaused" \ + >&2; \ + printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(DOCKER_NAME)" \ + "unpause" \ + >&2; \ exit 1; \ fi _require-docker-container-status-created: - @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=created") ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)This operation requires the $(DOCKER_NAME) docker container to be created."; \ - echo "$(PREFIX_SUB_STEP)Try installing it with: make install"; \ + @ if [[ -z $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=created" \ + ) ]]; \ + then \ + printf -- '%sThis operation requires the %s container %s.\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "$(DOCKER_NAME)" \ + "to be created" \ + >&2; \ + printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(DOCKER_NAME)" \ + "install" \ + >&2; \ exit 1; \ fi _require-docker-container-status-exited: - @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=exited") ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)This operation requires the $(DOCKER_NAME) docker container to be exited."; \ - echo "$(PREFIX_SUB_STEP)Try stopping it with: make stop"; \ + @ if [[ -z $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=exited" \ + ) ]]; \ + then \ + printf -- '%sThis operation requires the %s container %s.\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "$(DOCKER_NAME)" \ + "to be exited" \ + >&2; \ + printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(DOCKER_NAME)" \ + "stop" \ + >&2; \ exit 1; \ fi _require-docker-container-status-paused: - @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=paused") ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)This operation requires the $(DOCKER_NAME) docker container to be paused."; \ - echo "$(PREFIX_SUB_STEP)Try pausing it with: make pause"; \ + @ if [[ -z $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=paused" \ + ) ]]; \ + then \ + printf -- '%sThis operation requires the %s container %s.\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "$(DOCKER_NAME)" \ + "to be paused" \ + >&2; \ + printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(DOCKER_NAME)" \ + "pause" \ + >&2; \ exit 1; \ fi _require-docker-container-status-running: - @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=running") ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)This operation requires the $(DOCKER_NAME) docker container to be running."; \ - echo "$(PREFIX_SUB_STEP)Try starting it with: make start"; \ + @ if [[ -z $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=running" \ + ) ]]; \ + then \ + printf -- '%sThis operation requires the %s container %s.\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "$(DOCKER_NAME)" \ + "to be running" \ + >&2; \ + printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(DOCKER_NAME)" \ + "start" \ + >&2; \ exit 1; \ fi _require-docker-image-tag: - @ if [[ -z $$(if [[ $(DOCKER_IMAGE_TAG) =~ $(DOCKER_IMAGE_TAG_PATTERN) ]]; then echo $(DOCKER_IMAGE_TAG); else echo ''; fi) ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)Invalid DOCKER_IMAGE_TAG value: $(DOCKER_IMAGE_TAG)"; \ + @ if ! [[ "$(DOCKER_IMAGE_TAG)" =~ $(DOCKER_IMAGE_TAG_PATTERN) ]]; \ + then \ + printf -- '%sInvalid %s value: %s\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "DOCKER_IMAGE_TAG" \ + "$(DOCKER_IMAGE_TAG)" \ + >&2; \ exit 1; \ fi _require-docker-release-tag: - @ if [[ -z $$(if [[ $(DOCKER_IMAGE_TAG) =~ $(DOCKER_IMAGE_RELEASE_TAG_PATTERN) ]]; then echo $(DOCKER_IMAGE_TAG); else echo ''; fi) ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)Invalid DOCKER_IMAGE_TAG value: $(DOCKER_IMAGE_TAG)"; \ - echo "$(PREFIX_SUB_STEP)A release tag is required for this operation."; \ + @ if ! [[ "$(DOCKER_IMAGE_TAG)" =~ $(DOCKER_IMAGE_RELEASE_TAG_PATTERN) ]]; \ + then \ + printf -- '%sInvalid %s value: %s\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "DOCKER_IMAGE_TAG" \ + "$(DOCKER_IMAGE_TAG)" \ + >&2; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "A release tag is required for this operation." \ + >&2; \ exit 1; \ fi _require-package-path: - @ if [[ -n $(DIST_PATH) ]] && [[ ! -d $(DIST_PATH) ]]; then \ - echo "$(PREFIX_STEP)Creating package directory"; \ + @ if [[ -n $(DIST_PATH) ]] && [[ ! -d $(DIST_PATH) ]]; \ + then \ + printf -- '%s%\n' \ + "$(PREFIX_STEP)" \ + "Creating package directory"; \ mkdir -p $(DIST_PATH); \ fi; \ - if [[ ! $${?} -eq 0 ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)Failed to make package path: $(DIST_PATH)"; \ + if [[ ! $${?} -eq 0 ]]; \ + then \ + printf -- '%s%s: %s\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "Failed to make package path" \ + "$(DIST_PATH)" \ + >&2; \ exit 1; \ - elif [[ -z $(DIST_PATH) ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)Undefined DIST_PATH"; \ + elif [[ -z $(DIST_PATH) ]]; \ + then \ + printf -- '%sUndefined %s\n' \ + "$(PREFIX_STEP_NEGATIVE)" \ + "DIST_PATH" \ + >&2; \ exit 1; \ fi @@ -264,252 +382,557 @@ endif _usage: @: $(info $(USAGE)) -all: _prerequisites | build images install start ps +all: \ + _prerequisites \ + | \ + build \ + images \ + install \ + start \ + ps -# build NO_CACHE=[{false,true}] -build: _prerequisites _require-docker-image-tag - @ echo "$(PREFIX_STEP)Building $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" - @ if [[ $(NO_CACHE) == true ]]; then \ - echo "$(PREFIX_SUB_STEP)Skipping cache"; \ +build: \ + _prerequisites \ + _require-docker-image-tag + @ printf -- '%sBuilding %s/%s:%s\n' \ + "$(PREFIX_STEP)" \ + "$(DOCKER_USER)" \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)" + @ if [[ $(NO_CACHE) == true ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "Skipping cache"; \ fi @ $(docker) build \ --no-cache=$(NO_CACHE) \ -t $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) \ .; \ - if [[ $${?} -eq 0 ]]; then \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Build complete"; \ + if [[ $${?} -eq 0 ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Build complete"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Build error"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Build error" \ + >&2; \ exit 1; \ fi -clean: _prerequisites | terminate rmi +clean: \ + _prerequisites \ + | \ + terminate \ + rmi -create: _prerequisites _require-docker-container-not - @ echo "$(PREFIX_STEP)Creating container" +create: \ + _prerequisites \ + _require-docker-container-not + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Creating container" @ set -x; \ $(docker) create \ $(DOCKER_CONTAINER_PARAMETERS) \ $(DOCKER_PUBLISH) \ $(DOCKER_CONTAINER_OPTS) \ - $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) 1> /dev/null; - @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=created") ]]; then \ - echo "$(PREFIX_SUB_STEP)$$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=created")"; \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Container created"; \ + $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) \ + 1> /dev/null + @ if [[ -n $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=created" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=created" \ + )"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container created"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Container creation failed"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Container creation failed" \ + >&2; \ exit 1; \ fi -dist: _prerequisites _require-docker-release-tag _require-package-path | pull +dist: \ + _prerequisites \ + _require-docker-release-tag \ + _require-package-path \ + | \ + pull $(eval $@_dist_path := $(realpath \ $(DIST_PATH) \ )) - @ if [[ -s $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz ]]; then \ - echo "$(PREFIX_STEP)Saving package"; \ - echo "$(PREFIX_SUB_STEP)Package path: $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz"; \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Package already exists"; \ + $(eval $@_dist_file := $(shell \ + printf -- '%s.%s.tar.xz' \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)" \ + )) + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Saving package" + @ if [[ -s $($@_dist_path)/$($@_dist_file) ]]; \ + then \ + printf -- '%sPackage path: %s/%s.%s.tar.xz\n' \ + "$(PREFIX_SUB_STEP)" \ + "$($@_dist_path)" \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Package already exists"; \ else \ - echo "$(PREFIX_STEP)Saving package"; \ $(docker) save \ - $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) | \ - $(xz) -9 > \ - $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz; \ - if [[ $${?} -eq 0 ]]; then \ - echo "$(PREFIX_SUB_STEP)Package path: $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz"; \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Package saved"; \ + $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) \ + | $(xz) -9 \ + > $($@_dist_path)/$($@_dist_file); \ + if [[ $${?} -eq 0 ]]; \ + then \ + printf -- '%sPackage path: %s/%s.%s.tar.xz\n' \ + "$(PREFIX_SUB_STEP)" \ + "$($@_dist_path)" \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Package saved"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Package save error"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Package save error" \ + >&2; \ exit 1; \ fi; \ fi -distclean: _prerequisites _require-docker-release-tag _require-package-path | clean +distclean: \ + _prerequisites \ + _require-docker-release-tag \ + _require-package-path \ + | \ + clean $(eval $@_dist_path := $(realpath \ $(DIST_PATH) \ )) - @ if [[ -e $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz ]]; then \ - echo "$(PREFIX_STEP)Deleting package"; \ - echo "$(PREFIX_SUB_STEP)Package path: $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz"; \ + $(eval $@_dist_file := $(shell \ + printf -- '%s.%s.tar.xz' \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)" \ + )) + @ if [[ -e $($@_dist_path)/$($@_dist_file) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Deleting package"; \ + printf -- '%sPackage path: %s/%s.%s.tar.xz\n' \ + "$(PREFIX_SUB_STEP)" \ + "$($@_dist_path)" \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)"; \ find $($@_dist_path) \ - -name $(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz \ + -name $($@_dist_file) \ -delete; \ - if [[ ! -e $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz ]]; then \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Package cleanup complete"; \ + if [[ ! -e $($@_dist_path)/$($@_dist_file) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Package cleanup complete"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Package cleanup failed"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Package cleanup failed" \ + >&2; \ exit 1; \ fi; \ else \ - echo "$(PREFIX_STEP)Package cleanup skipped"; \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Package cleanup skipped"; \ fi -exec: _prerequisites +exec: \ + _prerequisites @ $(docker) exec -it $(DOCKER_NAME) $(filter-out $@, $(MAKECMDGOALS)) %:; @: -images: _prerequisites +images: \ + _prerequisites @ $(docker) images \ - $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG); + $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) -help: _usage +help: \ + _usage -install: | _prerequisites terminate create +install: | \ + _prerequisites \ + terminate \ + create -logs: _prerequisites +logs: \ + _prerequisites @ $(docker) logs $(DOCKER_NAME) -logs-delayed: _prerequisites +logs-delayed: \ + _prerequisites @ sleep $(STARTUP_TIME) @ $(MAKE) logs -load: _prerequisites _require-docker-release-tag _require-package-path +load: \ + _prerequisites \ + _require-docker-release-tag \ + _require-package-path $(eval $@_dist_path := $(realpath \ $(DIST_PATH) \ )) - @ echo "$(PREFIX_STEP)Loading image from package"; \ - echo "$(PREFIX_SUB_STEP)Package path: $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz"; \ - if [[ ! -s $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz ]]; then \ - echo "$(PREFIX_STEP_NEGATIVE)Package not found"; \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)To create a package try: DOCKER_IMAGE_TAG=\"$(DOCKER_IMAGE_TAG)\" make dist"; \ + $(eval $@_dist_file := $(shell \ + printf -- '%s.%s.tar.xz' \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)" \ + )) + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Loading image from package"; \ + printf -- '%sPackage path: %s/%s.%s.tar.xz\n' \ + "$(PREFIX_SUB_STEP)" \ + "$($@_dist_path)" \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)"; \ + if [[ ! -s $($@_dist_path)/$($@_dist_file) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Package not found" \ + >&2; \ + printf -- '%sTry: DOCKER_IMAGE_TAG=%s make %s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "$(DOCKER_IMAGE_TAG)" \ + "dist" \ + >&2; \ exit 1; \ else \ - $(xz) -dc $($@_dist_path)/$(DOCKER_IMAGE_NAME).$(DOCKER_IMAGE_TAG).tar.xz | \ - $(docker) load; \ - echo "$(PREFIX_SUB_STEP)$$(if [[ -n $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)) ]]; then echo $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)); else echo $$($(docker) images -q docker.io/$(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)); fi;)"; \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Image loaded"; \ + $(xz) -dc \ + $($@_dist_path)/$($@_dist_file) \ + | $(docker) load; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(call get-docker-image-id,$(DOCKER_IMAGE_TAG))"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Image loaded"; \ fi -pause: _prerequisites _require-docker-container-status-running - @ echo "$(PREFIX_STEP)Pausing container" - @ $(docker) pause $(DOCKER_NAME) 1> /dev/null - @ echo "$(PREFIX_SUB_STEP_POSITIVE)Container paused" - -pull: _prerequisites _require-docker-image-tag - @ echo "$(PREFIX_STEP)Pulling image from registry" +pause: \ + _prerequisites \ + _require-docker-container-status-running + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Pausing container" + @ $(docker) pause \ + $(DOCKER_NAME) \ + 1> /dev/null + @ printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container paused" + +pull: \ + _prerequisites \ + _require-docker-image-tag + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Pulling image from registry" @ $(docker) pull \ $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG); \ - if [[ $${?} -eq 0 ]]; then \ - echo "$(PREFIX_SUB_STEP)$$(if [[ -n $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)) ]]; then echo $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)); else echo $$($(docker) images -q docker.io/$(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)); fi;)"; \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Image pulled"; \ + if [[ $${?} -eq 0 ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(call get-docker-image-id,$(DOCKER_IMAGE_TAG))"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Image pulled"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Error pulling image"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Error pulling image" \ + >&2; \ exit 1; \ fi -ps: _prerequisites _require-docker-container - @ $(docker) ps -as --filter "name=$(DOCKER_NAME)"; - -restart: _prerequisites _require-docker-container _require-docker-container-not-status-paused - @ echo "$(PREFIX_STEP)Restarting container" - @ $(docker) restart $(DOCKER_NAME) 1> /dev/null - @ echo "$(PREFIX_SUB_STEP_POSITIVE)Container restarted" +ps: \ + _prerequisites \ + _require-docker-container + @ $(docker) ps -as \ + --filter "name=$(DOCKER_NAME)" -rm: _prerequisites _require-docker-container-not-status-paused - @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; then \ - echo "$(PREFIX_STEP)Container removal skipped"; \ +restart: \ + _prerequisites \ + _require-docker-container \ + _require-docker-container-not-status-paused + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Restarting container" + @ $(docker) restart \ + $(DOCKER_NAME) \ + 1> /dev/null + @ printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container restarted" + +rm: \ + _prerequisites \ + _require-docker-container-not-status-paused + @ if [[ -z $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Container removal skipped"; \ else \ - echo "$(PREFIX_STEP)Removing container"; \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Removing container"; \ $(docker) rm -f $(DOCKER_NAME); \ - if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; then \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Container removed"; \ + if [[ -z $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container removed"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Container removal failed"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Container removal failed" \ + >&2; \ exit 1; \ fi; \ fi -rmi: _prerequisites _require-docker-image-tag _require-docker-container-not - @ if [[ -n $$(if [[ -n $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)) ]]; then echo $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)); else echo $$($(docker) images -q docker.io/$(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)); fi;) ]]; then \ - echo "$(PREFIX_STEP)Untagging image"; \ - echo "$(PREFIX_SUB_STEP)$$(if [[ -n $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)) ]]; then echo $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)); else echo $$($(docker) images -q docker.io/$(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)); fi;) : $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)"; \ +rmi: \ + _prerequisites \ + _require-docker-image-tag \ + _require-docker-container-not + @ if [[ -n $(call get-docker-image-id,$(DOCKER_IMAGE_TAG)) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Untagging image"; \ + printf -- '%s%s : %s/%s:%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$(call get-docker-image-id,$(DOCKER_IMAGE_TAG))" \ + "$(DOCKER_USER)" \ + "$(DOCKER_IMAGE_NAME)" \ + "$(DOCKER_IMAGE_TAG)"; \ $(docker) rmi \ - $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) 1> /dev/null; \ - if [[ $${?} -eq 0 ]]; then \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Image untagged"; \ + $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) \ + 1> /dev/null; \ + if [[ $${?} -eq 0 ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Image untagged"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Error untagging image"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Error untagging image" \ + >&2; \ exit 1; \ fi; \ else \ - echo "$(PREFIX_STEP)Untagging image skipped"; \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Untagging image skipped"; \ fi -run: _prerequisites _require-docker-image-tag - @ echo "$(PREFIX_STEP)Running container" +run: \ + _prerequisites \ + _require-docker-image-tag + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Running container" @ set -x; \ $(docker) run \ --detach \ $(DOCKER_CONTAINER_PARAMETERS) \ $(DOCKER_PUBLISH) \ $(DOCKER_CONTAINER_OPTS) \ - $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) 1> /dev/null; - @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=running") ]]; then \ - echo "$(PREFIX_SUB_STEP)$$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=running")"; \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Container running"; \ + $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) \ + 1> /dev/null + @ if [[ -n $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=running" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "$$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=running" \ + )"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container running"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Container run failed"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Container run failed" \ + >&2; \ exit 1; \ fi -start: _prerequisites _require-docker-container _require-docker-container-not-status-paused - @ echo "$(PREFIX_STEP)Starting container" +start: \ + _prerequisites \ + _require-docker-container \ + _require-docker-container-not-status-paused + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Starting container" @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]] \ - && [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=running") ]]; then \ - $(docker) start $(DOCKER_NAME) 1> /dev/null; \ + && [[ -z $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=running" \ + ) ]]; \ + then \ + $(docker) start \ + $(DOCKER_NAME) \ + 1> /dev/null; \ fi - @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=running") ]]; then \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Container started"; \ + @ if [[ -n $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=running" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container started"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Container start failed"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Container start failed" \ + >&2; \ exit 1; \ fi -stop: _prerequisites _require-docker-container-not-status-paused _require-docker-container-status-running - @ echo "$(PREFIX_STEP)Stopping container" - @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=running") ]]; then \ - $(docker) stop $(DOCKER_NAME) 1> /dev/null; \ - if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=exited") ]]; then \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Container stopped"; \ +stop: \ + _prerequisites \ + _require-docker-container-not-status-paused \ + _require-docker-container-status-running + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Stopping container" + @ if [[ -n $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=running" \ + ) ]]; \ + then \ + $(docker) stop \ + $(DOCKER_NAME) \ + 1> /dev/null; \ + if [[ -n $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=exited" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container stopped"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Error stopping container"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Error stopping container" \ + >&2; \ exit 1; \ fi; \ fi -terminate: _prerequisites - @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; then \ - echo "$(PREFIX_STEP)Container termination skipped"; \ +terminate: \ + _prerequisites + @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Container termination skipped"; \ else \ - echo "$(PREFIX_STEP)Terminating container"; \ - if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=paused") ]]; then \ - echo "$(PREFIX_SUB_STEP)Unpausing container"; \ - $(docker) unpause $(DOCKER_NAME) 1> /dev/null; \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Terminating container"; \ + if [[ -n $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=paused" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "Unpausing container"; \ + $(docker) unpause \ + $(DOCKER_NAME) \ + 1> /dev/null; \ fi; \ - if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)" --filter "status=running") ]]; then \ - echo "$(PREFIX_SUB_STEP)Stopping container"; \ - $(docker) stop $(DOCKER_NAME) 1> /dev/null; \ + if [[ -n $$($(docker) ps -aq \ + --filter "name=$(DOCKER_NAME)" \ + --filter "status=running" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "Stopping container"; \ + $(docker) stop \ + $(DOCKER_NAME) \ + 1> /dev/null; \ fi; \ - if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; then \ - echo "$(PREFIX_SUB_STEP)Removing container"; \ - $(docker) rm -f $(DOCKER_NAME) 1> /dev/null; \ + if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP)" \ + "Removing container"; \ + $(docker) rm -f \ + $(DOCKER_NAME) \ + 1> /dev/null; \ fi; \ - if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; then \ - echo "$(PREFIX_SUB_STEP_POSITIVE)Container terminated"; \ + if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container terminated"; \ else \ - echo "$(PREFIX_SUB_STEP_NEGATIVE)Container termination failed"; \ + printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_NEGATIVE)" \ + "Container termination failed" \ + >&2; \ exit 1; \ fi; \ fi -test: _test-prerequisites - @ if [[ -z $$(if [[ -n $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):latest) ]]; then echo $$($(docker) images -q $(DOCKER_USER)/$(DOCKER_IMAGE_NAME):latest); else echo $$($(docker) images -q docker.io/$(DOCKER_USER)/$(DOCKER_IMAGE_NAME):latest); fi;) ]]; then \ - $(MAKE) build; \ - fi; - @ echo "$(PREFIX_STEP)Functional test"; - @ SHPEC_ROOT=$(SHPEC_ROOT) $(shpec); - -unpause: _prerequisites _require-docker-container-status-paused - @ echo "$(PREFIX_STEP)Unpausing container" - @ $(docker) unpause $(DOCKER_NAME) 1> /dev/null - @ echo "$(PREFIX_SUB_STEP_POSITIVE)Container unpaused" +test: \ + _test-prerequisites + @ if [[ -z $(call get-docker-image-id,latest) ]]; \ + then \ + DOCKER_IMAGE_TAG=latest $(MAKE) build; \ + fi + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Functional test" + @ SHPEC_ROOT=$(SHPEC_ROOT) $(shpec) + +unpause: \ + _prerequisites \ + _require-docker-container-status-paused + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Unpausing container" + @ $(docker) unpause \ + $(DOCKER_NAME) \ + 1> /dev/null + @ printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container unpaused" diff --git a/README.md b/README.md index 3167f83..fa12ddb 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,11 @@ For cases where access to docker exec is not possible the preferred method is to ## Quick Example -Run up a container named `mysql.pool-1.1.1` from the docker image `jdeathe/centos-ssh-mysql` on port 3306 of your docker host. +Run up a container named `mysql.1` from the docker image `jdeathe/centos-ssh-mysql` on port 3306 of your docker host. ``` $ docker run -d \ - --name mysql.pool-1.1.1 \ + --name mysql.1 \ -p 3306:3306 \ -v /var/lib/mysql \ jdeathe/centos-ssh-mysql:2.1.1 @@ -49,7 +49,7 @@ $ docker run -d \ Now you can verify it is initialised and running successfully by inspecting the container's logs. ``` -$ docker logs mysql.pool-1.1.1 +$ docker logs mysql.1 ``` On the first run, there will be additional output showing the initialisation SQL template and, before mysqld-bootstrap completes, the MySQL Details which shows the configured database, if applicable, and any associated user credentials. @@ -63,13 +63,13 @@ The MySQL table data is persistent across container restarts by setting the MySQ ``` $ docker inspect \ --format '{{ json (index .Mounts 0).Source }}' \ - mysql.pool-1.1.1 + mysql.1 ``` To access the MySQL SQL shell run the following: ``` -$ docker exec -it mysql.pool-1.1.1 mysql -p -u root +$ docker exec -it mysql.1 mysql -p -u root ``` To import the Sakila example database from the [MySQL Documentation](https://dev.mysql.com/doc/index-other.html) and view the first 2 records from the film table. @@ -77,20 +77,20 @@ To import the Sakila example database from the [MySQL Documentation](https://dev ``` $ export MYSQL_ROOT_PASSWORD={your-password} -$ docker exec -i mysql.pool-1.1.1 \ +$ docker exec -i mysql.1 \ mysql -u root -p${MYSQL_ROOT_PASSWORD} \ <<< $(curl -sSL http://downloads.mysql.com/docs/sakila-db.tar.gz \ | tar -xzO - "sakila-db/sakila-schema.sql" \ | sed -e '/^CREATE TABLE film_text/,/ENGINE=InnoDB / s/InnoDB/MyISAM/' ) -$ docker exec -i mysql.pool-1.1.1 \ +$ docker exec -i mysql.1 \ mysql -u root -p${MYSQL_ROOT_PASSWORD} \ <<< $(curl -sSL http://downloads.mysql.com/docs/sakila-db.tar.gz \ | tar -xzO - "sakila-db/sakila-data.sql" ) -$ docker exec mysql.pool-1.1.1 \ +$ docker exec mysql.1 \ mysql -u root -p${MYSQL_ROOT_PASSWORD} \ -e "SELECT * FROM sakila.film LIMIT 2 \G" ``` @@ -103,7 +103,7 @@ To run the a docker container from this image you can use the standard docker co #### SCMI Installation Examples -The following example uses docker to run the SCMI install command to create and start a container named `mysql.pool-1.1.1`. To use SCMI it requires the use of the `--privileged` docker run parameter and the docker host's root directory mounted as a volume with the container's mount directory also being set in the `scmi` `--chroot` option. The `--setopt` option is used to add extra parameters to the default docker run command template; in the following example a named configuration volume is added which allows the SSH host keys to persist after the first container initialisation. Not that the placeholder `{{NAME}}` can be used in this option and is replaced with the container's name. +The following example uses docker to run the SCMI install command to create and start a container named `mysql.1`. To use SCMI it requires the use of the `--privileged` docker run parameter and the docker host's root directory mounted as a volume with the container's mount directory also being set in the `scmi` `--chroot` option. The `--setopt` option is used to add extra parameters to the default docker run command template; in the following example a named configuration volume is added which allows the SSH host keys to persist after the first container initialisation. Not that the placeholder `{{NAME}}` can be used in this option and is replaced with the container's name. *Note:* In most cases you will want to create an initial database, database user, (optionally a static password), and define the user's network access. If you don't define these settings using the appropriate environment variables on first run, the settings will not be parsed by the bootstrap initialisation process and only local root access will be available. To re-initialise a container that uses a named data volume mapped to /var/lib/mysql terminate the container and the data volume to allow it to be recreated. @@ -118,7 +118,7 @@ $ docker run \ /usr/sbin/scmi install \ --chroot=/media/root \ --tag=2.1.1 \ - --name=mysql.pool-1.1.1 \ + --name=mysql.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -135,7 +135,7 @@ $ docker run \ /usr/sbin/scmi uninstall \ --chroot=/media/root \ --tag=2.1.1 \ - --name=mysql.pool-1.1.1 \ + --name=mysql.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -152,7 +152,7 @@ $ docker run \ /usr/sbin/scmi install \ --chroot=/media/root \ --tag=2.1.1 \ - --name=mysql.pool-1.1.1 \ + --name=mysql.1 \ --manager=systemd \ --register \ --env='MYSQL_SUBNET="0.0.0.0/0.0.0.0"' \ @@ -182,14 +182,14 @@ $ eval "sudo -E $( ) --info" ``` -To perform an installation using the docker name `mysql.pool-1.2.1` simply use the `--name` or `-n` option. +To perform an installation using the docker name `mysql.2` simply use the `--name` or `-n` option. ``` $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ jdeathe/centos-ssh-mysql:2.1.1 - ) --name=mysql.pool-1.2.1" + ) --name=mysql.2" ``` To uninstall use the *same command* that was used to install but with the `uninstall` Label. @@ -199,7 +199,7 @@ $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.uninstall}}" \ jdeathe/centos-ssh-mysql:2.1.1 - ) --name=mysql.pool-1.2.1" + ) --name=mysql.2" ``` ##### SCMI on Atomic Host @@ -210,16 +210,16 @@ To see detailed information about the image run `scmi` with the `--info` option. ``` $ sudo -E atomic install \ - -n mysql.pool-1.3.1 \ + -n mysql.3 \ jdeathe/centos-ssh-mysql:2.1.1 \ --info ``` -To perform an installation using the docker name `mysql.pool-1.3.1` simply use the `-n` option of the `atomic install` command. +To perform an installation using the docker name `mysql.3` simply use the `-n` option of the `atomic install` command. ``` $ sudo -E atomic install \ - -n mysql.pool-1.3.1 \ + -n mysql.3 \ jdeathe/centos-ssh-mysql:2.1.1 ``` @@ -228,14 +228,14 @@ Alternatively, you could use the `scmi` options `--name` or `-n` for naming the ``` $ sudo -E atomic install \ jdeathe/centos-ssh-mysql:2.1.1 \ - --name mysql.pool-1.3.1 + --name mysql.3 ``` To uninstall use the *same command* that was used to install but with the `uninstall` Label. ``` $ sudo -E atomic uninstall \ - -n mysql.pool-1.3.1 \ + -n mysql.3 \ jdeathe/centos-ssh-mysql:2.1.1 ``` @@ -244,17 +244,17 @@ $ sudo -E atomic uninstall \ The following example sets up a custom MySQL database, user and user password on first run. This will only work when MySQL runs the initialisation process and values must be specified for MYSQL_USER and MYSQL_USER_DATABASE. If MYSQL_USER_PASSWORD is not specified or left empty a random password will be generated. ``` -$ docker stop mysql.pool-1.1.1 && \ - docker rm mysql.pool-1.1.1 +$ docker stop mysql.1 && \ + docker rm mysql.1 $ docker run \ --detach \ - --name mysql.pool-1.1.1 \ + --name mysql.1 \ --publish 3306:3306 \ --env "MYSQL_SUBNET=0.0.0.0/0.0.0.0" \ --env "MYSQL_USER=app-user" \ --env "MYSQL_USER_PASSWORD=" \ --env "MYSQL_USER_DATABASE=app-db" \ - --volume mysql.pool-1.1.1.data-mysql:/var/lib/mysql \ + --volume mysql.1.data-mysql:/var/lib/mysql \ jdeathe/centos-ssh-mysql:2.1.1 ``` @@ -263,7 +263,7 @@ The environmental variable `MYSQL_SUBNET` is optional but can be used to generat Now you can verify it is initialised and running successfully by inspecting the container's logs: ``` -$ docker logs mysql.pool-1.1.1 +$ docker logs mysql.1 ``` #### Environment Variables diff --git a/command-keys.md b/command-keys.md index a9c414f..13bfc19 100644 --- a/command-keys.md +++ b/command-keys.md @@ -2,20 +2,20 @@ Using command keys to access containers (without sshd). -Access docker containers using docker host SSH public key authentication and nsenter command to start up a bash terminal inside a container. In the following example the container name is "mysql.pool-1.1.1" +Access docker containers using docker host SSH public key authentication and nsenter command to start up a bash terminal inside a container. In the following example the container name is "mysql.1" ## Create a unique public/private key pair for each container ``` -$ cd ~/.ssh/ && ssh-keygen -q -t rsa -f id-rsa.mysql.pool-1.1.1 +$ cd ~/.ssh/ && ssh-keygen -q -t rsa -f id-rsa.mysql.1 ``` ## Prefix the public key with the nsenter command ``` $ sed -i '' \ - '1s#^#command="sudo nsenter -m -u -i -n -p -t $(docker inspect --format \\\"{{ .State.Pid }}\\\" mysql.pool-1.1.1) /bin/bash" #' \ - ~/.ssh/id-rsa.mysql.pool-1.1.1.pub + '1s#^#command="sudo nsenter -m -u -i -n -p -t $(docker inspect --format \\\"{{ .State.Pid }}\\\" mysql.1) /bin/bash" #' \ + ~/.ssh/id-rsa.mysql.1.pub ``` ## Upload the public key to the docker host VM @@ -25,7 +25,7 @@ The host in this example is core-01.local that has SSH public key authentication ### Generic Linux Host Example ``` -$ cat ~/.ssh/id-rsa.mysql.pool-1.1.1.pub | ssh -i ~/.vagrant.d/insecure_private_key \ +$ cat ~/.ssh/id-rsa.mysql.1.pub | ssh -i ~/.vagrant.d/insecure_private_key \ core@core-01.local \ "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" ``` @@ -33,15 +33,15 @@ $ cat ~/.ssh/id-rsa.mysql.pool-1.1.1.pub | ssh -i ~/.vagrant.d/insecure_private_ ### CoreOS Host Example ``` -$ cat ~/.ssh/id-rsa.mysql.pool-1.1.1.pub | ssh -i ~/.vagrant.d/insecure_private_key \ +$ cat ~/.ssh/id-rsa.mysql.1.pub | ssh -i ~/.vagrant.d/insecure_private_key \ core@core-01.local \ - update-ssh-keys -a core@mysql.pool-1.1.1 + update-ssh-keys -a core@mysql.1 ``` ### Usage ``` -$ ssh -i ~/.ssh/id-rsa.mysql.pool-1.1.1 \ +$ ssh -i ~/.ssh/id-rsa.mysql.1 \ core@core-01.local \ -o StrictHostKeyChecking=no ``` @@ -51,17 +51,17 @@ $ ssh -i ~/.ssh/id-rsa.mysql.pool-1.1.1 \ To simplify the command required to access the running container we can add an entry to the SSH configuration file ```~/.ssh/config``` as follows: ``` -Host core-01.mysql.pool-1.1.1 +Host core-01.mysql.1 HostName core-01.local Port 22 User core StrictHostKeyChecking no IdentitiesOnly yes - IdentityFile ~/.ssh/id-rsa.mysql.pool-1.1.1 + IdentityFile ~/.ssh/id-rsa.mysql.1 ``` With the above entry in place we can now run the following to access the running container: ``` -$ ssh core-01.mysql.pool-1.1.1 +$ ssh core-01.mysql.1 ``` diff --git a/default.mk b/default.mk index 417ce62..0e3c14c 100644 --- a/default.mk +++ b/default.mk @@ -1,4 +1,42 @@ +# Handle incrementing the docker host port for instances unless a port range is defined. +DOCKER_PUBLISH := $(shell \ + if [[ "$(DOCKER_PORT_MAP_TCP_3306)" != NULL ]]; \ + then \ + if grep -qE \ + '^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:)?[1-9][0-9]*$$' \ + <<< "$(DOCKER_PORT_MAP_TCP_3306)" \ + && grep -qE \ + '^.+\.[0-9]+(\.[0-9]+)?$$' \ + <<< "$(DOCKER_NAME)"; \ + then \ + printf -- ' --publish %s%s:3306' \ + "$$(\ + grep -o '^[0-9\.]*:' \ + <<< "$(DOCKER_PORT_MAP_TCP_3306)" \ + )" \ + "$$(( \ + $$(\ + grep -oE \ + '[0-9]+$$' \ + <<< "$(DOCKER_PORT_MAP_TCP_3306)" \ + ) \ + + $$(\ + grep -oE \ + '([0-9]+)(\.[0-9]+)?$$' \ + <<< "$(DOCKER_NAME)" \ + | awk -F. \ + '{ print $$1; }' \ + ) \ + - 1 \ + ))"; \ + else \ + printf -- ' --publish %s:3306' \ + "$(DOCKER_PORT_MAP_TCP_3306)"; \ + fi; \ + fi; \ +) + # Common parameters of create and run targets define DOCKER_CONTAINER_PARAMETERS --name $(DOCKER_NAME) \ @@ -13,7 +51,3 @@ define DOCKER_CONTAINER_PARAMETERS --env "MYSQL_USER_PASSWORD=$(MYSQL_USER_PASSWORD)" \ --env "MYSQL_USER_PASSWORD_HASHED=$(MYSQL_USER_PASSWORD_HASHED)" endef - -DOCKER_PUBLISH := $(shell \ - if [[ $(DOCKER_PORT_MAP_TCP_3306) != NULL ]]; then printf -- '--publish %s:3306\n' $(DOCKER_PORT_MAP_TCP_3306); fi; \ -) diff --git a/environment.mk b/environment.mk index 0b44757..4b44376 100644 --- a/environment.mk +++ b/environment.mk @@ -1,6 +1,6 @@ -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Constants -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ DOCKER_USER := jdeathe DOCKER_IMAGE_NAME := centos-ssh-mysql SHPEC_ROOT := test/shpec @@ -9,14 +9,14 @@ SHPEC_ROOT := test/shpec DOCKER_IMAGE_TAG_PATTERN := ^(latest|centos-6|centos-7-mysql57-community|(([1-2]|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+))$ DOCKER_IMAGE_RELEASE_TAG_PATTERN := ^(1|2|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+$ -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Variables -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Docker image/container settings DOCKER_CONTAINER_OPTS ?= DOCKER_IMAGE_TAG ?= latest -DOCKER_NAME ?= mysql.pool-1.1.1 +DOCKER_NAME ?= mysql.1 DOCKER_PORT_MAP_TCP_22 ?= NULL DOCKER_PORT_MAP_TCP_3306 ?= 3306 DOCKER_RESTART_POLICY ?= always @@ -30,9 +30,9 @@ DIST_PATH ?= ./dist # Number of seconds expected to complete container startup including bootstrap. STARTUP_TIME ?= 10 -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Application container configuration -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ SSH_AUTHORIZED_KEYS ?= SSH_AUTOSTART_SSHD ?= false SSH_AUTOSTART_SSHD_BOOTSTRAP ?= false @@ -46,7 +46,7 @@ SSH_USER_ID ?= 500:500 SSH_USER_PASSWORD ?= SSH_USER_PASSWORD_HASHED ?= false SSH_USER_SHELL ?= /bin/bash -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP ?= true MYSQL_AUTOSTART_MYSQLD_WRAPPER ?= true MYSQL_ROOT_PASSWORD ?= diff --git a/src/etc/systemd/system/centos-ssh-mysql.register@.service b/src/etc/systemd/system/centos-ssh-mysql.register@.service index e299611..92cde37 100644 --- a/src/etc/systemd/system/centos-ssh-mysql.register@.service +++ b/src/etc/systemd/system/centos-ssh-mysql.register@.service @@ -1,26 +1,26 @@ -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Naming convention: # # centos-ssh-mysql@.service = {service-unit-install-template-name} # | -# +---------------------- {image-name} +# +-------------------- {image-name} # -# mysql.pool-1.register@.service = {service-unit-template-name} -# Y | -# | +------------- {service-unit-companion-name} -# +----------------------- {service-unit-name} +# mysql.register@.service = {service-unit-template-name} +# | | +# | +--------------- {service-unit-companion-name} +# +---------------------- {service-unit-name} # -# mysql.pool-1.register@1.1.service = {service-unit-instance-name} -# Y | Y -# | | +----- {service-unit-instance} -# | +------------ {service-unit-companion-name} -# +----------------------- {service-unit-name} +# mysql.register@1.service = {service-unit-instance-name} +# | | | +# | | +---------- {service-unit-instance} +# | +---------------- {service-unit-companion-name} +# +----------------------- {service-unit-name} # -# mysql.pool-1.1.1.register = {service-unit-long-name} -# Y Y | -# | | +-------- {service-unit-companion-name} -# | +-------------- {service-unit-instance} -# +----------------------- {service-unit-name} +# mysql.1.register = {service-unit-long-name} +# | | | +# | | +------ {service-unit-companion-name} +# | +----------- {service-unit-instance} +# +--------------- {service-unit-name} # # To install: # sudo sed -e "s~{{SERVICE_UNIT_NAME}}~{service-unit-name}~g" \ @@ -37,7 +37,7 @@ # sudo systemctl disable -f {service-unit-instance-name} # sudo rm /etc/systemd/system/{service-unit-template-name} # sudo systemctl daemon-reload -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ [Unit] Description=centos-mysql etcd registration // %p@%i @@ -56,7 +56,12 @@ Environment="REGISTER_UPDATE_INTERVAL=95" # Unregister service ExecStartPre=/bin/bash -c \ - "if /usr/bin/etcdctl ${REGISTER_ETCD_PARAMETERS} ls ${REGISTER_KEY_ROOT} &> /dev/null; then \ + "if /usr/bin/etcdctl \ + ${REGISTER_ETCD_PARAMETERS} \ + ls \ + ${REGISTER_KEY_ROOT} \ + &> /dev/null; \ + then \ /usr/bin/etcdctl \ ${REGISTER_ETCD_PARAMETERS} \ rm \ @@ -66,13 +71,23 @@ ExecStartPre=/bin/bash -c \ # Register service ExecStart=/bin/bash -c \ - "until /usr/bin/etcdctl ${REGISTER_ETCD_PARAMETERS} get ${REGISTER_KEY_ROOT}/ports/tcp/3306 &> /dev/null; do \ - if /usr/bin/docker port {{SERVICE_UNIT_NAME}}.%i 3306 &> /dev/null; then \ + "until /usr/bin/etcdctl \ + ${REGISTER_ETCD_PARAMETERS} \ + get \ + ${REGISTER_KEY_ROOT}/ports/tcp/3306 \ + &> /dev/null; \ + do \ + if /usr/bin/docker port {{SERVICE_UNIT_NAME}}.%i 3306 &> /dev/null; \ + then \ /usr/bin/etcdctl \ ${REGISTER_ETCD_PARAMETERS} \ mk \ ${REGISTER_KEY_ROOT}/ports/tcp/3306 \ - \"$(/usr/bin/docker port {{SERVICE_UNIT_NAME}}.%i 3306 | /usr/bin/sed 's~^[0-9.]*:~~')\" \ + \"$(/usr/bin/docker port \ + {{SERVICE_UNIT_NAME}}.%i \ + 3306 \ + | /usr/bin/sed 's~^[0-9.]*:~~' \ + )\" \ --ttl ${REGISTER_TTL} 2> /dev/null; \ fi; \ sleep 0.5; \ @@ -83,19 +98,43 @@ ExecStart=/bin/bash -c \ ${REGISTER_KEY_ROOT}/hostname \ %H \ --ttl ${REGISTER_TTL}; \ - while true; do \ + while true; \ + do \ sleep ${REGISTER_UPDATE_INTERVAL}; \ - if /usr/bin/docker port {{SERVICE_UNIT_NAME}}.%i 3306 &> /dev/null; then \ + if /usr/bin/docker port {{SERVICE_UNIT_NAME}}.%i 3306 &> /dev/null; \ + then \ /usr/bin/etcdctl \ ${REGISTER_ETCD_PARAMETERS} \ - $(if ! /usr/bin/etcdctl ${REGISTER_ETCD_PARAMETERS} get ${REGISTER_KEY_ROOT}/ports/tcp/3306 &> /dev/null; then echo set; else echo update; fi) \ + $(if ! /usr/bin/etcdctl \ + ${REGISTER_ETCD_PARAMETERS} \ + get \ + ${REGISTER_KEY_ROOT}/ports/tcp/3306 \ + &> /dev/null; \ + then \ + echo set; \ + else \ + echo update; \ + fi) \ ${REGISTER_KEY_ROOT}/ports/tcp/3306 \ - \"$(/usr/bin/docker port {{SERVICE_UNIT_NAME}}.%i 3306 | /usr/bin/sed 's~^[0-9.]*:~~')\" \ + \"$(/usr/bin/docker port \ + {{SERVICE_UNIT_NAME}}.%i \ + 3306 \ + | /usr/bin/sed 's~^[0-9.]*:~~' \ + )\" \ --ttl ${REGISTER_TTL}; \ fi; \ /usr/bin/etcdctl \ ${REGISTER_ETCD_PARAMETERS} \ - $(if ! /usr/bin/etcdctl ${REGISTER_ETCD_PARAMETERS} get ${REGISTER_KEY_ROOT}/hostname &> /dev/null; then echo set; else echo update; fi) \ + $(if ! /usr/bin/etcdctl \ + ${REGISTER_ETCD_PARAMETERS} \ + get \ + ${REGISTER_KEY_ROOT}/hostname \ + &> /dev/null; \ + then \ + echo set; \ + else \ + echo update; \ + fi) \ ${REGISTER_KEY_ROOT}/hostname \ %H \ --ttl ${REGISTER_TTL}; \ @@ -103,7 +142,12 @@ ExecStart=/bin/bash -c \ # Unregister service ExecStop=/bin/bash -c \ - "if /usr/bin/etcdctl ${REGISTER_ETCD_PARAMETERS} ls ${REGISTER_KEY_ROOT} &> /dev/null; then \ + "if /usr/bin/etcdctl \ + ${REGISTER_ETCD_PARAMETERS} \ + ls \ + ${REGISTER_KEY_ROOT} \ + &> /dev/null; \ + then \ /usr/bin/etcdctl \ ${REGISTER_ETCD_PARAMETERS} \ rm \ @@ -112,5 +156,5 @@ ExecStop=/bin/bash -c \ fi" [Install] -DefaultInstance=1.1 +DefaultInstance=1 RequiredBy={{SERVICE_UNIT_NAME}}@%i.service diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index b3ef901..46532c9 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -1,23 +1,23 @@ -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Naming convention: # # centos-ssh-mysql@.service = {service-unit-install-template-name} # | -# +------------ {image-name} +# +-------------------- {image-name} # -# mysql.pool-1@.service = {service-unit-template-name} -# Y -# +------------- {service-unit-name} +# mysql@.service = {service-unit-template-name} +# | +# +------------- {service-unit-name} # -# mysql.pool-1@1.1.service = {service-unit-instance-name} -# Y Y -# | +---- {service-unit-instance} -# +------------- {service-unit-name} +# mysql@1.service = {service-unit-instance-name} +# | | +# | +---------- {service-unit-instance} +# +-------------- {service-unit-name} # -# mysql.pool-1.1.1 = {service-unit-long-name} -# Y Y -# | +---- {service-unit-instance} -# +------------- {service-unit-name} +# mysql.1 = {service-unit-long-name} +# | | +# | +-- {service-unit-instance} +# +------ {service-unit-name} # # To install: # sudo cat {service-unit-install-template-name} \ @@ -37,7 +37,7 @@ # sudo systemctl stop {service-unit-instance-name} # sudo rm /etc/systemd/system/{service-unit-template-name} # sudo docker rm -f {service-unit-long-name} -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ [Unit] Description=centos-ssh-mysql // %p@%i @@ -48,12 +48,12 @@ After=docker.service Restart=on-failure RestartSec=30 TimeoutStartSec=1200 -Environment="DOCKER_USER=jdeathe" -Environment="DOCKER_IMAGE_NAME=centos-ssh-mysql" Environment="DOCKER_CONTAINER_OPTS=" +Environment="DOCKER_IMAGE_NAME=centos-ssh-mysql" Environment="DOCKER_IMAGE_PACKAGE_PATH=/var/opt/scmi/packages" Environment="DOCKER_IMAGE_TAG={{RELEASE_VERSION}}" Environment="DOCKER_PORT_MAP_TCP_3306=3306" +Environment="DOCKER_USER=jdeathe" Environment="MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true" Environment="MYSQL_AUTOSTART_MYSQLD_WRAPPER=true" Environment="MYSQL_ROOT_PASSWORD=" @@ -66,24 +66,59 @@ Environment="MYSQL_USER_PASSWORD_HASHED=false" # Initialisation: Load image from local storage if available, otherwise pull. ExecStartPre=/bin/bash -c \ - "if [[ -z $( if [[ -n $(/usr/bin/docker images -q ${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}) ]]; then echo $(/usr/bin/docker images -q ${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}); else echo $(/usr/bin/docker images -q docker.io/${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}); fi; ) ]]; then \ - if [[ -f ${DOCKER_IMAGE_PACKAGE_PATH}/${DOCKER_USER}/${DOCKER_IMAGE_NAME}.${DOCKER_IMAGE_TAG}.tar.xz ]]; then \ - /usr/bin/xz -dc ${DOCKER_IMAGE_PACKAGE_PATH}/${DOCKER_USER}/${DOCKER_IMAGE_NAME}.${DOCKER_IMAGE_TAG}.tar.xz | /usr/bin/docker load; \ + "if [[ -z $( \ + if [[ -n $(/usr/bin/docker images -q \ + ${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} \ + ) ]]; \ + then \ + echo $(/usr/bin/docker images -q \ + ${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} \ + ); \ + else \ + echo $(/usr/bin/docker images -q \ + docker.io/${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} \ + ); \ + fi; \ + ) ]]; \ + then \ + if [[ -f ${DOCKER_IMAGE_PACKAGE_PATH}/${DOCKER_USER}/${DOCKER_IMAGE_NAME}.${DOCKER_IMAGE_TAG}.tar.xz ]]; \ + then \ + printf -- '%%s/%%s/%%s.%%s.tar.xz\n' \ + \"${DOCKER_IMAGE_PACKAGE_PATH}\" \ + \"${DOCKER_USER}\" \ + \"${DOCKER_IMAGE_NAME}\" \ + \"${DOCKER_IMAGE_TAG}\" \ + | /usr/bin/xargs /usr/bin/xz -dc \ + | /usr/bin/docker load; \ else \ - /usr/bin/docker pull ${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}; \ + printf -- '%%s/%%s:%%s\n' \ + \"${DOCKER_USER}\" \ + \"${DOCKER_IMAGE_NAME}\" \ + \"${DOCKER_IMAGE_TAG}\" \ + | /usr/bin/xargs /usr/bin/docker pull; \ fi; \ fi" # Terminate existing container to allow for redeployment -ExecStartPre=/bin/bash -c \ - "if [[ -n $(/usr/bin/docker ps -aq --filter \"name=%p.%i\") ]]; then \ - if [[ -n $(/usr/bin/docker ps -aq --filter \"name=%p.%i\" --filter \"status=paused\") ]]; then \ +ExecStartPre=-/bin/bash -c \ + "if [[ -n $(/usr/bin/docker ps -aq --filter \"name=%p.%i\") ]]; \ + then \ + if [[ -n $(/usr/bin/docker ps -aq \ + --filter \"name=%p.%i\" \ + --filter \"status=paused\" \ + ) ]]; \ + then \ /usr/bin/docker unpause %p.%i; \ fi; \ - if [[ -n $(/usr/bin/docker ps -aq --filter \"name=%p.%i\" --filter \"status=running\") ]]; then \ + if [[ -n $(/usr/bin/docker ps -aq \ + --filter \"name=%p.%i\" \ + --filter \"status=running\" \ + ) ]]; \ + then \ /usr/bin/docker stop %p.%i; \ fi; \ - if [[ -n $(/usr/bin/docker ps -aq --filter \"name=%p.%i\") ]]; then \ + if [[ -n $(/usr/bin/docker ps -aq --filter \"name=%p.%i\") ]]; \ + then \ /usr/bin/docker rm -f %p.%i; \ fi; \ fi" @@ -101,11 +136,34 @@ ExecStart=/bin/bash -c \ --env \"MYSQL_USER_DATABASE=${MYSQL_USER_DATABASE}\" \ --env \"MYSQL_USER_PASSWORD=${MYSQL_USER_PASSWORD}\" \ --env \"MYSQL_USER_PASSWORD_HASHED=${MYSQL_USER_PASSWORD_HASHED}\" \ - $(if [[ ${DOCKER_PORT_MAP_TCP_3306} != NULL ]]; then \ - if /usr/bin/grep -qE '^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:)?[0-9]*$' <<< \"${DOCKER_PORT_MAP_TCP_3306}\"; then \ + $(if [[ ${DOCKER_PORT_MAP_TCP_3306} != NULL ]]; \ + then \ + if /usr/bin/grep -qE \ + '^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:)?[1-9][0-9]*$' \ + <<< \"${DOCKER_PORT_MAP_TCP_3306}\"; \ + && /usr/bin/grep -qE \ + '^.+\.[0-9]+(\.[0-9]+)?$' \ + <<< "${DOCKER_NAME}" + then \ printf -- '--publish %%s%%s:3306' \ - $(/usr/bin/grep -o '^[0-9\.]*:' <<< \"${DOCKER_PORT_MAP_TCP_3306}\") \ - $(( $(/usr/bin/grep -o '[0-9]*$' <<< \"${DOCKER_PORT_MAP_TCP_3306}\") + $(/usr/bin/sed 's~\.[0-9]*$~~' <<< %i) - 1 )); \ + $(\ + /usr/bin/grep -o \ + '^[0-9\.]*:' \ + <<< \"${DOCKER_PORT_MAP_TCP_3306}\" \ + ) \ + $(( \ + $(\ + /usr/bin/grep -oE \ + '[0-9]+$' \ + <<< \"${DOCKER_PORT_MAP_TCP_3306}\" \ + ) \ + + $(\ + /usr/bin/grep -oE \ + '^[0-9]+' \ + <<< %i \ + ) \ + - 1 \ + )); \ else \ printf -- '--publish %%s:3306' \ \"${DOCKER_PORT_MAP_TCP_3306}\"; \ @@ -119,5 +177,5 @@ ExecStart=/bin/bash -c \ ExecStop=/usr/bin/docker stop --time 10 %p.%i [Install] -DefaultInstance=1.1 +DefaultInstance=1 WantedBy=multi-user.target diff --git a/src/opt/scmi/default.sh b/src/opt/scmi/default.sh index fd43564..6309adc 100644 --- a/src/opt/scmi/default.sh +++ b/src/opt/scmi/default.sh @@ -1,15 +1,39 @@ # Handle incrementing the docker host port for instances unless a port range is defined. DOCKER_PUBLISH= -if [[ ${DOCKER_PORT_MAP_TCP_3306} != NULL ]]; then - if grep -qE '^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:)?[0-9]*$' <<< "${DOCKER_PORT_MAP_TCP_3306}" \ - && grep -qE '^.+\.([0-9]+)\.([0-9]+)$' <<< "${DOCKER_NAME}"; then +if [[ ${DOCKER_PORT_MAP_TCP_3306} != NULL ]] +then + if grep -qE \ + '^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:)?[1-9][0-9]*$' \ + <<< "${DOCKER_PORT_MAP_TCP_3306}" \ + && grep -qE \ + '^.+\.[0-9]+(\.[0-9]+)?$' \ + <<< "${DOCKER_NAME}" + then printf -v \ DOCKER_PUBLISH \ -- '%s --publish %s%s:3306' \ "${DOCKER_PUBLISH}" \ - "$(grep -o '^[0-9\.]*:' <<< "${DOCKER_PORT_MAP_TCP_3306}")" \ - "$(( $(grep -o '[0-9]*$' <<< "${DOCKER_PORT_MAP_TCP_3306}") + $(sed 's~\.[0-9]*$~~' <<< "${DOCKER_NAME}" | awk -F. '{ print $NF; }') - 1 ))" + "$( + grep -o \ + '^[0-9\.]*:' \ + <<< "${DOCKER_PORT_MAP_TCP_3306}" + )" \ + "$(( + $( + grep -oE \ + '[0-9]+$' \ + <<< "${DOCKER_PORT_MAP_TCP_3306}" + ) \ + + $( + grep -oE \ + '([0-9]+)(\.[0-9]+)?$' \ + <<< "${DOCKER_NAME}" \ + | awk -F. \ + '{ print $1; }' + ) \ + - 1 + ))" else printf -v \ DOCKER_PUBLISH \ diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index 9b23cd9..46d34e1 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -1,6 +1,6 @@ -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Constants -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ readonly DOCKER_USER=jdeathe readonly DOCKER_IMAGE_NAME=centos-ssh-mysql @@ -8,14 +8,14 @@ readonly DOCKER_IMAGE_NAME=centos-ssh-mysql readonly DOCKER_IMAGE_TAG_PATTERN='^(latest|centos-6|centos-7-mysql57-community|(([1-2]|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+))$' readonly DOCKER_IMAGE_RELEASE_TAG_PATTERN='^(1|2|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+$' -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Variables -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Docker image/container settings DOCKER_CONTAINER_OPTS="${DOCKER_CONTAINER_OPTS:-}" DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG:-latest}" -DOCKER_NAME="${DOCKER_NAME:-mysql.pool-1.1.1}" +DOCKER_NAME="${DOCKER_NAME:-mysql.1}" DOCKER_PORT_MAP_TCP_22="${DOCKER_PORT_MAP_TCP_22:-}" DOCKER_PORT_MAP_TCP_3306="${DOCKER_PORT_MAP_TCP_3306:-3306}" DOCKER_RESTART_POLICY="${DOCKER_RESTART_POLICY:-always}" @@ -34,9 +34,9 @@ REGISTER_ETCD_PARAMETERS="${REGISTER_ETCD_PARAMETERS:-}" REGISTER_TTL="${REGISTER_TTL:-120}" REGISTER_UPDATE_INTERVAL="${REGISTER_UPDATE_INTERVAL:-95}" -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Application container configuration -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ SSH_AUTHORIZED_KEYS="${SSH_AUTHORIZED_KEYS:-}" SSH_AUTOSTART_SSHD="${SSH_AUTOSTART_SSHD:-true}" SSH_AUTOSTART_SSHD_BOOTSTRAP="${SSH_AUTOSTART_SSHD_BOOTSTRAP:-true}" @@ -50,7 +50,7 @@ SSH_USER_ID="${SSH_USER_ID:-500:500}" SSH_USER_PASSWORD="${SSH_USER_PASSWORD:-}" SSH_USER_PASSWORD_HASHED="${SSH_USER_PASSWORD_HASHED:-false}" SSH_USER_SHELL="${SSH_USER_SHELL:-/bin/bash}" -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP:-true}" MYSQL_AUTOSTART_MYSQLD_WRAPPER="${MYSQL_AUTOSTART_MYSQLD_WRAPPER:-true}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" diff --git a/src/opt/scmi/service-unit.sh b/src/opt/scmi/service-unit.sh index d73c22e..24accaf 100644 --- a/src/opt/scmi/service-unit.sh +++ b/src/opt/scmi/service-unit.sh @@ -1,6 +1,6 @@ -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Constants -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ readonly SERVICE_UNIT_ENVIRONMENT_KEYS=" DOCKER_CONTAINER_OPTS DOCKER_IMAGE_PACKAGE_PATH @@ -22,7 +22,7 @@ readonly SERVICE_UNIT_REGISTER_ENVIRONMENT_KEYS=" REGISTER_UPDATE_INTERVAL " -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Variables -# ----------------------------------------------------------------------------- -SERVICE_UNIT_INSTALL_TIMEOUT=${SERVICE_UNIT_INSTALL_TIMEOUT:-13} +# ------------------------------------------------------------------------------ +SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-13}" diff --git a/src/usr/bin/healthcheck b/src/usr/bin/healthcheck index 3ce5d8e..07adff7 100755 --- a/src/usr/bin/healthcheck +++ b/src/usr/bin/healthcheck @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source /etc/mysqld-bootstrap.conf - function __have_mysql_access () { local db_name="${3:-mysql}" @@ -21,17 +19,13 @@ function __have_mysql_access () return 1 } +# interval must be between 1 and 60 seconds function __is_throttled_interval () { local -i interval="${1:-10}" - local pattern_seconds_in_minute='^([1-9]|[1-5][0-9]|60)$' - - # interval must be between 1 and 60 seconds - if [[ ! ${interval} =~ ${pattern_seconds_in_minute} ]]; then - interval=10 - fi - if (( $(date +%-S)%${interval} == 0 )); then + if (( $(date +%-S)%${interval} == 0 )) + then return 0 else return 1 @@ -47,7 +41,8 @@ function __is_mysql_data_directory_populated () "/var/lib/mysql" )" - if [[ -f ${datadir}/ibdata1 ]]; then + if [[ -f ${datadir}/ibdata1 ]] + then return 0 fi @@ -58,7 +53,8 @@ function __last_check_passed () { local -i status=0 - if [[ ! -f ${STATUS_PATH} ]]; then + if [[ ! -f ${STATUS_PATH} ]] + then return ${status} else read \ @@ -66,7 +62,8 @@ function __last_check_passed () status \ < "${STATUS_PATH}" - if [[ ${status} =~ ^[01]$ ]]; then + if [[ ${status} =~ ^[01]$ ]] + then return ${status} else return 1 @@ -90,9 +87,9 @@ function __mysql_get_option () function __print_message () { local -r type="${1:-}" - local -r quiet=${QUIET:-false} + local -r quiet="${QUIET:-false}" local message="${2:-}" - local prefix="" + local prefix case "${type}" in error) @@ -132,7 +129,8 @@ function __record_exit_status () dirname "${STATUS_PATH}" )" - if [[ ! -d ${status_directory} ]]; then + if [[ ! -d ${status_directory} ]] + then install \ -d \ -m 0660 \ @@ -141,7 +139,8 @@ function __record_exit_status () "${status_directory}" fi - if [[ ! -f ${STATUS_PATH} ]]; then + if [[ ! -f ${STATUS_PATH} ]] + then install \ -m 0660 \ -o root \ @@ -185,21 +184,21 @@ function __usage () function main () { - readonly MYSQLD=/usr/sbin/mysqld - - local -r REDACTED_VALUE="********" local -r STATUS_PATH="/var/lib/healthcheck/status" - local QUITE=false + local -r mysqld="/usr/sbin/mysqld" + local -r pattern_seconds_in_minute='^([1-9]|[1-5][0-9]|60)$' + local -r redacted_value="********" + local QUIET="false" local -i interval=10 local db_password - local pattern_seconds_in_minute='^([1-9]|[1-5][0-9]|60)$' # Trap and record the exit status trap "__record_exit_status \${?}" \ EXIT - while [[ ${#} -gt 0 ]]; do + while [[ "${#}" -gt 0 ]] + do case "${1}" in -h|--help) __usage @@ -214,7 +213,7 @@ function main () shift 1 ;; -q|--quiet) - QUIET=true + QUIET="true" shift 1 ;; *) @@ -223,15 +222,26 @@ function main () esac done - if [[ ! ${interval} =~ ${pattern_seconds_in_minute} ]]; then + if [[ ! ${interval} =~ ${pattern_seconds_in_minute} ]] + then __print_message \ "error" \ "Invalid interval." exit 1 fi + if ! ps axo command \ + | grep -qE '^/usr/bin/python /usr/bin/supervisord' + then + __print_message \ + "error" \ + "supervisord not running." + exit 1 + fi + # mysqld-bootstrap - if [[ ${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP} == true ]]; then + if [[ ${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP} == true ]] + then if [[ -e /var/lock/subsys/mysqld-bootstrap ]] \ || ! __is_mysql_data_directory_populated then @@ -243,9 +253,10 @@ function main () fi # mysqld-wrapper - if [[ ${MYSQL_AUTOSTART_MYSQLD_WRAPPER} == true ]]; then + if [[ ${MYSQL_AUTOSTART_MYSQLD_WRAPPER} == true ]] + then if ! ps axo command \ - | grep -qE "^${MYSQLD} " + | grep -qE "^${mysqld} " then __print_message \ "error" \ @@ -254,7 +265,8 @@ function main () fi # Skip connection tests if password is stored hashed - if [[ ${MYSQL_ROOT_PASSWORD_HASHED} == true ]]; then + if [[ ${MYSQL_ROOT_PASSWORD_HASHED} == true ]] + then exit 0 fi @@ -267,7 +279,7 @@ function main () fi if [[ -n ${MYSQL_ROOT_PASSWORD} ]] \ - && [[ ${MYSQL_ROOT_PASSWORD} != "${REDACTED_VALUE}" ]] + && [[ ${MYSQL_ROOT_PASSWORD} != "${redacted_value}" ]] then db_password="${MYSQL_ROOT_PASSWORD}" else diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 0dbd4fa..a654203 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -10,8 +10,8 @@ function __destroy () { local -r private_network_1="bridge_internal_1" local -r private_network_2="bridge_internal_2" - local -r data_volume_1="mysql.pool-1.1.2.data-mysql" - local -r data_volume_2="mysql.pool-1.1.4.data-mysql" + local -r data_volume_1="mysql.2.data-mysql" + local -r data_volume_2="mysql.4.data-mysql" # Destroy the bridge networks if [[ -n $(docker network ls -q -f name="${private_network_1}") ]]; then @@ -97,8 +97,8 @@ function __is_container_ready () function __reset_data_volume () { - local -r data_volume_1="mysql.pool-1.1.2.data-mysql" - local -r data_volume_2="mysql.pool-1.1.4.data-mysql" + local -r data_volume_1="mysql.2.data-mysql" + local -r data_volume_2="mysql.4.data-mysql" local group="${1:-1}" @@ -142,8 +142,8 @@ function __setup () { local -r private_network_1="bridge_internal_1" local -r private_network_2="bridge_internal_2" - local -r data_volume_1="mysql.pool-1.1.2.data-mysql" - local -r data_volume_2="mysql.pool-1.1.4.data-mysql" + local -r data_volume_1="mysql.2.data-mysql" + local -r data_volume_2="mysql.4.data-mysql" # Create the bridge networks if [[ -z $(docker network ls -q -f name="${private_network_1}") ]]; then @@ -231,7 +231,7 @@ function test_basic_operations () local show_databases="" local show_grants="" - trap "__terminate_container mysql.pool-1.1.1 &> /dev/null; \ + trap "__terminate_container mysql.1 &> /dev/null; \ __destroy; \ exit 1" \ INT TERM EXIT @@ -239,20 +239,20 @@ function test_basic_operations () describe "Basic MySQL operations" describe "Runs named container" __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null it "Can publish ${DOCKER_PORT_MAP_TCP_3306}:3306." docker run \ --detach \ - --name mysql.pool-1.1.1 \ + --name mysql.1 \ --publish ${DOCKER_PORT_MAP_TCP_3306}:3306 \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null container_port_3306="$( __get_container_port \ - mysql.pool-1.1.1 \ + mysql.1 \ 3306/tcp )" @@ -270,7 +270,7 @@ function test_basic_operations () end if ! __is_container_ready \ - mysql.pool-1.1.1 \ + mysql.1 \ ${STARTUP_TIME} \ "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ @@ -285,7 +285,7 @@ function test_basic_operations () it "Sets a 16 character password." mysql_root_password="$( docker logs \ - mysql.pool-1.1.1 \ + mysql.1 \ | grep 'user : root@localhost' \ | sed -e 's~^.*,.*password : \([a-zA-Z0-9]*\).*$~\1~' )" @@ -298,7 +298,7 @@ function test_basic_operations () it "Limits access to localhost only." select_users="$( docker exec \ - mysql.pool-1.1.1 \ + mysql.1 \ mysql \ --batch \ --password="${mysql_root_password}" \ @@ -326,7 +326,7 @@ function test_basic_operations () it "Removes all but the necessary databases." show_databases="$( docker exec \ - mysql.pool-1.1.1 \ + mysql.1 \ mysql \ --batch \ --password="${mysql_root_password}" \ @@ -349,7 +349,7 @@ function test_basic_operations () it "Shows N/A in MySQL Details." docker logs \ - mysql.pool-1.1.1 \ + mysql.1 \ | grep -q 'database : N/A' \ &> /dev/null @@ -363,19 +363,19 @@ function test_basic_operations () describe "Database setup" it "Creates a database on first run." __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null docker run \ --detach \ - --name mysql.pool-1.1.1 \ + --name mysql.1 \ --env "MYSQL_ROOT_PASSWORD=mypasswd" \ --env "MYSQL_USER_DATABASE=my-db" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null if ! __is_container_ready \ - mysql.pool-1.1.1 \ + mysql.1 \ ${STARTUP_TIME} \ "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ @@ -387,7 +387,7 @@ function test_basic_operations () docker exec \ -t \ - mysql.pool-1.1.1 \ + mysql.1 \ mysql \ -pmypasswd \ -uroot \ @@ -401,7 +401,7 @@ function test_basic_operations () it "Has database name in MySQL Details." docker logs \ - mysql.pool-1.1.1 \ + mysql.1 \ | grep -q 'database : my-db' \ &> /dev/null @@ -414,19 +414,19 @@ function test_basic_operations () describe "User setup" it "Creates a user on first run." __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null docker run \ --detach \ - --name mysql.pool-1.1.1 \ + --name mysql.1 \ --env "MYSQL_ROOT_PASSWORD=mypasswd" \ --env "MYSQL_USER=my-user" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null if ! __is_container_ready \ - mysql.pool-1.1.1 \ + mysql.1 \ ${STARTUP_TIME} \ "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ @@ -438,7 +438,7 @@ function test_basic_operations () select_users="$( docker exec \ - mysql.pool-1.1.1 \ + mysql.1 \ mysql \ --batch \ --password=mypasswd \ @@ -465,7 +465,7 @@ function test_basic_operations () it "Has user in MySQL Details." docker logs \ - mysql.pool-1.1.1 \ + mysql.1 \ | grep -q 'user : my-user@localhost' \ &> /dev/null @@ -477,7 +477,7 @@ function test_basic_operations () it "Grants the user USAGE privileges." show_grants="$( docker exec \ - mysql.pool-1.1.1 \ + mysql.1 \ mysql \ --batch \ --password=mypasswd \ @@ -493,7 +493,7 @@ function test_basic_operations () end __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null end @@ -503,8 +503,8 @@ function test_basic_operations () function test_custom_configuration () { - local -r data_volume_1="mysql.pool-1.1.2.data-mysql" - local -r data_volume_2="mysql.pool-1.1.4.data-mysql" + local -r data_volume_1="mysql.2.data-mysql" + local -r data_volume_2="mysql.4.data-mysql" local -r private_network_1="bridge_internal_1" local -r private_network_2="bridge_internal_2" local -r redacted_value="********" @@ -517,29 +517,29 @@ function test_custom_configuration () local select_users="" local show_databases="" - trap "__terminate_container mysql.pool-1.1.2 &> /dev/null; \ - __terminate_container mysql.pool-1.1.3 &> /dev/null; \ - __terminate_container mysql.pool-1.1.4 &> /dev/null; \ - __terminate_container mysql.pool-1.1.5 &> /dev/null; \ + trap "__terminate_container mysql.2 &> /dev/null; \ + __terminate_container mysql.3 &> /dev/null; \ + __terminate_container mysql.4 &> /dev/null; \ + __terminate_container mysql.5 &> /dev/null; \ __destroy; \ exit 1" \ INT TERM EXIT describe "Customised MySQL configuration" __terminate_container \ - mysql.pool-1.1.2 \ + mysql.2 \ &> /dev/null __terminate_container \ - mysql.pool-1.1.3 \ + mysql.3 \ &> /dev/null describe "Single internal network" it "Runs a named server container." docker run \ --detach \ - --name mysql.pool-1.1.2 \ - --network-alias mysql.pool-1.1.2 \ + --name mysql.2 \ + --network-alias mysql.2 \ --network ${private_network_1} \ --env "MYSQL_ROOT_PASSWORD=${mysql_root_password_hash}" \ --env "MYSQL_ROOT_PASSWORD_HASHED=true" \ @@ -559,7 +559,7 @@ function test_custom_configuration () it "Runs a named client container." docker run \ --detach \ - --name mysql.pool-1.1.3 \ + --name mysql.3 \ --network ${private_network_1} \ --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ @@ -572,7 +572,7 @@ function test_custom_configuration () end if ! __is_container_ready \ - mysql.pool-1.1.2 \ + mysql.2 \ ${STARTUP_TIME} \ "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ @@ -585,7 +585,7 @@ function test_custom_configuration () describe "MySQL Details log output" it "Has the database name." docker logs \ - mysql.pool-1.1.2 \ + mysql.2 \ | grep -q 'database : app-db' \ &> /dev/null @@ -598,7 +598,7 @@ function test_custom_configuration () it "Redacts root password." mysql_root_password_log="$( docker logs \ - mysql.pool-1.1.2 \ + mysql.2 \ | grep 'user : root@localhost' \ | sed -e 's~^.*,.*password : \([^ ,:]*\).*$~\1~' )" @@ -611,7 +611,7 @@ function test_custom_configuration () it "Redacts user password." mysql_user_password_log="$( docker logs \ - mysql.pool-1.1.2 \ + mysql.2 \ | grep 'user : app-user@172.172.40.0/255.255.255.0' \ | sed -e 's~^.*,.*password : \([^ ,:]*\).*$~\1~' )" @@ -627,7 +627,7 @@ function test_custom_configuration () it "Creates a subnet restricted user." select_users="$( docker exec \ - mysql.pool-1.1.2 \ + mysql.2 \ mysql \ --batch \ --password="${mysql_root_password}" \ @@ -658,9 +658,9 @@ function test_custom_configuration () show_databases="$( docker exec \ -t \ - mysql.pool-1.1.3 \ + mysql.3 \ mysql \ - -h mysql.pool-1.1.2 \ + -h mysql.2 \ -p${mysql_user_password} \ -uapp-user \ app-db \ @@ -676,7 +676,7 @@ function test_custom_configuration () # Clean up server but keep client running. __terminate_container \ - mysql.pool-1.1.2 \ + mysql.2 \ &> /dev/null end @@ -690,8 +690,8 @@ function test_custom_configuration () it "Runs a named server container." docker run \ --detach \ - --name mysql.pool-1.1.2 \ - --network-alias mysql.pool-1.1.2 \ + --name mysql.2 \ + --network-alias mysql.2 \ --network ${private_network_1} \ --env "MYSQL_ROOT_PASSWORD=/run/secrets/mysql_root_password" \ --env "MYSQL_SUBNET=172.172.40.0/255.255.255.0" \ @@ -709,7 +709,7 @@ function test_custom_configuration () end if ! __is_container_ready \ - mysql.pool-1.1.2 \ + mysql.2 \ ${STARTUP_TIME} \ "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ @@ -723,7 +723,7 @@ function test_custom_configuration () it "Creates a subnet restricted user." select_users="$( docker exec \ - mysql.pool-1.1.2 \ + mysql.2 \ mysql \ --batch \ --password="${mysql_root_password}" \ @@ -754,9 +754,9 @@ function test_custom_configuration () show_databases="$( docker exec \ -t \ - mysql.pool-1.1.3 \ + mysql.3 \ mysql \ - -h mysql.pool-1.1.2 \ + -h mysql.2 \ -p${mysql_user_password} \ -uapp-user \ app-db \ @@ -772,7 +772,7 @@ function test_custom_configuration () # Clean up server but keep client running. __terminate_container \ - mysql.pool-1.1.2 \ + mysql.2 \ &> /dev/null end @@ -786,8 +786,8 @@ function test_custom_configuration () it "Runs a named server container." docker run \ --detach \ - --name mysql.pool-1.1.2 \ - --network-alias mysql.pool-1.1.2 \ + --name mysql.2 \ + --network-alias mysql.2 \ --network ${private_network_1} \ --env "MYSQL_ROOT_PASSWORD=/run/secrets/mysql_root_password_hashed" \ --env "MYSQL_ROOT_PASSWORD_HASHED=true" \ @@ -807,7 +807,7 @@ function test_custom_configuration () end if ! __is_container_ready \ - mysql.pool-1.1.2 \ + mysql.2 \ ${STARTUP_TIME} \ "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ @@ -821,7 +821,7 @@ function test_custom_configuration () it "Creates a subnet restricted user." select_users="$( docker exec \ - mysql.pool-1.1.2 \ + mysql.2 \ mysql \ --batch \ --password="${mysql_root_password}" \ @@ -852,9 +852,9 @@ function test_custom_configuration () show_databases="$( docker exec \ -t \ - mysql.pool-1.1.3 \ + mysql.3 \ mysql \ - -h mysql.pool-1.1.2 \ + -h mysql.2 \ -p${mysql_user_password} \ -uapp-user \ app-db \ @@ -870,22 +870,22 @@ function test_custom_configuration () # Clean up server but keep client running. __terminate_container \ - mysql.pool-1.1.2 \ + mysql.2 \ &> /dev/null end describe "Multiple internal networks" __terminate_container \ - mysql.pool-1.1.4 \ + mysql.4 \ &> /dev/null __terminate_container \ - mysql.pool-1.1.5 \ + mysql.5 \ &> /dev/null it "Runs a named server container." docker create \ - --name mysql.pool-1.1.4 \ + --name mysql.4 \ --network ${private_network_1} \ --env "MYSQL_ROOT_PASSWORD=${mysql_root_password_hash}" \ --env "MYSQL_ROOT_PASSWORD_HASHED=true" \ @@ -900,11 +900,11 @@ function test_custom_configuration () docker network connect \ ${private_network_2} \ - mysql.pool-1.1.4 \ + mysql.4 \ &> /dev/null docker start \ - mysql.pool-1.1.4 \ + mysql.4 \ &> /dev/null assert equal \ @@ -915,7 +915,7 @@ function test_custom_configuration () it "Runs a named client container." docker run \ --detach \ - --name mysql.pool-1.1.5 \ + --name mysql.5 \ --network ${private_network_2} \ --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ @@ -928,7 +928,7 @@ function test_custom_configuration () end if ! __is_container_ready \ - mysql.pool-1.1.4 \ + mysql.4 \ ${STARTUP_TIME} \ "/usr/sbin/mysqld " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ @@ -942,7 +942,7 @@ function test_custom_configuration () it "Creates an unrestricted user." select_users="$( docker exec \ - mysql.pool-1.1.4 \ + mysql.4 \ mysql \ --batch \ --password="${mysql_root_password}" \ @@ -973,9 +973,9 @@ function test_custom_configuration () show_databases="$( docker exec \ -t \ - mysql.pool-1.1.5 \ + mysql.5 \ mysql \ - -h mysql.pool-1.1.4 \ + -h mysql.4 \ -p${mysql_user_password} \ -uapp2-user \ app2-db \ @@ -988,9 +988,9 @@ function test_custom_configuration () show_databases+="$( docker exec \ -t \ - mysql.pool-1.1.3 \ + mysql.3 \ mysql \ - -h mysql.pool-1.1.4 \ + -h mysql.4 \ -p${mysql_user_password} \ -uapp2-user \ app2-db \ @@ -1005,32 +1005,32 @@ function test_custom_configuration () end __terminate_container \ - mysql.pool-1.1.3 \ + mysql.3 \ &> /dev/null __terminate_container \ - mysql.pool-1.1.4 \ + mysql.4 \ &> /dev/null __terminate_container \ - mysql.pool-1.1.5 \ + mysql.5 \ &> /dev/null end describe "Configure autostart" __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null docker run \ --detach \ - --name mysql.pool-1.1.1 \ + --name mysql.1 \ --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null if ! __is_container_ready \ - mysql.pool-1.1.1 \ + mysql.1 \ ${STARTUP_TIME} \ "/usr/sbin/mysqld " then @@ -1038,7 +1038,7 @@ function test_custom_configuration () fi it "Can disable mysqld-bootstrap." - docker logs mysql.pool-1.1.1 \ + docker logs mysql.1 \ | grep -qE 'INFO success: mysqld-bootstrap entered RUNNING state' assert equal \ @@ -1047,18 +1047,18 @@ function test_custom_configuration () end __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null docker run \ --detach \ - --name mysql.pool-1.1.1 \ + --name mysql.1 \ --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null if ! __is_container_ready \ - mysql.pool-1.1.1 \ + mysql.1 \ ${STARTUP_TIME} \ "/usr/bin/python /usr/bin/supervisord " \ "[[ -e /var/lib/mysql/ibdata1 ]] \ @@ -1068,7 +1068,7 @@ function test_custom_configuration () fi it "Can disable mysqld-wrapper." - docker top mysql.pool-1.1.1 \ + docker top mysql.1 \ | grep -qE '/usr/sbin/mysqld ' assert equal \ @@ -1077,7 +1077,7 @@ function test_custom_configuration () end __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null end end @@ -1094,7 +1094,7 @@ function test_healthcheck () local events_since_timestamp local health_status - trap "__terminate_container mysql.pool-1.1.1 &> /dev/null; \ + trap "__terminate_container mysql.1 &> /dev/null; \ __destroy; \ exit 1" \ INT TERM EXIT @@ -1102,12 +1102,12 @@ function test_healthcheck () describe "Healthcheck" describe "Default configuration" __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null docker run \ --detach \ - --name mysql.pool-1.1.1 \ + --name mysql.1 \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -1119,7 +1119,7 @@ function test_healthcheck () health_status="$( docker inspect \ --format='{{json .State.Health.Status}}' \ - mysql.pool-1.1.1 + mysql.1 )" assert __shpec_matcher_egrep \ @@ -1138,7 +1138,7 @@ function test_healthcheck () health_status="$( test/health_status \ - --container=mysql.pool-1.1.1 \ + --container=mysql.1 \ --since="${events_since_timestamp}" \ --timeout="${events_timeout}" \ --monochrome \ @@ -1153,12 +1153,12 @@ function test_healthcheck () it "Returns unhealthy on failure." # mysqld-wrapper failure docker exec -t \ - mysql.pool-1.1.1 \ + mysql.1 \ bash -c "mv \ /usr/sbin/mysqld \ /usr/sbin/mysqld2" \ && docker exec -t \ - mysql.pool-1.1.1 \ + mysql.1 \ bash -c "if [[ -n \$(pgrep -f '^/usr/sbin/mysqld ') ]]; then \ kill -9 -\$(ps axo pgid,command | grep -P '/usr/sbin/mysqld --pid-file=/var/run/mysqld/mysqld.pid$' | awk '{ print \$1; }') fi" @@ -1177,7 +1177,7 @@ function test_healthcheck () health_status="$( test/health_status \ - --container=mysql.pool-1.1.1 \ + --container=mysql.1 \ --since="$(( ${event_lag_seconds} + ${events_since_timestamp} ))" \ --timeout="${events_timeout}" \ --monochrome \ @@ -1190,7 +1190,7 @@ function test_healthcheck () end __terminate_container \ - mysql.pool-1.1.1 \ + mysql.1 \ &> /dev/null end end From 7b2dea8f61e4ed441d963bb47136f8b586cb0741 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Wed, 20 Feb 2019 00:15:11 +0000 Subject: [PATCH 21/98] [#215] Updates mysql-community-server package to 5.7.25-1. --- CHANGELOG.md | 1 + Dockerfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00bd84a..abd0424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). - Updates and restructures Dockerfile. - Updates container naming conventions and readability of `Makefile`. +- Updates `mysql-community-server` package to 5.7.25-1. - Fixes issue with unexpected published port in run templates when `DOCKER_PORT_MAP_TCP_3306` is set to an empty string or 0. - Adds placeholder replacement of `RELEASE_VERSION` docker argument to systemd service unit template. - Adds consideration for event lag into test cases for unhealthy health_status events. diff --git a/Dockerfile b/Dockerfile index 1c557df..4cd90c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ RUN { printf -- \ && yum -y install \ --setopt=tsflags=nodocs \ --disableplugin=fastestmirror \ - mysql-community-server-5.7.23-1.el7 \ + mysql-community-server-5.7.25-1.el7 \ psmisc-22.20-15.el7 \ && yum versionlock add \ mysql* \ From 633a4ce9dc02ba9631872489dd5a2def09f6296f Mon Sep 17 00:00:00 2001 From: James Deathe Date: Wed, 20 Feb 2019 00:34:45 +0000 Subject: [PATCH 22/98] #213: Excludes images directory from docker build context. --- .dockerignore | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index f1d19b8..6aa4b24 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ .git .gitignore dist +images test LICENSE README-short.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index abd0424..6b5e6bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Adds consideration for event lag into test cases for unhealthy health_status events. - Adds port incrementation to Makefile's run template for container names with an instance suffix. - Adds supervisord check to healthcheck script and removes unnecessary source script. +- Adds images directory `.dockerignore` to reduce size of build context. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. From 7af11128ddec2e8d9e3dd54a15104ef5b2881a64 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Wed, 20 Feb 2019 23:27:20 +0000 Subject: [PATCH 23/98] #205: Removes unused environment variables from Makefile and scmi configuration. --- CHANGELOG.md | 1 + environment.mk | 15 --------------- src/opt/scmi/environment.sh | 15 --------------- test/shpec/operation_shpec.sh | 1 - 4 files changed, 1 insertion(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b5e6bc..46d5a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Removes X-Fleet section from etcd register template unit-file. - Removes the unused group element from the default container name. - Removes the node element from the default container name. +- Removes unused environment variables from Makefile and scmi configuration. ### 2.1.1 - 2018-11-18 diff --git a/environment.mk b/environment.mk index 4b44376..38a7ddd 100644 --- a/environment.mk +++ b/environment.mk @@ -17,7 +17,6 @@ DOCKER_IMAGE_RELEASE_TAG_PATTERN := ^(1|2|centos-(6-1|7-mysql57-community-2))\.[ DOCKER_CONTAINER_OPTS ?= DOCKER_IMAGE_TAG ?= latest DOCKER_NAME ?= mysql.1 -DOCKER_PORT_MAP_TCP_22 ?= NULL DOCKER_PORT_MAP_TCP_3306 ?= 3306 DOCKER_RESTART_POLICY ?= always @@ -33,20 +32,6 @@ STARTUP_TIME ?= 10 # ------------------------------------------------------------------------------ # Application container configuration # ------------------------------------------------------------------------------ -SSH_AUTHORIZED_KEYS ?= -SSH_AUTOSTART_SSHD ?= false -SSH_AUTOSTART_SSHD_BOOTSTRAP ?= false -SSH_CHROOT_DIRECTORY ?= %h -SSH_INHERIT_ENVIRONMENT ?= false -SSH_SUDO ?= ALL=(ALL) ALL -SSH_USER ?= app-admin -SSH_USER_FORCE_SFTP ?= false -SSH_USER_HOME ?= /home/%u -SSH_USER_ID ?= 500:500 -SSH_USER_PASSWORD ?= -SSH_USER_PASSWORD_HASHED ?= false -SSH_USER_SHELL ?= /bin/bash -# ------------------------------------------------------------------------------ MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP ?= true MYSQL_AUTOSTART_MYSQLD_WRAPPER ?= true MYSQL_ROOT_PASSWORD ?= diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index 46d34e1..0c73f6f 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -16,7 +16,6 @@ readonly DOCKER_IMAGE_RELEASE_TAG_PATTERN='^(1|2|centos-(6-1|7-mysql57-community DOCKER_CONTAINER_OPTS="${DOCKER_CONTAINER_OPTS:-}" DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG:-latest}" DOCKER_NAME="${DOCKER_NAME:-mysql.1}" -DOCKER_PORT_MAP_TCP_22="${DOCKER_PORT_MAP_TCP_22:-}" DOCKER_PORT_MAP_TCP_3306="${DOCKER_PORT_MAP_TCP_3306:-3306}" DOCKER_RESTART_POLICY="${DOCKER_RESTART_POLICY:-always}" @@ -37,20 +36,6 @@ REGISTER_UPDATE_INTERVAL="${REGISTER_UPDATE_INTERVAL:-95}" # ------------------------------------------------------------------------------ # Application container configuration # ------------------------------------------------------------------------------ -SSH_AUTHORIZED_KEYS="${SSH_AUTHORIZED_KEYS:-}" -SSH_AUTOSTART_SSHD="${SSH_AUTOSTART_SSHD:-true}" -SSH_AUTOSTART_SSHD_BOOTSTRAP="${SSH_AUTOSTART_SSHD_BOOTSTRAP:-true}" -SSH_CHROOT_DIRECTORY="${SSH_CHROOT_DIRECTORY:-%h}" -SSH_INHERIT_ENVIRONMENT="${SSH_INHERIT_ENVIRONMENT:-false}" -SSH_SUDO="${SSH_SUDO:-ALL=(ALL) ALL}" -SSH_USER="${SSH_USER:-app-admin}" -SSH_USER_FORCE_SFTP="${SSH_USER_FORCE_SFTP:-false}" -SSH_USER_HOME="${SSH_USER_HOME:-/home/%u}" -SSH_USER_ID="${SSH_USER_ID:-500:500}" -SSH_USER_PASSWORD="${SSH_USER_PASSWORD:-}" -SSH_USER_PASSWORD_HASHED="${SSH_USER_PASSWORD_HASHED:-false}" -SSH_USER_SHELL="${SSH_USER_SHELL:-/bin/bash}" -# ------------------------------------------------------------------------------ MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP:-true}" MYSQL_AUTOSTART_MYSQLD_WRAPPER="${MYSQL_AUTOSTART_MYSQLD_WRAPPER:-true}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index a654203..ebcfdb8 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -3,7 +3,6 @@ readonly TEST_DIRECTORY="test" # These should ideally be a static value but hosts might be using this port so # need to allow for alternatives. -DOCKER_PORT_MAP_TCP_22="${DOCKER_PORT_MAP_TCP_22:-NULL}" DOCKER_PORT_MAP_TCP_3306="${DOCKER_PORT_MAP_TCP_3306:-3306}" function __destroy () From e3d79f26922a7a676d5461f9c70ede75f577b636 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 21 Feb 2019 21:55:32 +0000 Subject: [PATCH 24/98] #207: Adds docker-compose configuration example. --- .dockerignore | 3 +++ .env.example | 7 +++++++ .gitignore | 1 + CHANGELOG.md | 1 + docker-compose.yml | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+) create mode 100644 .env.example create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore index 6aa4b24..9920c41 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,11 @@ +.env +.env.example .git .gitignore dist images test +docker-compose.yml LICENSE README-short.txt *.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..242d41e --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +MYSQL_ROOT_PASSWORD= +MYSQL_ROOT_PASSWORD_HASHED=false +MYSQL_SUBNET=127.0.0.1 +MYSQL_USER= +MYSQL_USER_DATABASE= +MYSQL_USER_PASSWORD= +MYSQL_USER_PASSWORD_HASHED=false diff --git a/.gitignore b/.gitignore index 1db27ab..01faf4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +.env packages dist \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d5a95..9afb3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Adds port incrementation to Makefile's run template for container names with an instance suffix. - Adds supervisord check to healthcheck script and removes unnecessary source script. - Adds images directory `.dockerignore` to reduce size of build context. +- Adds docker-compose configuration example. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..337a96d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +# ------------------------------------------------------------------------------ +# Ref: https://docs.docker.com/compose/compose-file/ +# +# Setup: +# Copy .env.example to .env and modify values as required. +# docker-compose build +# docker-compose down +# +# Run Default example: +# docker-compose up -d +# +# Check service logs: +# docker-compose logs mysql +# +# Usage: +# docker-compose exec mysql mysqladmin --help +# docker-compose exec mysql mysqladmin -p status +# docker-compose exec mysql mysqladmin -p processlist +# docker-compose exec mysql mysqladmin -p create app-db +# docker-compose exec mysql mysql -p app-db +# +# Reset - bring down services + delete volume data: +# docker-compose down -v +# ------------------------------------------------------------------------------ +version: "3.0" +volumes: + data-mysql: + driver: "local" +services: + mysql: + build: + context: "." + dockerfile: "Dockerfile" + environment: + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" + MYSQL_ROOT_PASSWORD_HASHED: "${MYSQL_ROOT_PASSWORD_HASHED}" + MYSQL_SUBNET: "${MYSQL_SUBNET}" + MYSQL_USER: "${MYSQL_USER}" + MYSQL_USER_DATABASE: "${MYSQL_USER_DATABASE}" + MYSQL_USER_PASSWORD: "${MYSQL_USER_PASSWORD}" + MYSQL_USER_PASSWORD_HASHED: "${MYSQL_USER_PASSWORD_HASHED}" + image: "jdeathe/centos-ssh-mysql:latest" + ports: + - "3306:3306" + restart: "always" + volumes: + - "data-mysql:/var/lib/mysql" From 0a5292d6467c5157f5483d20d65809727ed9d49c Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 21 Feb 2019 22:48:22 +0000 Subject: [PATCH 25/98] #207: Removes unnecessary extra details. --- docker-compose.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 337a96d..a924ba5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,11 +13,8 @@ # docker-compose logs mysql # # Usage: -# docker-compose exec mysql mysqladmin --help -# docker-compose exec mysql mysqladmin -p status -# docker-compose exec mysql mysqladmin -p processlist -# docker-compose exec mysql mysqladmin -p create app-db -# docker-compose exec mysql mysql -p app-db +# docker-compose exec mysql mysqladmin -p --help +# docker-compose exec mysql mysql -p # # Reset - bring down services + delete volume data: # docker-compose down -v From b755fca04d2796d265f08f746779cffb80df9da5 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 21 Feb 2019 23:42:27 +0000 Subject: [PATCH 26/98] #209: Adds improvements to logging output. --- CHANGELOG.md | 1 + environment.mk | 2 +- src/etc/supervisord.d/mysqld-bootstrap.conf | 4 ++-- src/etc/supervisord.d/mysqld-wrapper.conf | 4 ++-- src/opt/scmi/environment.sh | 2 +- src/opt/scmi/service-unit.sh | 2 +- test/shpec/operation_shpec.sh | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9afb3a5..3b05ce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Adds supervisord check to healthcheck script and removes unnecessary source script. - Adds images directory `.dockerignore` to reduce size of build context. - Adds docker-compose configuration example. +- Adds improved logging output. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. diff --git a/environment.mk b/environment.mk index 38a7ddd..4e4073e 100644 --- a/environment.mk +++ b/environment.mk @@ -27,7 +27,7 @@ NO_CACHE ?= false DIST_PATH ?= ./dist # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME ?= 10 +STARTUP_TIME ?= 8 # ------------------------------------------------------------------------------ # Application container configuration diff --git a/src/etc/supervisord.d/mysqld-bootstrap.conf b/src/etc/supervisord.d/mysqld-bootstrap.conf index 6869f8d..83c4bb3 100644 --- a/src/etc/supervisord.d/mysqld-bootstrap.conf +++ b/src/etc/supervisord.d/mysqld-bootstrap.conf @@ -6,5 +6,5 @@ startsecs = 0 startretries = 0 autorestart = false redirect_stderr = true -stdout_logfile = /var/log/mysqld.log -stdout_events_enabled = true \ No newline at end of file +stdout_logfile = /dev/stdout +stdout_logfile_maxbytes = 0 diff --git a/src/etc/supervisord.d/mysqld-wrapper.conf b/src/etc/supervisord.d/mysqld-wrapper.conf index de27208..d978dc2 100644 --- a/src/etc/supervisord.d/mysqld-wrapper.conf +++ b/src/etc/supervisord.d/mysqld-wrapper.conf @@ -5,5 +5,5 @@ autostart = %(ENV_MYSQL_AUTOSTART_MYSQLD_WRAPPER)s startsecs = 0 autorestart = true redirect_stderr = true -stdout_logfile = /var/log/mysqld.log -stdout_events_enabled = true \ No newline at end of file +stdout_logfile = /dev/stdout +stdout_logfile_maxbytes = 0 diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index 0c73f6f..e82d643 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -26,7 +26,7 @@ NO_CACHE="${NO_CACHE:-false}" DIST_PATH="${DIST_PATH:-./dist}" # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME="${STARTUP_TIME:-10}" +STARTUP_TIME="${STARTUP_TIME:-8}" # ETCD register service settings REGISTER_ETCD_PARAMETERS="${REGISTER_ETCD_PARAMETERS:-}" diff --git a/src/opt/scmi/service-unit.sh b/src/opt/scmi/service-unit.sh index 24accaf..3ccaeb1 100644 --- a/src/opt/scmi/service-unit.sh +++ b/src/opt/scmi/service-unit.sh @@ -25,4 +25,4 @@ readonly SERVICE_UNIT_REGISTER_ENVIRONMENT_KEYS=" # ------------------------------------------------------------------------------ # Variables # ------------------------------------------------------------------------------ -SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-13}" +SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-11}" diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index ebcfdb8..af21176 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -1,4 +1,4 @@ -readonly STARTUP_TIME=10 +readonly STARTUP_TIME=8 readonly TEST_DIRECTORY="test" # These should ideally be a static value but hosts might be using this port so From f8e634ca883db70cb9ffa15467ea1347583aa552 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 22 Feb 2019 09:00:32 +0000 Subject: [PATCH 27/98] #209: Adds temporary work-around to password discovery in healthcheck script. --- environment.mk | 2 +- src/opt/scmi/environment.sh | 2 +- src/opt/scmi/service-unit.sh | 2 +- src/usr/sbin/mysqld-bootstrap | 2 +- test/shpec/operation_shpec.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/environment.mk b/environment.mk index 4e4073e..63b5c45 100644 --- a/environment.mk +++ b/environment.mk @@ -27,7 +27,7 @@ NO_CACHE ?= false DIST_PATH ?= ./dist # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME ?= 8 +STARTUP_TIME ?= 9 # ------------------------------------------------------------------------------ # Application container configuration diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index e82d643..96af2ab 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -26,7 +26,7 @@ NO_CACHE="${NO_CACHE:-false}" DIST_PATH="${DIST_PATH:-./dist}" # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME="${STARTUP_TIME:-8}" +STARTUP_TIME="${STARTUP_TIME:-9}" # ETCD register service settings REGISTER_ETCD_PARAMETERS="${REGISTER_ETCD_PARAMETERS:-}" diff --git a/src/opt/scmi/service-unit.sh b/src/opt/scmi/service-unit.sh index 3ccaeb1..4430084 100644 --- a/src/opt/scmi/service-unit.sh +++ b/src/opt/scmi/service-unit.sh @@ -25,4 +25,4 @@ readonly SERVICE_UNIT_REGISTER_ENVIRONMENT_KEYS=" # ------------------------------------------------------------------------------ # Variables # ------------------------------------------------------------------------------ -SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-11}" +SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-12}" diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 7f71587..fb71111 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -410,7 +410,7 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then { print T2 - T1; }" )" - cat \ + tee /var/log/mysqld.log \ <<-EOT ================================================================================ diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index af21176..65b1a3f 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -1,4 +1,4 @@ -readonly STARTUP_TIME=8 +readonly STARTUP_TIME=9 readonly TEST_DIRECTORY="test" # These should ideally be a static value but hosts might be using this port so From b850659466a6716caef9dc1d27061baa41094212 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 22 Feb 2019 09:01:34 +0000 Subject: [PATCH 28/98] #209: Adds reduction in expected startup time. --- environment.mk | 2 +- src/opt/scmi/environment.sh | 2 +- src/opt/scmi/service-unit.sh | 2 +- test/shpec/operation_shpec.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/environment.mk b/environment.mk index 63b5c45..4e4073e 100644 --- a/environment.mk +++ b/environment.mk @@ -27,7 +27,7 @@ NO_CACHE ?= false DIST_PATH ?= ./dist # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME ?= 9 +STARTUP_TIME ?= 8 # ------------------------------------------------------------------------------ # Application container configuration diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index 96af2ab..e82d643 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -26,7 +26,7 @@ NO_CACHE="${NO_CACHE:-false}" DIST_PATH="${DIST_PATH:-./dist}" # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME="${STARTUP_TIME:-9}" +STARTUP_TIME="${STARTUP_TIME:-8}" # ETCD register service settings REGISTER_ETCD_PARAMETERS="${REGISTER_ETCD_PARAMETERS:-}" diff --git a/src/opt/scmi/service-unit.sh b/src/opt/scmi/service-unit.sh index 4430084..3ccaeb1 100644 --- a/src/opt/scmi/service-unit.sh +++ b/src/opt/scmi/service-unit.sh @@ -25,4 +25,4 @@ readonly SERVICE_UNIT_REGISTER_ENVIRONMENT_KEYS=" # ------------------------------------------------------------------------------ # Variables # ------------------------------------------------------------------------------ -SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-12}" +SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-11}" diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 65b1a3f..ebcfdb8 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -1,4 +1,4 @@ -readonly STARTUP_TIME=9 +readonly STARTUP_TIME=10 readonly TEST_DIRECTORY="test" # These should ideally be a static value but hosts might be using this port so From 7d4100271d2a122590ac62d7bb9939bd93616bd6 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 7 Mar 2019 18:28:40 +0000 Subject: [PATCH 29/98] #225: Adds improved root password configuration. --- CHANGELOG.md | 1 + Dockerfile | 8 ++++- src/usr/bin/healthcheck | 40 +++++++++++----------- src/usr/sbin/mysqld-bootstrap | 63 ++++++++++++++++++++++++++++------- test/shpec/operation_shpec.sh | 18 +++------- 5 files changed, 84 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b05ce6..e74b454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Adds images directory `.dockerignore` to reduce size of build context. - Adds docker-compose configuration example. - Adds improved logging output. +- Adds improved root password configuration. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. diff --git a/Dockerfile b/Dockerfile index 4cd90c8..6e5ef53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,10 +19,16 @@ RUN { printf -- \ && yum -y install \ --setopt=tsflags=nodocs \ --disableplugin=fastestmirror \ + mysql-community-client-5.7.25-1.el7 \ + mysql-community-common-5.7.25-1.el7 \ + mysql-community-libs-5.7.25-1.el7 \ mysql-community-server-5.7.25-1.el7 \ psmisc-22.20-15.el7 \ + sshpass-1.06-2.el7 \ && yum versionlock add \ - mysql* \ + mysql-community-* \ + psmisc-* \ + sshpass \ && rm -rf /var/cache/yum/* \ && yum clean all diff --git a/src/usr/bin/healthcheck b/src/usr/bin/healthcheck index 07adff7..d4573a7 100755 --- a/src/usr/bin/healthcheck +++ b/src/usr/bin/healthcheck @@ -6,14 +6,26 @@ function __have_mysql_access () local db_password="${2:-}" local db_user="${1:-root}" - if mysql \ - --protocol=socket \ - --user="${db_user}" \ - --password="${db_password}" \ - -e "use ${db_name}" \ - 2> /dev/null + if [[ ${db_user} == root ]] then - return 0 + if mysql \ + --protocol=socket \ + --user="${db_user}" \ + -e "use ${db_name}" \ + 2> /dev/null + then + return 0 + fi + else + if mysql \ + --protocol=socket \ + --user="${db_user}" \ + --password="${db_password}" \ + -e "use ${db_name}" \ + 2> /dev/null + then + return 0 + fi fi return 1 @@ -278,20 +290,8 @@ function main () exit 0 fi - if [[ -n ${MYSQL_ROOT_PASSWORD} ]] \ - && [[ ${MYSQL_ROOT_PASSWORD} != "${redacted_value}" ]] - then - db_password="${MYSQL_ROOT_PASSWORD}" - else - db_password="$( - grep 'user : root@localhost' /var/log/mysqld.log \ - | sed -e 's~^.*,.*password : \([a-zA-Z0-9]*\).*$~\1~' - )" - fi - if ! __have_mysql_access \ - root \ - "${db_password}" + root then __print_message \ "root@localhost access failed." diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index fb71111..f01c2e9 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -63,13 +63,26 @@ function have_mysql_access () local DB_PASSWORD="${2:-}" local DB_NAME="${3:-mysql}" - if mysql \ - --protocol=socket \ - --user="${DB_USER}" \ - --password="${DB_PASSWORD}" \ - -e "use ${DB_NAME}" \ - 2> /dev/null; then - return 0 + if [[ ${DB_USER} == root ]] + then + if mysql \ + --protocol=socket \ + --user="${DB_USER}" \ + -e "use ${DB_NAME}" \ + 2> /dev/null; + then + return 0 + fi + else + if mysql \ + --protocol=socket \ + --user="${DB_USER}" \ + --password="${DB_PASSWORD}" \ + -e "use ${DB_NAME}" \ + 2> /dev/null; + then + return 0 + fi fi return 1 @@ -313,26 +326,53 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then PIDS[1]=${!} echo "Initialising MySQL." + ${MYSQLD} \ --pid-file="${PID_PATH}" \ --skip-networking \ --init-file=/tmp/mysql-init \ & + if ! command -v mysql_config_editor \ + &> /dev/null + then + touch \ + /root/.my.cnf + chown root:root \ + /root/.my.cnf + chmod 0600 \ + /root/.my.cnf + + tee /root/.my.cnf &> /dev/null \ + <<-EOT + [client] + password=${OPTS_MYSQL_ROOT_PASSWORD} + EOT + else + SSHPASS="${OPTS_MYSQL_ROOT_PASSWORD}" \ + sshpass \ + -e \ + mysql_config_editor \ + set \ + --skip-warn \ + --password + fi + # Wait for the MySQL database to be initialised by testing COUNTER=$(( 2 * OPTS_MYSQL_INIT_LIMIT )) until (( COUNTER == 0 )); do sleep 0.5 - - if have_mysql_access root "${OPTS_MYSQL_ROOT_PASSWORD}" mysql; then + if have_mysql_access root "-" mysql; then # Set the password if it was supplied pre-hashed. if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]]; then mysql \ - --password="${OPTS_MYSQL_ROOT_PASSWORD}" \ -e "UPDATE mysql.user SET authentication_string = '${OPTS_MYSQL_ROOT_PASSWORD}' WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES;" + + rm -f \ + /root/.{my,mylogin}.cnf fi break @@ -363,7 +403,6 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then else mysqladmin \ --user=root \ - --password="${OPTS_MYSQL_ROOT_PASSWORD}" \ shutdown fi @@ -390,7 +429,7 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then -- "user : %s@%s, password : %s" \ root \ localhost \ - "${OPTS_MYSQL_ROOT_PASSWORD}" + "${REDACTED_VALUE}" if [[ -n ${OPTS_MYSQL_USER} ]] \ && [[ -n ${OPTS_MYSQL_USER_PASSWORD} ]]; then diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index ebcfdb8..5a89242 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -281,17 +281,17 @@ function test_basic_operations () describe "Default initialisation" describe "Setup of root user" - it "Sets a 16 character password." + it "Redacts password in logs output." mysql_root_password="$( docker logs \ mysql.1 \ | grep 'user : root@localhost' \ - | sed -e 's~^.*,.*password : \([a-zA-Z0-9]*\).*$~\1~' + | sed -e 's~^.*,.*password : \([^ ,:]*\).*$~\1~' )" - assert __shpec_matcher_egrep \ + assert equal \ "${mysql_root_password}" \ - "[a-zA-Z0-9]{16}" + "********" end it "Limits access to localhost only." @@ -300,7 +300,6 @@ function test_basic_operations () mysql.1 \ mysql \ --batch \ - --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user;" @@ -328,7 +327,6 @@ function test_basic_operations () mysql.1 \ mysql \ --batch \ - --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SHOW DATABASES;" @@ -388,7 +386,6 @@ function test_basic_operations () -t \ mysql.1 \ mysql \ - -pmypasswd \ -uroot \ -e "USE my-db;" \ &> /dev/null @@ -440,7 +437,6 @@ function test_basic_operations () mysql.1 \ mysql \ --batch \ - --password=mypasswd \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" @@ -479,7 +475,6 @@ function test_basic_operations () mysql.1 \ mysql \ --batch \ - --password=mypasswd \ --skip-column-names \ --user=root \ -e "SHOW GRANTS FOR 'my-user'@'localhost';" @@ -540,8 +535,7 @@ function test_custom_configuration () --name mysql.2 \ --network-alias mysql.2 \ --network ${private_network_1} \ - --env "MYSQL_ROOT_PASSWORD=${mysql_root_password_hash}" \ - --env "MYSQL_ROOT_PASSWORD_HASHED=true" \ + --env "MYSQL_ROOT_PASSWORD=${mysql_root_password}" \ --env "MYSQL_SUBNET=172.172.40.0/255.255.255.0" \ --env "MYSQL_USER=app-user" \ --env "MYSQL_USER_PASSWORD=${mysql_user_password}" \ @@ -629,7 +623,6 @@ function test_custom_configuration () mysql.2 \ mysql \ --batch \ - --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" @@ -725,7 +718,6 @@ function test_custom_configuration () mysql.2 \ mysql \ --batch \ - --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" From 0bd42b4bfe3ec3d46059ef6a9c7d9bb6dffc19f6 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 7 Mar 2019 19:02:46 +0000 Subject: [PATCH 30/98] #225: Switch to file based method of passing a password to mysql_config_editor. --- src/usr/sbin/mysqld-bootstrap | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index f01c2e9..c7aaaaa 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -349,13 +349,27 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then password=${OPTS_MYSQL_ROOT_PASSWORD} EOT else - SSHPASS="${OPTS_MYSQL_ROOT_PASSWORD}" \ + touch \ + /root/.mypasswd.cnf + chown root:root \ + /root/.mypasswd.cnf + chmod 0600 \ + /root/.mypasswd.cnf + + printf -- \ + '%s\n' \ + "${OPTS_MYSQL_ROOT_PASSWORD}" \ + > /root/.mypasswd.cnf + sshpass \ - -e \ + -f /root/.mypasswd.cnf \ mysql_config_editor \ set \ --skip-warn \ --password + + rm -f \ + /root/.mypasswd.cnf fi # Wait for the MySQL database to be initialised by testing From 24243915e85eff7f69f8fbc6c60275544bd4ea57 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 8 Mar 2019 19:28:51 +0000 Subject: [PATCH 31/98] #225: Adds revised method of populating the MySQL root user's password. --- src/usr/sbin/mysqld-bootstrap | 90 +++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index c7aaaaa..1cae3d5 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -336,40 +336,85 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then if ! command -v mysql_config_editor \ &> /dev/null then - touch \ - /root/.my.cnf - chown root:root \ - /root/.my.cnf - chmod 0600 \ - /root/.my.cnf - tee /root/.my.cnf &> /dev/null \ <<-EOT [client] - password=${OPTS_MYSQL_ROOT_PASSWORD} + password={{MYSQL_ROOT_PASSWORD}} EOT - else - touch \ - /root/.mypasswd.cnf + chown root:root \ - /root/.mypasswd.cnf + /root/.my.cnf chmod 0600 \ - /root/.mypasswd.cnf + /root/.my.cnf - printf -- \ - '%s\n' \ - "${OPTS_MYSQL_ROOT_PASSWORD}" \ - > /root/.mypasswd.cnf + sed -i \ + -e "s~{{MYSQL_ROOT_PASSWORD}}~${OPTS_MYSQL_ROOT_PASSWORD}~g" \ + /root/.my.cnf + else + fifo_path="$( + mktemp -d + )" + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + '%s %s %s\n' \ + "ERROR: Failed to create directory" \ + "${fifo_path}" + "- aborting." + exit 1 + fi + + trap \ + "rm -rf \"${fifo_path}\"" \ + INT TERM EXIT + + mkfifo \ + -m 0600 \ + "${fifo_path}/mypasswd" + + exec \ + {mypasswd_fd}<>"${fifo_path}/mypasswd" + + trap \ + "exec {mypasswd_fd}>&-" \ + INT TERM EXIT + + rm -rf \ + "${fifo_path}" + + # Send into background waiting to read the password sshpass \ - -f /root/.mypasswd.cnf \ + -d ${mypasswd_fd} \ mysql_config_editor \ set \ --skip-warn \ - --password + --password \ + & + PIDS[2]=${!} - rm -f \ - /root/.mypasswd.cnf + trap \ + "exec {mypasswd_fd}>&- ; \ + kill -9 ${PIDS[2]};" \ + INT TERM EXIT + + printf -- \ + '%s\n' \ + "${OPTS_MYSQL_ROOT_PASSWORD}" \ + >&${mypasswd_fd} \ + & + PIDS[3]=${!} + + trap \ + "exec {mypasswd_fd}>&- ; \ + [[ -n ${PIDS[2]} ]] && kill -9 ${PIDS[2]}; \ + [[ -n ${PIDS[3]} ]] && kill -9 ${PIDS[3]};" \ + INT TERM EXIT + + [[ -n ${PIDS[2]} ]] && wait ${PIDS[2]} + + exec \ + {mypasswd_fd}>&- fi # Wait for the MySQL database to be initialised by testing @@ -475,6 +520,9 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then ${TIMER_TOTAL} EOT + + trap - \ + INT TERM EXIT fi # Release lock file From 512db10f156543f7004a4082b91fdf3e6f9ae45f Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 8 Mar 2019 19:35:48 +0000 Subject: [PATCH 32/98] #255: Abort on error to set MySQL root user's password. --- src/usr/sbin/mysqld-bootstrap | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 1cae3d5..477d018 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -411,7 +411,17 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then [[ -n ${PIDS[3]} ]] && kill -9 ${PIDS[3]};" \ INT TERM EXIT - [[ -n ${PIDS[2]} ]] && wait ${PIDS[2]} + if [[ -n ${PIDS[2]} ]] + then + wait ${PIDS[2]} + fi + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + "ERROR: Unable to set MySQL root password - aborting.\n" + exit 1 + fi exec \ {mypasswd_fd}>&- From 4c5ff3ce5ff222e74e6185bbb7cefc80f99993bf Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 9 Mar 2019 00:30:30 +0000 Subject: [PATCH 33/98] #225: Prefer to store MySQL root password encrypted. --- src/usr/sbin/mysqld-bootstrap | 36 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 477d018..8fd5783 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -333,24 +333,11 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then --init-file=/tmp/mysql-init \ & - if ! command -v mysql_config_editor \ - &> /dev/null + # Set MySQL root user password + # - Prefer to store encrypted via mysql_config_editor. + # - Fallback to store as plaintext in secured file. + if command -v mysql_config_editor &> /dev/null then - tee /root/.my.cnf &> /dev/null \ - <<-EOT - [client] - password={{MYSQL_ROOT_PASSWORD}} - EOT - - chown root:root \ - /root/.my.cnf - chmod 0600 \ - /root/.my.cnf - - sed -i \ - -e "s~{{MYSQL_ROOT_PASSWORD}}~${OPTS_MYSQL_ROOT_PASSWORD}~g" \ - /root/.my.cnf - else fifo_path="$( mktemp -d )" @@ -425,6 +412,21 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then exec \ {mypasswd_fd}>&- + else + tee /root/.my.cnf &> /dev/null \ + <<-EOT + [client] + password={{MYSQL_ROOT_PASSWORD}} + EOT + + chown root:root \ + /root/.my.cnf + chmod 0600 \ + /root/.my.cnf + + sed -i \ + -e "s~{{MYSQL_ROOT_PASSWORD}}~${OPTS_MYSQL_ROOT_PASSWORD}~g" \ + /root/.my.cnf fi # Wait for the MySQL database to be initialised by testing From 40b8d6270fa246e0c1a4b4cf08edd52ae11a10a8 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 9 Mar 2019 00:43:02 +0000 Subject: [PATCH 34/98] #225: Removes insecure method of running mysql with root password in tests. --- test/shpec/operation_shpec.sh | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 5a89242..8b1859a 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -810,12 +810,24 @@ function test_custom_configuration () describe "Root password" it "Creates a subnet restricted user." + # Set MySQL root password + if docker exec mysql.2 bash command -v mysql_config_editor &> /dev/null + then + docker exec -i mysql.2 sshpass mysql_config_editor set --skip-warn --password \ + < ${PWD}/${TEST_DIRECTORY}/fixture/secrets/mysql_root_password + else + docker exec mysql.2 bash -c "printf -- '[client]\npassword={{MYSQL_ROOT_PASSWORD}}\n' > /root/.my.cnf" + docker exec mysql.2 chown 0:0 /root/.my.cnf + docker exec mysql.2 chmod 0600 /root/.my.cnf + docker exec -i mysql.2 bash -c "IFS= read -r mysql_root_password; sed -i -e \"s~{{MYSQL_ROOT_PASSWORD}}~\${mysql_root_password}~g\" /root/.my.cnf;" \ + < ${PWD}/${TEST_DIRECTORY}/fixture/secrets/mysql_root_password + fi + select_users="$( docker exec \ mysql.2 \ mysql \ --batch \ - --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" @@ -931,12 +943,24 @@ function test_custom_configuration () describe "User creation" it "Creates an unrestricted user." + # Set MySQL root password + if docker exec mysql.4 bash command -v mysql_config_editor &> /dev/null + then + docker exec -i mysql.4 sshpass mysql_config_editor set --skip-warn --password \ + < ${PWD}/${TEST_DIRECTORY}/fixture/secrets/mysql_root_password + else + docker exec mysql.4 bash -c "printf -- '[client]\npassword={{MYSQL_ROOT_PASSWORD}}\n' > /root/.my.cnf" + docker exec mysql.4 chown 0:0 /root/.my.cnf + docker exec mysql.4 chmod 0600 /root/.my.cnf + docker exec -i mysql.4 bash -c "IFS= read -r mysql_root_password; sed -i -e \"s~{{MYSQL_ROOT_PASSWORD}}~\${mysql_root_password}~g\" /root/.my.cnf;" \ + < ${PWD}/${TEST_DIRECTORY}/fixture/secrets/mysql_root_password + fi + select_users="$( docker exec \ mysql.4 \ mysql \ --batch \ - --password="${mysql_root_password}" \ --skip-column-names \ --user=root \ -e "SELECT User, Host from mysql.user ORDER BY User ASC;" From 8f0807b8ae53b237bc6ac32d57e268b8d14c0af7 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 9 Mar 2019 17:23:49 +0000 Subject: [PATCH 35/98] #225: Updates README with example usage without exposed password. --- README.md | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fa12ddb..8ca753b 100644 --- a/README.md +++ b/README.md @@ -66,32 +66,38 @@ $ docker inspect \ mysql.1 ``` -To access the MySQL SQL shell run the following: +To access the interactive MySQL SQL shell run the following: ``` -$ docker exec -it mysql.1 mysql -p -u root +$ docker exec -it mysql.1 mysql ``` +### Sakila Example To import the Sakila example database from the [MySQL Documentation](https://dev.mysql.com/doc/index-other.html) and view the first 2 records from the film table. -``` -$ export MYSQL_ROOT_PASSWORD={your-password} +#### Import Schema -$ docker exec -i mysql.1 \ - mysql -u root -p${MYSQL_ROOT_PASSWORD} \ +``` +$ docker exec -i mysql.1 mysql \ <<< $(curl -sSL http://downloads.mysql.com/docs/sakila-db.tar.gz \ | tar -xzO - "sakila-db/sakila-schema.sql" \ | sed -e '/^CREATE TABLE film_text/,/ENGINE=InnoDB / s/InnoDB/MyISAM/' ) +``` + +#### Import Data -$ docker exec -i mysql.1 \ - mysql -u root -p${MYSQL_ROOT_PASSWORD} \ +``` +$ docker exec -i mysql.1 mysql \ <<< $(curl -sSL http://downloads.mysql.com/docs/sakila-db.tar.gz \ | tar -xzO - "sakila-db/sakila-data.sql" ) +``` -$ docker exec mysql.1 \ - mysql -u root -p${MYSQL_ROOT_PASSWORD} \ +#### Select 2 Records from the film Table + +``` +$ docker exec mysql.1 mysql \ -e "SELECT * FROM sakila.film LIMIT 2 \G" ``` @@ -303,7 +309,7 @@ If set to a valid container file path the value will be read from the file - thi ##### MYSQL_ROOT_PASSWORD_HASHED -To indicate `MYSQL_ROOT_PASSWORD` is a pre-hashed value instead of the default plain-text type set `MYSQL_ROOT_PASSWORD_HASHED` to `true`. +To indicate `MYSQL_ROOT_PASSWORD` is a pre-hashed value instead of the default plain-text type set `MYSQL_ROOT_PASSWORD_HASHED` to `true`. When using this option the MySQL root user password will not be stored in the running container so you will need to either add it as a manual step or you will need to supply the password when running `mysql` or `mysqladmin`. ``` ... @@ -315,7 +321,7 @@ To indicate `MYSQL_ROOT_PASSWORD` is a pre-hashed value instead of the default p *Note:* To generate a pre-hashed password you could use the following MySQL command. ``` -$ mysql -u root -p{mysql_root_password} \ +$ docker exec mysql.1 mysql -NB \ -e "SELECT PASSWORD('{mysql_user_password}');" ``` From 1c940bc9431bc158d10f5b4cf4402f23790da868 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 9 Mar 2019 18:20:51 +0000 Subject: [PATCH 36/98] #225: Adds check for sshpass and reverts unintended line changes. --- src/usr/sbin/mysqld-bootstrap | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 8fd5783..d39b788 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -326,7 +326,6 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then PIDS[1]=${!} echo "Initialising MySQL." - ${MYSQLD} \ --pid-file="${PID_PATH}" \ --skip-networking \ @@ -336,7 +335,8 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then # Set MySQL root user password # - Prefer to store encrypted via mysql_config_editor. # - Fallback to store as plaintext in secured file. - if command -v mysql_config_editor &> /dev/null + if command -v mysql_config_editor &> /dev/null \ + && command -v sshpass &> /dev/null then fifo_path="$( mktemp -d @@ -435,6 +435,7 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then )) until (( COUNTER == 0 )); do sleep 0.5 + if have_mysql_access root "-" mysql; then # Set the password if it was supplied pre-hashed. From 4244ed9ed1b001440c39a124f82ad5ba372afba7 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 9 Mar 2019 22:37:34 +0000 Subject: [PATCH 37/98] #231: Updates source image to 2.5.1. --- CHANGELOG.md | 6 +++- Dockerfile | 12 +++---- .../system/centos-ssh-mysql.register@.service | 17 +++++---- .../systemd/system/centos-ssh-mysql@.service | 36 ++++++++----------- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e74b454..98f9704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,13 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. ### 2.2.0 - Unreleased -- Updates source image to [2.5.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.0). +- Updates source image to [2.5.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.1). - Updates and restructures Dockerfile. - Updates container naming conventions and readability of `Makefile`. - Updates `mysql-community-server` package to 5.7.25-1. +- Updates Dockerfile with combined ADD to reduce layer count in final image. - Fixes issue with unexpected published port in run templates when `DOCKER_PORT_MAP_TCP_3306` is set to an empty string or 0. +- Fixes binary paths in systemd unit files for compatibility with both EL and Ubuntu hosts. - Adds placeholder replacement of `RELEASE_VERSION` docker argument to systemd service unit template. - Adds consideration for event lag into test cases for unhealthy health_status events. - Adds port incrementation to Makefile's run template for container names with an instance suffix. @@ -21,6 +23,8 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Adds docker-compose configuration example. - Adds improved logging output. - Adds improved root password configuration. +- Adds improvement to pull logic in systemd unit install template. +- Adds `SSH_AUTOSTART_SUPERVISOR_STDOUT` with a value "false", disabling startup of `supervisor_stdout`. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. diff --git a/Dockerfile b/Dockerfile index 6e5ef53..dfbcc0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM jdeathe/centos-ssh:2.5.0 +FROM jdeathe/centos-ssh:2.5.1 ARG RELEASE_VERSION="2.1.1" @@ -35,12 +35,7 @@ RUN { printf -- \ # ------------------------------------------------------------------------------ # Copy files into place # ------------------------------------------------------------------------------ -ADD src/etc \ - /etc/ -ADD src/opt/scmi \ - /opt/scmi/ -ADD src/usr \ - /usr/ +ADD src / # ------------------------------------------------------------------------------ # Provisioning @@ -72,7 +67,8 @@ ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="true" \ MYSQL_USER_PASSWORD="" \ MYSQL_USER_PASSWORD_HASHED="false" \ SSH_AUTOSTART_SSHD="false" \ - SSH_AUTOSTART_SSHD_BOOTSTRAP="false" + SSH_AUTOSTART_SSHD_BOOTSTRAP="false" \ + SSH_AUTOSTART_SUPERVISOR_STDOUT="false" # ------------------------------------------------------------------------------ # Set image metadata diff --git a/src/etc/systemd/system/centos-ssh-mysql.register@.service b/src/etc/systemd/system/centos-ssh-mysql.register@.service index 92cde37..65a4e79 100644 --- a/src/etc/systemd/system/centos-ssh-mysql.register@.service +++ b/src/etc/systemd/system/centos-ssh-mysql.register@.service @@ -35,6 +35,7 @@ # # To uninstall: # sudo systemctl disable -f {service-unit-instance-name} +# sudo systemctl daemon-reload # sudo rm /etc/systemd/system/{service-unit-template-name} # sudo systemctl daemon-reload # ------------------------------------------------------------------------------ @@ -86,7 +87,7 @@ ExecStart=/bin/bash -c \ \"$(/usr/bin/docker port \ {{SERVICE_UNIT_NAME}}.%i \ 3306 \ - | /usr/bin/sed 's~^[0-9.]*:~~' \ + | /bin/sed 's~^[0-9.]*:~~' \ )\" \ --ttl ${REGISTER_TTL} 2> /dev/null; \ fi; \ @@ -111,15 +112,17 @@ ExecStart=/bin/bash -c \ ${REGISTER_KEY_ROOT}/ports/tcp/3306 \ &> /dev/null; \ then \ - echo set; \ + printf -- 'set +'; \ else \ - echo update; \ + printf -- 'update +'; \ fi) \ ${REGISTER_KEY_ROOT}/ports/tcp/3306 \ \"$(/usr/bin/docker port \ {{SERVICE_UNIT_NAME}}.%i \ 3306 \ - | /usr/bin/sed 's~^[0-9.]*:~~' \ + | /bin/sed 's~^[0-9.]*:~~' \ )\" \ --ttl ${REGISTER_TTL}; \ fi; \ @@ -131,9 +134,11 @@ ExecStart=/bin/bash -c \ ${REGISTER_KEY_ROOT}/hostname \ &> /dev/null; \ then \ - echo set; \ + printf -- 'set +'; \ else \ - echo update; \ + printf -- 'update +'; \ fi) \ ${REGISTER_KEY_ROOT}/hostname \ %H \ diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index 46532c9..5413ddb 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -26,7 +26,8 @@ # sudo systemctl enable -f {service-unit-instance-name} # # Start using: -# sudo systemctl [start|stop|restart|kill|status] {service-unit-instance-name} +# sudo systemctl [start|stop|restart|kill|status] \ +# {service-unit-instance-name} # # Debugging: # sudo systemctl status {service-unit-instance-name} @@ -34,6 +35,7 @@ # # To uninstall: # sudo systemctl disable -f {service-unit-instance-name} +# sudo systemctl daemon-reload # sudo systemctl stop {service-unit-instance-name} # sudo rm /etc/systemd/system/{service-unit-template-name} # sudo docker rm -f {service-unit-long-name} @@ -66,20 +68,12 @@ Environment="MYSQL_USER_PASSWORD_HASHED=false" # Initialisation: Load image from local storage if available, otherwise pull. ExecStartPre=/bin/bash -c \ - "if [[ -z $( \ - if [[ -n $(/usr/bin/docker images -q \ - ${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} \ - ) ]]; \ - then \ - echo $(/usr/bin/docker images -q \ - ${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} \ - ); \ - else \ - echo $(/usr/bin/docker images -q \ - docker.io/${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} \ - ); \ - fi; \ - ) ]]; \ + "if [[ -z \"$(/usr/bin/docker images -q \ + ${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} \ + )\" ]] \ + && [[ -z \"$(/usr/bin/docker images -q \ + docker.io/${DOCKER_USER}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} \ + )\" ]]; \ then \ if [[ -f ${DOCKER_IMAGE_PACKAGE_PATH}/${DOCKER_USER}/${DOCKER_IMAGE_NAME}.${DOCKER_IMAGE_TAG}.tar.xz ]]; \ then \ @@ -138,27 +132,27 @@ ExecStart=/bin/bash -c \ --env \"MYSQL_USER_PASSWORD_HASHED=${MYSQL_USER_PASSWORD_HASHED}\" \ $(if [[ ${DOCKER_PORT_MAP_TCP_3306} != NULL ]]; \ then \ - if /usr/bin/grep -qE \ + if /bin/grep -qE \ '^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:)?[1-9][0-9]*$' \ <<< \"${DOCKER_PORT_MAP_TCP_3306}\"; \ - && /usr/bin/grep -qE \ + && /bin/grep -qE \ '^.+\.[0-9]+(\.[0-9]+)?$' \ - <<< "${DOCKER_NAME}" + <<< %p.%i; \ then \ printf -- '--publish %%s%%s:3306' \ $(\ - /usr/bin/grep -o \ + /bin/grep -o \ '^[0-9\.]*:' \ <<< \"${DOCKER_PORT_MAP_TCP_3306}\" \ ) \ $(( \ $(\ - /usr/bin/grep -oE \ + /bin/grep -oE \ '[0-9]+$' \ <<< \"${DOCKER_PORT_MAP_TCP_3306}\" \ ) \ + $(\ - /usr/bin/grep -oE \ + /bin/grep -oE \ '^[0-9]+' \ <<< %i \ ) \ From 2f6199503b0177bf46c17e07adec28bb1de4dfb9 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 9 Mar 2019 23:42:20 +0000 Subject: [PATCH 38/98] #231: Fixes issue with missing newline character. --- .../system/centos-ssh-mysql.register@.service | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/etc/systemd/system/centos-ssh-mysql.register@.service b/src/etc/systemd/system/centos-ssh-mysql.register@.service index 65a4e79..9b71319 100644 --- a/src/etc/systemd/system/centos-ssh-mysql.register@.service +++ b/src/etc/systemd/system/centos-ssh-mysql.register@.service @@ -112,11 +112,9 @@ ExecStart=/bin/bash -c \ ${REGISTER_KEY_ROOT}/ports/tcp/3306 \ &> /dev/null; \ then \ - printf -- 'set -'; \ + printf -- 'set\n'; \ else \ - printf -- 'update -'; \ + printf -- 'update\n'; \ fi) \ ${REGISTER_KEY_ROOT}/ports/tcp/3306 \ \"$(/usr/bin/docker port \ @@ -134,11 +132,9 @@ ExecStart=/bin/bash -c \ ${REGISTER_KEY_ROOT}/hostname \ &> /dev/null; \ then \ - printf -- 'set -'; \ + printf -- 'set\n'; \ else \ - printf -- 'update -'; \ + printf -- 'update\n'; \ fi) \ ${REGISTER_KEY_ROOT}/hostname \ %H \ From c254c3c369936d5143677082dc9ef949ff2d2f2a Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 10 Mar 2019 17:13:04 +0000 Subject: [PATCH 39/98] #211: Adds updated healthcheck script. --- src/usr/bin/healthcheck | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/usr/bin/healthcheck b/src/usr/bin/healthcheck index d4573a7..5a1308e 100755 --- a/src/usr/bin/healthcheck +++ b/src/usr/bin/healthcheck @@ -2,16 +2,16 @@ function __have_mysql_access () { - local db_name="${3:-mysql}" - local db_password="${2:-}" - local db_user="${1:-root}" + local -r database="${3:-mysql}" + local -r password="${2:-}" + local -r user="${1:-root}" - if [[ ${db_user} == root ]] + if [[ ${user} == root ]] then if mysql \ --protocol=socket \ - --user="${db_user}" \ - -e "use ${db_name}" \ + --user="${user}" \ + -e "use ${database}" \ 2> /dev/null then return 0 @@ -19,9 +19,9 @@ function __have_mysql_access () else if mysql \ --protocol=socket \ - --user="${db_user}" \ - --password="${db_password}" \ - -e "use ${db_name}" \ + --user="${user}" \ + --password="${password}" \ + -e "use ${database}" \ 2> /dev/null then return 0 @@ -203,7 +203,6 @@ function main () local QUIET="false" local -i interval=10 - local db_password # Trap and record the exit status trap "__record_exit_status \${?}" \ @@ -290,8 +289,7 @@ function main () exit 0 fi - if ! __have_mysql_access \ - root + if ! __have_mysql_access then __print_message \ "root@localhost access failed." From d620512cfa6c6b1d63511cd542163587ae1a14d2 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 10 Mar 2019 17:14:58 +0000 Subject: [PATCH 40/98] #211: Adds improved wrapper script. --- src/usr/sbin/mysqld-wrapper | 72 ++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index 5adfdc2..57c6fc3 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -1,18 +1,66 @@ #!/usr/bin/env bash +set -e + source /etc/mysqld-bootstrap.conf -readonly MYSQLD=/usr/sbin/mysqld -readonly NICE=/bin/nice -readonly NICENESS="${MYSQL_NICENESS:-10}" -readonly PID_PATH=/var/run/mysqld/mysqld.pid +function __is_valid_mysql_autostart_mysqld_bootstrap () +{ + local -r boolean_value='^(true|false)$' + local -r value="${1}" + + if [[ ${value} =~ ${boolean_value} ]] + then + return 0 + fi + + return 1 +} + +function __get_mysql_autostart_mysqld_bootstrap () +{ + local -r default_value="${1:-true}" + + local value="${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP}" + + if ! __is_valid_mysql_autostart_mysqld_bootstrap "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + +function main () +{ + local -r autostart_bootstrap="$( + __get_mysql_autostart_mysqld_bootstrap + )" + local -r bin="/usr/sbin/mysqld" + local -r lock_file="/var/lock/subsys/mysqld-bootstrap" + local -r nice="/bin/nice" + local -r niceness="10" + local -r options="--pid-file=/var/run/mysqld/mysqld.pid" + + if [[ ${autostart_bootstrap} == false ]] + then + # block. + sleep infinity + fi + + while true + do + sleep 0.1 + if [[ ! -e ${lock_file} ]] + then + break + fi + done -while true; do - sleep 0.1 - [[ -e /var/lock/subsys/mysqld-bootstrap ]] || break -done + exec ${nice} \ + -n ${niceness} \ + ${bin} \ + ${options} +} -exec ${NICE} \ - -n ${NICENESS} \ - ${MYSQLD} \ - --pid-file=${PID_PATH} +main "${@}" \ No newline at end of file From 0809c0e65558608943aecda29b375996dce15d4d Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 10 Mar 2019 17:16:56 +0000 Subject: [PATCH 41/98] #211: Adds WIP refactoring of bootstrap script. --- CHANGELOG.md | 1 + src/usr/sbin/mysqld-bootstrap | 539 +++++++++++++++++++--------------- 2 files changed, 298 insertions(+), 242 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f9704..62a21f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Adds improved root password configuration. - Adds improvement to pull logic in systemd unit install template. - Adds `SSH_AUTOSTART_SUPERVISOR_STDOUT` with a value "false", disabling startup of `supervisor_stdout`. +- Adds improved `healtchcheck`, `sshd-bootstrap` and `sshd-wrapper` scripts. - Removes use of `/etc/services-config` paths. - Removes code from configuration file `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index d39b788..c8c7aa4 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Create lock file +# Create lock touch /var/lock/subsys/mysqld-bootstrap TIMER_START="$( @@ -9,66 +9,177 @@ TIMER_START="$( source /etc/mysqld-bootstrap.conf -function get_mysql_user_host () +# Set MySQL root user password +# - Prefer to store encrypted via mysql_config_editor. +# - Fallback to store as plaintext in secured file. +function __configure_mysql_root_password () { - local CLIENT_SUBNET="${1:-127.0.0.1}" - local VALUE + local -r password="${1:-}" - case "${CLIENT_SUBNET}" in + local -a pids + + if [[ -z ${password} ]] + then + return 1 + fi + + if command -v mysql_config_editor &> /dev/null \ + && command -v sshpass &> /dev/null + then + fifo_path="$( + mktemp -d + )" + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + '%s %s %s\n' \ + "ERROR: Failed to create directory" \ + "${fifo_path}" + "- aborting." + exit 1 + fi + + trap \ + "rm -rf \"${fifo_path}\"" \ + INT TERM EXIT + + mkfifo \ + -m 0600 \ + "${fifo_path}/mypasswd" + + exec \ + {mypasswd_fd}<>"${fifo_path}/mypasswd" + + trap \ + "exec {mypasswd_fd}>&-" \ + INT TERM EXIT + + rm -rf \ + "${fifo_path}" + + # Send into background waiting to read the password + sshpass \ + -d ${mypasswd_fd} \ + mysql_config_editor \ + set \ + --skip-warn \ + --password \ + & + pids[1]="${!}" + + trap \ + "exec {mypasswd_fd}>&- ; \ + [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]};" \ + INT TERM EXIT + + printf -- \ + '%s\n' \ + "${password}" \ + >&${mypasswd_fd} \ + & + pids[2]="${!}" + + trap \ + "exec {mypasswd_fd}>&- ; \ + [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]}; \ + [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]};" \ + INT TERM EXIT + + if [[ ${pids[1]} -gt 0 ]] + then + wait ${pids[1]} + fi + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + "ERROR: Unable to set MySQL root password - aborting.\n" + exit 1 + fi + + exec \ + {mypasswd_fd}>&- + else + tee /root/.my.cnf &> /dev/null \ + <<-EOT + [client] + password={{MYSQL_ROOT_PASSWORD}} + EOT + + chown root:root \ + /root/.my.cnf + chmod 0600 \ + /root/.my.cnf + + sed -i \ + -e "s~{{MYSQL_ROOT_PASSWORD}}~${password}~g" \ + /root/.my.cnf + fi + + trap - \ + INT TERM EXIT +} + +function __get_mysql_user_host () +{ + local -r client_subnet="${1:-127.0.0.1}" + + local value + + case "${client_subnet}" in 0.0.0.0|0.0.0.0/0.0.0.0) # Connect from any network - VALUE="%" + value="%" ;; 127.0.0.1) # Internal connection - VALUE="localhost" + value="localhost" ;; *) # User defined host / subnet - VALUE="${CLIENT_SUBNET}" + value="${client_subnet}" ;; esac - printf -- "%s" "${VALUE}" + printf -- '%s' "${value}" } -function get_option () +function __get_option () { - local VALUE=$( - /usr/bin/my_print_defaults "${1}" | \ - sed -n "s/^--${2}=//p" | \ - tail -n 1 - ) + local -r value="$( + /usr/bin/my_print_defaults "${1}" \ + | sed -n "s/^--${2}=//p" \ + | tail -n 1 + )" - printf -- "%s" "${VALUE:-$3}" + printf -- '%s' "${value:-"${3}"}" } -function get_password () +function __get_password () { - local LENGTH="${1:-16}" - local PASSWORD="$( + local -r length="${1:-16}" + local -r password="$( head -n 4096 /dev/urandom \ | tr -cd '[:alnum:]' \ - | cut -c1-"${LENGTH}" + | cut -c1-"${length}" )" - printf -- "%s" "${PASSWORD}" - - return 0 + printf -- '%s' "${password}" } -function have_mysql_access () +function __have_mysql_access () { - local DB_USER="${1:-root}" - local DB_PASSWORD="${2:-}" - local DB_NAME="${3:-mysql}" + local -r database="${3:-mysql}" + local -r password="${2:-}" + local -r user="${1:-root}" - if [[ ${DB_USER} == root ]] + if [[ ${user} == root ]] then if mysql \ --protocol=socket \ - --user="${DB_USER}" \ - -e "use ${DB_NAME}" \ + --user="${user}" \ + -e "use ${database}" \ 2> /dev/null; then return 0 @@ -76,9 +187,9 @@ function have_mysql_access () else if mysql \ --protocol=socket \ - --user="${DB_USER}" \ - --password="${DB_PASSWORD}" \ - -e "use ${DB_NAME}" \ + --user="${user}" \ + --password="${password}" \ + -e "use ${database}" \ 2> /dev/null; then return 0 @@ -88,32 +199,115 @@ function have_mysql_access () return 1 } -function is_mysql_data_directory_populated () +function __is_mysql_datadir_populated () { - local MYSQL_DATA_DIRECTORY="${1:-/var/lib/mysql}" + local -r directory="${1:-"$( + __get_option \ + mysqld \ + datadir \ + "/var/lib/mysql" + )"}" # Test for the InnoDB shared tablespace - if [[ -f ${MYSQL_DATA_DIRECTORY}/ibdata1 ]]; then + if [[ -f ${directory}/ibdata1 ]] + then return 0 fi return 1 } -OPTS_MYSQL_DATA_DIR="$( - get_option \ +function __map_mysql_service_user_to_datadir () +{ + local -r datadir="${1:-"$( + __get_option \ + mysqld \ + datadir \ + "/var/lib/mysql" + )"}" + local -r service_user="${3:-"$( + __get_option \ + mysqld \ + user \ + "mysql" + )"}" + local -r socket="${2:-"$( + __get_option \ + mysqld \ + socket \ + "/var/run/mysqld/mysql.sock" + )"}" + local -r socket_directory="${socket%/*}" + + local datadir_gid + local datadir_uid + local service_user_gid + local service_user_uid + + if [[ ! -d ${datadir} ]] + then + return 0 + fi + + datadir_gid="$( + stat \ + -c \ + "%g" \ + "${datadir}" + )" + datadir_uid="$( + stat \ + -c \ + "%u" \ + "${datadir}" + )" + service_user_gid="$( + id \ + -g \ + "${service_user}" + )" + service_user_uid="$( + id \ + -u \ + "${service_user}" + )" + + if [[ ${datadir_gid} -gt 0 ]] \ + && [[ ${datadir_gid} != ${service_user_gid} ]] + then + groupmod \ + -g \ + "${datadir_gid}" \ + "${service_user}" + fi + + if [[ ${datadir_uid} -gt 0 ]] \ + && [[ ${datadir_uid} != ${service_user_uid} ]] + then + usermod \ + -u \ + "${datadir_uid}" \ + "${service_user}" + chown \ + -R \ + "${service_user}" \ + "${socket_directory}" + fi +} + +OPTS_MYSQL_DATADIR="$( + __get_option \ mysqld \ datadir \ "/var/lib/mysql" )" # MySQL initialisation is a one-shot process. -if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then - - readonly MYSQLD=/usr/sbin/mysqld - readonly PASSWORD_LENGTH=16 - readonly PID_PATH=/var/run/mysqld/mysqld.pid - readonly REDACTED_VALUE="********" +if ! __is_mysql_datadir_populated "${OPTS_MYSQL_DATADIR}" +then + readonly mysqld=/usr/sbin/mysqld + readonly password_length=16 + readonly redacted_value="********" # Get passwords from file if applicable if [[ -n ${MYSQL_ROOT_PASSWORD} ]] \ @@ -130,97 +324,41 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then OPTS_CUSTOM_MYSQL_INIT_SQL="${CUSTOM_MYSQL_INIT_SQL:-}" OPTS_MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" - OPTS_MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-$(get_password ${PASSWORD_LENGTH})}" + OPTS_MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-"$( + __get_password "${password_length}" + )"}" OPTS_MYSQL_ROOT_PASSWORD_HASHED="${MYSQL_ROOT_PASSWORD_HASHED:-false}" - OPTS_MYSQL_SERVICE_USER="$( - get_option \ - mysqld \ - user \ - "mysql" - )" - OPTS_MYSQL_SOCKET="$( - get_option \ - mysqld \ - socket \ - "/var/run/mysqld/mysql.sock" - )" - OPTS_MYSQL_SOCKET_DIR="${OPTS_MYSQL_SOCKET%/*}" OPTS_MYSQL_USER="${MYSQL_USER:-}" OPTS_MYSQL_USER_DATABASE="${MYSQL_USER_DATABASE:-}" OPTS_MYSQL_USER_HOST="$( - get_mysql_user_host \ + __get_mysql_user_host \ "${MYSQL_SUBNET:-127.0.0.1}" )" - OPTS_MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-$(get_password ${PASSWORD_LENGTH})}" + OPTS_MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-"$( + __get_password "${password_length}" + )"}" OPTS_MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" # Adjust the UID/GID values of the service user to match a directory that # could be a mounted volume - if [[ -d ${OPTS_MYSQL_DATA_DIR} ]]; then - SERVICE_UID="$( - stat \ - -c \ - "%u" \ - "${OPTS_MYSQL_DATA_DIR}" - )" - SERVICE_GID="$( - stat \ - -c \ - "%g" \ - "${OPTS_MYSQL_DATA_DIR}" - )" - MYSQL_SERVICE_USER_UID="$( - id \ - -u \ - "${OPTS_MYSQL_SERVICE_USER}" - )" - MYSQL_SERVICE_USER_GID="$( - id \ - -g \ - "${OPTS_MYSQL_SERVICE_USER}" - )" - - if [[ -n ${SERVICE_UID} ]] && [[ ${SERVICE_UID} -ne 0 ]] \ - && [[ ${SERVICE_UID} != "${MYSQL_SERVICE_USER_UID}" ]]; then - usermod \ - -u \ - "${SERVICE_UID}" \ - "${OPTS_MYSQL_SERVICE_USER}" - chown \ - -R \ - "${OPTS_MYSQL_SERVICE_USER}" \ - "${OPTS_MYSQL_SOCKET_DIR}" - fi - - if [[ -n ${SERVICE_GID} ]] && [[ ${SERVICE_GID} -ne 0 ]] \ - && [[ ${SERVICE_GID} != "${MYSQL_SERVICE_USER_GID}" ]]; then - groupmod \ - -g \ - "${SERVICE_GID}" \ - "${OPTS_MYSQL_SERVICE_USER}" - elif [[ -n ${SERVICE_GID} ]] && [[ ${SERVICE_GID} -ne 0 ]]; then - # Add as a supplementary group - usermod \ - -G \ - "${SERVICE_GID}" \ - "${OPTS_MYSQL_SERVICE_USER}" - fi - fi + __map_mysql_service_user_to_datadir \ + "${OPTS_MYSQL_DATADIR}" echo "Initalising MySQL data directory." - ${MYSQLD} \ - --datadir="${OPTS_MYSQL_DATA_DIR}" \ + ${mysqld} \ + --datadir="${OPTS_MYSQL_DATADIR}" \ --initialize-insecure \ - --pid-file="${PID_PATH}" \ + --pid-file=/var/run/mysqld/mysqld.pid \ --skip-name-resolve \ --skip-networking \ - --tmpdir="${OPTS_MYSQL_DATA_DIR}" \ + --tmpdir="${OPTS_MYSQL_DATADIR}" \ --user=mysql \ & - PIDS[0]=${!} + PIDS[0]="${!}" MYSQL_INIT_SQL_DATABASE_TEMPLATE="-- Create database" - if [[ -n ${OPTS_MYSQL_USER_DATABASE} ]]; then + if [[ -n ${OPTS_MYSQL_USER_DATABASE} ]] + then printf \ -v MYSQL_INIT_SQL_DATABASE_TEMPLATE \ -- "%s\nCREATE DATABASE IF NOT EXISTS \`%s\`;" \ @@ -230,10 +368,12 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then MYSQL_INIT_SQL_USER_TEMPLATE="-- Create user" if [[ -n ${OPTS_MYSQL_USER} ]] \ - && [[ -n ${OPTS_MYSQL_USER_HOST} ]]; then + && [[ -n ${OPTS_MYSQL_USER_HOST} ]] + then MYSQL_INIT_SQL_IDENTIFIED_BY="IDENTIFIED BY" - if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]]; then + if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]] + then MYSQL_INIT_SQL_IDENTIFIED_BY+=" PASSWORD" fi @@ -250,7 +390,8 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then MYSQL_INIT_SQL_PRIVILEGES_TEMPLATE="-- Grant privileges" if [[ -n ${OPTS_MYSQL_USER} ]] \ && [[ -n ${OPTS_MYSQL_USER_DATABASE} ]] \ - && [[ -n ${OPTS_MYSQL_USER_HOST} ]]; then + && [[ -n ${OPTS_MYSQL_USER_HOST} ]] + then printf \ -v MYSQL_INIT_SQL_PRIVILEGES_TEMPLATE \ -- "%s\nGRANT %s \nON \`%s\`.* \nTO '%s'@'%s';" \ @@ -311,135 +452,46 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then > /tmp/mysql-init # Wait for the MySQL system table installation to complete - [[ -n ${PIDS[0]} ]] && wait ${PIDS[0]} + if [[ ${PIDS[0]} -gt 0 ]] + then + wait ${PIDS[0]} + fi if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ - && [[ ! -e ${OPTS_MYSQL_DATA_DIR}/server-key.pem ]] + && [[ ! -e ${OPTS_MYSQL_DATADIR}/server-key.pem ]] then echo "Generating MySQL certificates." /usr/bin/mysql_ssl_rsa_setup \ - --datadir="${OPTS_MYSQL_DATA_DIR}" \ + --datadir="${OPTS_MYSQL_DATADIR}" \ --uid=mysql \ &> /dev/null \ & fi - PIDS[1]=${!} + PIDS[1]="${!}" echo "Initialising MySQL." - ${MYSQLD} \ - --pid-file="${PID_PATH}" \ + ${mysqld} \ + --pid-file=/var/run/mysqld/mysqld.pid \ --skip-networking \ --init-file=/tmp/mysql-init \ & - # Set MySQL root user password - # - Prefer to store encrypted via mysql_config_editor. - # - Fallback to store as plaintext in secured file. - if command -v mysql_config_editor &> /dev/null \ - && command -v sshpass &> /dev/null - then - fifo_path="$( - mktemp -d - )" - - if [[ ${?} -ne 0 ]] - then - >&2 printf -- \ - '%s %s %s\n' \ - "ERROR: Failed to create directory" \ - "${fifo_path}" - "- aborting." - exit 1 - fi - - trap \ - "rm -rf \"${fifo_path}\"" \ - INT TERM EXIT - - mkfifo \ - -m 0600 \ - "${fifo_path}/mypasswd" - - exec \ - {mypasswd_fd}<>"${fifo_path}/mypasswd" - - trap \ - "exec {mypasswd_fd}>&-" \ - INT TERM EXIT - - rm -rf \ - "${fifo_path}" - - # Send into background waiting to read the password - sshpass \ - -d ${mypasswd_fd} \ - mysql_config_editor \ - set \ - --skip-warn \ - --password \ - & - PIDS[2]=${!} - - trap \ - "exec {mypasswd_fd}>&- ; \ - kill -9 ${PIDS[2]};" \ - INT TERM EXIT - - printf -- \ - '%s\n' \ - "${OPTS_MYSQL_ROOT_PASSWORD}" \ - >&${mypasswd_fd} \ - & - PIDS[3]=${!} - - trap \ - "exec {mypasswd_fd}>&- ; \ - [[ -n ${PIDS[2]} ]] && kill -9 ${PIDS[2]}; \ - [[ -n ${PIDS[3]} ]] && kill -9 ${PIDS[3]};" \ - INT TERM EXIT - - if [[ -n ${PIDS[2]} ]] - then - wait ${PIDS[2]} - fi - - if [[ ${?} -ne 0 ]] - then - >&2 printf -- \ - "ERROR: Unable to set MySQL root password - aborting.\n" - exit 1 - fi - - exec \ - {mypasswd_fd}>&- - else - tee /root/.my.cnf &> /dev/null \ - <<-EOT - [client] - password={{MYSQL_ROOT_PASSWORD}} - EOT - - chown root:root \ - /root/.my.cnf - chmod 0600 \ - /root/.my.cnf - - sed -i \ - -e "s~{{MYSQL_ROOT_PASSWORD}}~${OPTS_MYSQL_ROOT_PASSWORD}~g" \ - /root/.my.cnf - fi + __configure_mysql_root_password \ + "${OPTS_MYSQL_ROOT_PASSWORD}" # Wait for the MySQL database to be initialised by testing COUNTER=$(( 2 * OPTS_MYSQL_INIT_LIMIT )) - until (( COUNTER == 0 )); do + until (( COUNTER == 0 )) + do sleep 0.5 - if have_mysql_access root "-" mysql; then - + if __have_mysql_access + then # Set the password if it was supplied pre-hashed. - if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]]; then + if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] + then mysql \ -e "UPDATE mysql.user SET authentication_string = '${OPTS_MYSQL_ROOT_PASSWORD}' WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES;" @@ -453,7 +505,8 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then (( COUNTER -= 1 )) done - if [[ ${COUNTER} -eq 0 ]]; then + if [[ ${COUNTER} -eq 0 ]] + then printf \ -- "MySQL initilisation failed after %s seconds.\n" \ "${OPTS_MYSQL_INIT_LIMIT}" @@ -467,7 +520,8 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then echo "Stopping MySQL." # Prefer mysqladmin shutdown method if password is known - if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]]; then + if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] + then killall \ -w \ -15 \ @@ -483,16 +537,21 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then fi # Wait for the MySQL certificate setup to complete - [[ -n ${PIDS[1]} ]] && wait ${PIDS[1]} + if [[ ${PIDS[1]} -gt 0 ]] + then + wait ${PIDS[1]} + fi - if [[ -n ${MYSQL_ROOT_PASSWORD} ]]; then - OPTS_MYSQL_ROOT_PASSWORD="${REDACTED_VALUE}" - MYSQL_ROOT_PASSWORD="${REDACTED_VALUE}" + if [[ -n ${MYSQL_ROOT_PASSWORD} ]] + then + OPTS_MYSQL_ROOT_PASSWORD="${redacted_value}" + MYSQL_ROOT_PASSWORD="${redacted_value}" fi - if [[ -n ${MYSQL_USER_PASSWORD} ]]; then - OPTS_MYSQL_USER_PASSWORD="${REDACTED_VALUE}" - MYSQL_USER_PASSWORD="${REDACTED_VALUE}" + if [[ -n ${MYSQL_USER_PASSWORD} ]] + then + OPTS_MYSQL_USER_PASSWORD="${redacted_value}" + MYSQL_USER_PASSWORD="${redacted_value}" fi # Local root user details @@ -501,10 +560,11 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then -- "user : %s@%s, password : %s" \ root \ localhost \ - "${REDACTED_VALUE}" + "${redacted_value}" if [[ -n ${OPTS_MYSQL_USER} ]] \ - && [[ -n ${OPTS_MYSQL_USER_PASSWORD} ]]; then + && [[ -n ${OPTS_MYSQL_USER_PASSWORD} ]] + then printf \ -v DETAILS_MYSQL_USER_CREDENTIALS \ -- "%s\nuser : %s@%s, password : %s" \ @@ -533,12 +593,7 @@ if ! is_mysql_data_directory_populated "${OPTS_MYSQL_DATA_DIR}"; then ${TIMER_TOTAL} EOT - - trap - \ - INT TERM EXIT fi -# Release lock file +# Release lock rm -f /var/lock/subsys/mysqld-bootstrap - -exit 0 From ec93b3113587af40991cb922df793eb38bf7c1e9 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 10 Mar 2019 17:20:29 +0000 Subject: [PATCH 42/98] #211: Updates function name and comment to clarify purpose. --- src/usr/sbin/mysqld-bootstrap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index c8c7aa4..824b70a 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -9,10 +9,10 @@ TIMER_START="$( source /etc/mysqld-bootstrap.conf -# Set MySQL root user password +# Set MySQL client root user password # - Prefer to store encrypted via mysql_config_editor. # - Fallback to store as plaintext in secured file. -function __configure_mysql_root_password () +function __configure_mysql_client_root_password () { local -r password="${1:-}" @@ -476,7 +476,7 @@ then --init-file=/tmp/mysql-init \ & - __configure_mysql_root_password \ + __configure_mysql_client_root_password \ "${OPTS_MYSQL_ROOT_PASSWORD}" # Wait for the MySQL database to be initialised by testing From ad172a5507943b4c6749b61a71a4d0638ffa2868 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 10 Mar 2019 21:23:26 +0000 Subject: [PATCH 43/98] #211: Adds WIP iteration of refactoring bootstrap script. --- src/usr/sbin/mysqld-bootstrap | 60 +++++++++++++++-------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 824b70a..e2c84e4 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -310,20 +310,18 @@ then readonly redacted_value="********" # Get passwords from file if applicable - if [[ -n ${MYSQL_ROOT_PASSWORD} ]] \ - && [[ -s ${MYSQL_ROOT_PASSWORD} ]] + if [[ -s ${MYSQL_ROOT_PASSWORD} ]] then MYSQL_ROOT_PASSWORD="$(< "${MYSQL_ROOT_PASSWORD}")" fi - if [[ -n ${MYSQL_USER_PASSWORD} ]] \ - && [[ -s ${MYSQL_USER_PASSWORD} ]] + if [[ -s ${MYSQL_USER_PASSWORD} ]] then MYSQL_USER_PASSWORD="$(< "${MYSQL_USER_PASSWORD}")" fi - OPTS_CUSTOM_MYSQL_INIT_SQL="${CUSTOM_MYSQL_INIT_SQL:-}" OPTS_MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" + OPTS_MYSQL_INIT_SQL="${CUSTOM_MYSQL_INIT_SQL:-}" OPTS_MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-"$( __get_password "${password_length}" )"}" @@ -356,46 +354,45 @@ then & PIDS[0]="${!}" - MYSQL_INIT_SQL_DATABASE_TEMPLATE="-- Create database" + MYSQL_INIT_SQL_TEMPLATE_DATABASE="-- Create database" if [[ -n ${OPTS_MYSQL_USER_DATABASE} ]] then printf \ - -v MYSQL_INIT_SQL_DATABASE_TEMPLATE \ + -v MYSQL_INIT_SQL_TEMPLATE_DATABASE \ -- "%s\nCREATE DATABASE IF NOT EXISTS \`%s\`;" \ - "${MYSQL_INIT_SQL_DATABASE_TEMPLATE}" \ + "${MYSQL_INIT_SQL_TEMPLATE_DATABASE}" \ "{{MYSQL_USER_DATABASE}}" fi - MYSQL_INIT_SQL_USER_TEMPLATE="-- Create user" + MYSQL_INIT_SQL_TEMPLATE_USER="-- Create user" if [[ -n ${OPTS_MYSQL_USER} ]] \ && [[ -n ${OPTS_MYSQL_USER_HOST} ]] then - - MYSQL_INIT_SQL_IDENTIFIED_BY="IDENTIFIED BY" + MYSQL_INIT_SQL_USER_IDENTIFIED_BY="IDENTIFIED BY" if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]] then - MYSQL_INIT_SQL_IDENTIFIED_BY+=" PASSWORD" + MYSQL_INIT_SQL_USER_IDENTIFIED_BY+=" PASSWORD" fi printf \ - -v MYSQL_INIT_SQL_USER_TEMPLATE \ + -v MYSQL_INIT_SQL_TEMPLATE_USER \ -- "%s\nCREATE USER '%s'@'%s' \n%s '%s';" \ - "${MYSQL_INIT_SQL_USER_TEMPLATE}" \ + "${MYSQL_INIT_SQL_TEMPLATE_USER}" \ "{{MYSQL_USER}}" \ "{{MYSQL_USER_HOST}}" \ - "${MYSQL_INIT_SQL_IDENTIFIED_BY}" \ + "${MYSQL_INIT_SQL_USER_IDENTIFIED_BY}" \ "{{MYSQL_USER_PASSWORD}}" fi - MYSQL_INIT_SQL_PRIVILEGES_TEMPLATE="-- Grant privileges" + MYSQL_INIT_SQL_TEMPLATE_PRIVILEGES="-- Grant privileges" if [[ -n ${OPTS_MYSQL_USER} ]] \ && [[ -n ${OPTS_MYSQL_USER_DATABASE} ]] \ && [[ -n ${OPTS_MYSQL_USER_HOST} ]] then printf \ - -v MYSQL_INIT_SQL_PRIVILEGES_TEMPLATE \ + -v MYSQL_INIT_SQL_TEMPLATE_PRIVILEGES \ -- "%s\nGRANT %s \nON \`%s\`.* \nTO '%s'@'%s';" \ - "${MYSQL_INIT_SQL_PRIVILEGES_TEMPLATE}" \ + "${MYSQL_INIT_SQL_TEMPLATE_PRIVILEGES}" \ "ALL PRIVILEGES" \ "{{MYSQL_USER_DATABASE}}" \ "{{MYSQL_USER}}" \ @@ -414,16 +411,16 @@ then DROP DATABASE IF EXISTS test; DELETE FROM mysql.user WHERE User='' OR User='root' AND Host != 'localhost'; DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'; - ${MYSQL_INIT_SQL_DATABASE_TEMPLATE} - ${MYSQL_INIT_SQL_USER_TEMPLATE} + ${MYSQL_INIT_SQL_TEMPLATE_DATABASE} + ${MYSQL_INIT_SQL_TEMPLATE_USER} -- ============================================================================= -- Custom Initialisation SQL start -- - ${OPTS_CUSTOM_MYSQL_INIT_SQL} + ${OPTS_MYSQL_INIT_SQL} -- -- Custom Initialisation SQL end -- ----------------------------------------------------------------------------- - ${MYSQL_INIT_SQL_PRIVILEGES_TEMPLATE} + ${MYSQL_INIT_SQL_TEMPLATE_PRIVILEGES} GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' @@ -542,18 +539,6 @@ then wait ${PIDS[1]} fi - if [[ -n ${MYSQL_ROOT_PASSWORD} ]] - then - OPTS_MYSQL_ROOT_PASSWORD="${redacted_value}" - MYSQL_ROOT_PASSWORD="${redacted_value}" - fi - - if [[ -n ${MYSQL_USER_PASSWORD} ]] - then - OPTS_MYSQL_USER_PASSWORD="${redacted_value}" - MYSQL_USER_PASSWORD="${redacted_value}" - fi - # Local root user details printf \ -v DETAILS_MYSQL_USER_CREDENTIALS \ @@ -565,6 +550,12 @@ then if [[ -n ${OPTS_MYSQL_USER} ]] \ && [[ -n ${OPTS_MYSQL_USER_PASSWORD} ]] then + # Redact operator supplied password + if [[ -n ${MYSQL_USER_PASSWORD} ]] + then + OPTS_MYSQL_USER_PASSWORD="${redacted_value}" + fi + printf \ -v DETAILS_MYSQL_USER_CREDENTIALS \ -- "%s\nuser : %s@%s, password : %s" \ @@ -581,6 +572,7 @@ then { print T2 - T1; }" )" + # Send to log file until user password can be stored in configuration file. tee /var/log/mysqld.log \ <<-EOT From ac19df5f017b069aac0797458b59baf5e4de1689 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 10 Mar 2019 21:25:03 +0000 Subject: [PATCH 44/98] #211: Removes unecessarily setting password length value to the default. --- src/usr/sbin/mysqld-bootstrap | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index e2c84e4..23fdf6d 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -306,7 +306,6 @@ OPTS_MYSQL_DATADIR="$( if ! __is_mysql_datadir_populated "${OPTS_MYSQL_DATADIR}" then readonly mysqld=/usr/sbin/mysqld - readonly password_length=16 readonly redacted_value="********" # Get passwords from file if applicable @@ -323,7 +322,7 @@ then OPTS_MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" OPTS_MYSQL_INIT_SQL="${CUSTOM_MYSQL_INIT_SQL:-}" OPTS_MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-"$( - __get_password "${password_length}" + __get_password )"}" OPTS_MYSQL_ROOT_PASSWORD_HASHED="${MYSQL_ROOT_PASSWORD_HASHED:-false}" OPTS_MYSQL_USER="${MYSQL_USER:-}" @@ -333,7 +332,7 @@ then "${MYSQL_SUBNET:-127.0.0.1}" )" OPTS_MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-"$( - __get_password "${password_length}" + __get_password )"}" OPTS_MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" From 40b078ea36e140dc676a87d4b510e0cea09d2e97 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 10 Mar 2019 21:34:49 +0000 Subject: [PATCH 45/98] #211: Adds simplified variable names. --- src/usr/sbin/mysqld-bootstrap | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 23fdf6d..969c617 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -353,45 +353,45 @@ then & PIDS[0]="${!}" - MYSQL_INIT_SQL_TEMPLATE_DATABASE="-- Create database" + MYSQL_INIT_TEMPLATE_DATABASE="-- Create database" if [[ -n ${OPTS_MYSQL_USER_DATABASE} ]] then printf \ - -v MYSQL_INIT_SQL_TEMPLATE_DATABASE \ + -v MYSQL_INIT_TEMPLATE_DATABASE \ -- "%s\nCREATE DATABASE IF NOT EXISTS \`%s\`;" \ - "${MYSQL_INIT_SQL_TEMPLATE_DATABASE}" \ + "${MYSQL_INIT_TEMPLATE_DATABASE}" \ "{{MYSQL_USER_DATABASE}}" fi - MYSQL_INIT_SQL_TEMPLATE_USER="-- Create user" + MYSQL_INIT_TEMPLATE_USER="-- Create user" if [[ -n ${OPTS_MYSQL_USER} ]] \ && [[ -n ${OPTS_MYSQL_USER_HOST} ]] then - MYSQL_INIT_SQL_USER_IDENTIFIED_BY="IDENTIFIED BY" + IDENTIFIED_BY="IDENTIFIED BY" if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]] then - MYSQL_INIT_SQL_USER_IDENTIFIED_BY+=" PASSWORD" + IDENTIFIED_BY+=" PASSWORD" fi printf \ - -v MYSQL_INIT_SQL_TEMPLATE_USER \ + -v MYSQL_INIT_TEMPLATE_USER \ -- "%s\nCREATE USER '%s'@'%s' \n%s '%s';" \ - "${MYSQL_INIT_SQL_TEMPLATE_USER}" \ + "${MYSQL_INIT_TEMPLATE_USER}" \ "{{MYSQL_USER}}" \ "{{MYSQL_USER_HOST}}" \ - "${MYSQL_INIT_SQL_USER_IDENTIFIED_BY}" \ + "${IDENTIFIED_BY}" \ "{{MYSQL_USER_PASSWORD}}" fi - MYSQL_INIT_SQL_TEMPLATE_PRIVILEGES="-- Grant privileges" + MYSQL_INIT_TEMPLATE_PRIVILEGES="-- Grant privileges" if [[ -n ${OPTS_MYSQL_USER} ]] \ && [[ -n ${OPTS_MYSQL_USER_DATABASE} ]] \ && [[ -n ${OPTS_MYSQL_USER_HOST} ]] then printf \ - -v MYSQL_INIT_SQL_TEMPLATE_PRIVILEGES \ + -v MYSQL_INIT_TEMPLATE_PRIVILEGES \ -- "%s\nGRANT %s \nON \`%s\`.* \nTO '%s'@'%s';" \ - "${MYSQL_INIT_SQL_TEMPLATE_PRIVILEGES}" \ + "${MYSQL_INIT_TEMPLATE_PRIVILEGES}" \ "ALL PRIVILEGES" \ "{{MYSQL_USER_DATABASE}}" \ "{{MYSQL_USER}}" \ @@ -410,8 +410,8 @@ then DROP DATABASE IF EXISTS test; DELETE FROM mysql.user WHERE User='' OR User='root' AND Host != 'localhost'; DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'; - ${MYSQL_INIT_SQL_TEMPLATE_DATABASE} - ${MYSQL_INIT_SQL_TEMPLATE_USER} + ${MYSQL_INIT_TEMPLATE_DATABASE} + ${MYSQL_INIT_TEMPLATE_USER} -- ============================================================================= -- Custom Initialisation SQL start -- @@ -419,7 +419,7 @@ then -- -- Custom Initialisation SQL end -- ----------------------------------------------------------------------------- - ${MYSQL_INIT_SQL_TEMPLATE_PRIVILEGES} + ${MYSQL_INIT_TEMPLATE_PRIVILEGES} GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' From e259b1ce3f352a23b620ef32f50109546a9b5b01 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 10 Mar 2019 21:36:50 +0000 Subject: [PATCH 46/98] #211: Adds updated timer logic. --- src/usr/sbin/mysqld-bootstrap | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 969c617..67044a4 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -3,7 +3,7 @@ # Create lock touch /var/lock/subsys/mysqld-bootstrap -TIMER_START="$( +timer_start="$( date +%s.%N )" @@ -564,11 +564,15 @@ then "${OPTS_MYSQL_USER_PASSWORD}" fi - TIMER_TOTAL="$( - echo - | awk "\ - { T1=\"${TIMER_START}\" } \ - { T2=\"$(date +%s.%N)\" } \ - { print T2 - T1; }" + timer_total="$( + awk \ + -v timer_end="$( + date +%s.%N + )" \ + -v timer_start="${timer_start}" \ + 'BEGIN { print \ + timer_end - timer_start; + }' )" # Send to log file until user password can be stored in configuration file. @@ -581,7 +585,7 @@ then database : ${OPTS_MYSQL_USER_DATABASE:-N/A} ${DETAILS_MYSQL_USER_CREDENTIALS} -------------------------------------------------------------------------------- - ${TIMER_TOTAL} + ${timer_total} EOT fi From f082daec3ee80fc70b32faa7531d2b421047260d Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 11 Mar 2019 08:01:55 +0000 Subject: [PATCH 47/98] #211: Move placeholder replacement to final step of MySQL init SQL. --- src/usr/sbin/mysqld-bootstrap | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 67044a4..8f57d1a 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -437,14 +437,16 @@ then -e '/^[ \t]*--.*$/d' \ -e 's/;[ \t]*--.*$/;/g' \ -e '/^$/d' \ + | \ + awk \ + '{ ORS=( /;$/ ? RS:FS ) } 1' \ + | \ + sed \ -e "s~{{MYSQL_ROOT_PASSWORD}}~${OPTS_MYSQL_ROOT_PASSWORD}~g" \ -e "s~{{MYSQL_USER}}~${OPTS_MYSQL_USER}~g" \ -e "s~{{MYSQL_USER_DATABASE}}~${OPTS_MYSQL_USER_DATABASE}~g" \ -e "s~{{MYSQL_USER_HOST}}~${OPTS_MYSQL_USER_HOST}~g" \ -e "s~{{MYSQL_USER_PASSWORD}}~${OPTS_MYSQL_USER_PASSWORD}~g" \ - | \ - awk \ - '{ ORS=( /;$/ ? RS:FS ) } 1' \ > /tmp/mysql-init # Wait for the MySQL system table installation to complete From 347267458ed792c921a2b036ec3a21911871f6e7 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 11 Mar 2019 16:53:23 +0000 Subject: [PATCH 48/98] #211: Adds bootstrap function to generate MySQL init template. --- src/usr/sbin/mysqld-bootstrap | 331 ++++++++++++++++++++++++---------- 1 file changed, 237 insertions(+), 94 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 8f57d1a..3f44451 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -121,6 +121,204 @@ function __configure_mysql_client_root_password () INT TERM EXIT } +# __get_mysql_init_template \ +# --init-sql="${OPTS_MYSQL_INIT_SQL}" \ +# --user="${OPTS_MYSQL_USER}" \ +# --database="${OPTS_MYSQL_USER_DATABASE}" \ +# --host="${OPTS_MYSQL_USER_HOST}" \ +# --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ +# --compact +function __get_mysql_init_template () +{ + local compact="false" + local database + local host + local init_sql + local password_hashed="false" + local -a pids + local template_database="-- Create database" + local template_identified_by="IDENTIFIED BY" + local template_privileges="-- Grant privileges" + local template_user="-- Create user" + local user + + # Parse options + while [[ "${#}" -gt 0 ]] + do + case "${1}" in + --compact) + compact="true" + shift 1 + ;; + --database=|--database=*) + database="${1#*=}" + shift 1 + ;; + --host=|--host=*) + host="${1#*=}" + shift 1 + ;; + --init-sql=*) + init_sql="${1#*=}" + shift 1 + ;; + --password-hashed) + password_hashed="true" + shift 1 + ;; + --user=|--user=*) + user="${1#*=}" + shift 1 + ;; + *) + >&2 printf -- \ + 'ERROR: Unknown option %s\n' \ + "${1}" + return 1 + ;; + esac + done + + fifo_path="$( + mktemp -d + )" + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + '%s %s %s\n' \ + "ERROR: Failed to create directory" \ + "${fifo_path}" + "- aborting." + exit 1 + fi + + trap \ + "rm -rf \"${fifo_path}\"" \ + INT TERM EXIT + + mkfifo \ + -m 0600 \ + "${fifo_path}"/mysql-init-template + + if [[ -n ${database} ]] + then + printf \ + -v template_database \ + -- "%s\nCREATE DATABASE IF NOT EXISTS \`%s\`;" \ + "${template_database}" \ + "{{MYSQL_USER_DATABASE}}" + fi + + if [[ -n ${user} ]] \ + && [[ -n ${host} ]] + then + if [[ ${password_hashed} == true ]] + then + template_identified_by+=" PASSWORD" + fi + + printf \ + -v template_user \ + -- "%s\nCREATE USER '%s'@'%s' \n%s '%s';" \ + "${template_user}" \ + "{{MYSQL_USER}}" \ + "{{MYSQL_USER_HOST}}" \ + "${template_identified_by}" \ + "{{MYSQL_USER_PASSWORD}}" + fi + + if [[ -n ${user} ]] \ + && [[ -n ${database} ]] \ + && [[ -n ${host} ]] + then + printf \ + -v template_privileges \ + -- "%s\nGRANT %s \nON \`%s\`.* \nTO '%s'@'%s';" \ + "${template_privileges}" \ + "ALL PRIVILEGES" \ + "{{MYSQL_USER_DATABASE}}" \ + "{{MYSQL_USER}}" \ + "{{MYSQL_USER_HOST}}" + fi + + # Listen for the template and output as required + if [[ ${compact} == false ]] + then + cat <<-EOF & + $(<"${fifo_path}"/mysql-init-template) + EOF + else + cat -s \ + <<-EOF | \ + sed \ + -e '/^[ \t]*--.*$/d' \ + -e 's/;[ \t]*--.*$/;/g' \ + -e '/^$/d' \ + | \ + awk \ + '{ ORS=( /;$/ ? RS:FS ) } 1' \ + & + $(<"${fifo_path}"/mysql-init-template) + EOF + fi + pids[1]="${!}" + + # Generate the initialisation SQL used secure MySQL + cat \ + <<-EOT > "${fifo_path}"/mysql-init-template & + + -- ============================================================================= + -- Initialisation SQL + -- ----------------------------------------------------------------------------- + -- Secure MySQL + DROP DATABASE IF EXISTS test; + DELETE FROM mysql.user WHERE User='' OR User='root' AND Host != 'localhost'; + DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'; + ${template_database} + ${template_user} + -- ============================================================================= + -- Custom Initialisation SQL start + -- + ${init_sql} + -- + -- Custom Initialisation SQL end + -- ----------------------------------------------------------------------------- + ${template_privileges} + GRANT ALL PRIVILEGES + ON *.* + TO 'root'@'localhost' + IDENTIFIED BY '{{MYSQL_ROOT_PASSWORD}}' + WITH GRANT OPTION; + FLUSH PRIVILEGES; + EOT + pids[2]="${!}" + + trap \ + "rm -rf \"${fifo_path}\"; \ + [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]}; \ + [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]};" \ + INT TERM EXIT + + if [[ ${pids[1]} -gt 0 ]] + then + wait ${pids[1]} + fi + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + "ERROR: Unable to gererate MySQL init template - aborting.\n" + exit 1 + fi + + rm -rf \ + "${fifo_path}" + + trap - \ + INT TERM EXIT +} + function __get_mysql_user_host () { local -r client_subnet="${1:-127.0.0.1}" @@ -353,100 +551,45 @@ then & PIDS[0]="${!}" - MYSQL_INIT_TEMPLATE_DATABASE="-- Create database" - if [[ -n ${OPTS_MYSQL_USER_DATABASE} ]] - then - printf \ - -v MYSQL_INIT_TEMPLATE_DATABASE \ - -- "%s\nCREATE DATABASE IF NOT EXISTS \`%s\`;" \ - "${MYSQL_INIT_TEMPLATE_DATABASE}" \ - "{{MYSQL_USER_DATABASE}}" - fi - - MYSQL_INIT_TEMPLATE_USER="-- Create user" - if [[ -n ${OPTS_MYSQL_USER} ]] \ - && [[ -n ${OPTS_MYSQL_USER_HOST} ]] - then - IDENTIFIED_BY="IDENTIFIED BY" - if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]] - then - IDENTIFIED_BY+=" PASSWORD" - fi - - printf \ - -v MYSQL_INIT_TEMPLATE_USER \ - -- "%s\nCREATE USER '%s'@'%s' \n%s '%s';" \ - "${MYSQL_INIT_TEMPLATE_USER}" \ - "{{MYSQL_USER}}" \ - "{{MYSQL_USER_HOST}}" \ - "${IDENTIFIED_BY}" \ - "{{MYSQL_USER_PASSWORD}}" - fi - - MYSQL_INIT_TEMPLATE_PRIVILEGES="-- Grant privileges" - if [[ -n ${OPTS_MYSQL_USER} ]] \ - && [[ -n ${OPTS_MYSQL_USER_DATABASE} ]] \ - && [[ -n ${OPTS_MYSQL_USER_HOST} ]] - then - printf \ - -v MYSQL_INIT_TEMPLATE_PRIVILEGES \ - -- "%s\nGRANT %s \nON \`%s\`.* \nTO '%s'@'%s';" \ - "${MYSQL_INIT_TEMPLATE_PRIVILEGES}" \ - "ALL PRIVILEGES" \ - "{{MYSQL_USER_DATABASE}}" \ - "{{MYSQL_USER}}" \ - "{{MYSQL_USER_HOST}}" - fi - - # Generate the initialisation SQL used secure MySQL - tee \ - /tmp/mysql-init-template \ - <<-EOT - - -- ============================================================================= - -- Initialisation SQL - -- ----------------------------------------------------------------------------- - -- Secure MySQL - DROP DATABASE IF EXISTS test; - DELETE FROM mysql.user WHERE User='' OR User='root' AND Host != 'localhost'; - DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'; - ${MYSQL_INIT_TEMPLATE_DATABASE} - ${MYSQL_INIT_TEMPLATE_USER} - -- ============================================================================= - -- Custom Initialisation SQL start - -- - ${OPTS_MYSQL_INIT_SQL} - -- - -- Custom Initialisation SQL end - -- ----------------------------------------------------------------------------- - ${MYSQL_INIT_TEMPLATE_PRIVILEGES} - GRANT ALL PRIVILEGES - ON *.* - TO 'root'@'localhost' - IDENTIFIED BY '{{MYSQL_ROOT_PASSWORD}}' - WITH GRANT OPTION; - FLUSH PRIVILEGES; - EOT - - # Each statement must be on a single line and should not include comments. - cat \ - -s \ - /tmp/mysql-init-template \ - | \ - sed \ - -e '/^[ \t]*--.*$/d' \ - -e 's/;[ \t]*--.*$/;/g' \ - -e '/^$/d' \ - | \ - awk \ - '{ ORS=( /;$/ ? RS:FS ) } 1' \ - | \ - sed \ - -e "s~{{MYSQL_ROOT_PASSWORD}}~${OPTS_MYSQL_ROOT_PASSWORD}~g" \ - -e "s~{{MYSQL_USER}}~${OPTS_MYSQL_USER}~g" \ - -e "s~{{MYSQL_USER_DATABASE}}~${OPTS_MYSQL_USER_DATABASE}~g" \ - -e "s~{{MYSQL_USER_HOST}}~${OPTS_MYSQL_USER_HOST}~g" \ - -e "s~{{MYSQL_USER_PASSWORD}}~${OPTS_MYSQL_USER_PASSWORD}~g" \ + # MySQL Initialisation Template logs output + __get_mysql_init_template \ + --user="${OPTS_MYSQL_USER}" \ + --database="${OPTS_MYSQL_USER_DATABASE}" \ + --host="${OPTS_MYSQL_USER_HOST}" \ + $( + if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]] + then + printf -- \ + '--password-hashed' + fi + ) \ + --init-sql="${OPTS_MYSQL_INIT_SQL}" + + # Replace compact template placeholders with values + mysql_init_template="$( + __get_mysql_init_template \ + --user="${OPTS_MYSQL_USER}" \ + --database="${OPTS_MYSQL_USER_DATABASE}" \ + --host="${OPTS_MYSQL_USER_HOST}" \ + $( + if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]] + then + printf -- \ + '--password-hashed' + fi + ) \ + --init-sql="${OPTS_MYSQL_INIT_SQL}" \ + --compact + )" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER}}'/${OPTS_MYSQL_USER}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_DATABASE}}'/${OPTS_MYSQL_USER_DATABASE}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_HOST}}'/${OPTS_MYSQL_USER_HOST}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_PASSWORD}}'/${OPTS_MYSQL_USER_PASSWORD}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_ROOT_PASSWORD}}'/${OPTS_MYSQL_ROOT_PASSWORD}}" + + printf -- \ + '%s\n' \ + "${mysql_init_template}" \ > /tmp/mysql-init # Wait for the MySQL system table installation to complete From 401ddf376285f1683c13700589cbead588a00c74 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 11 Mar 2019 20:37:35 +0000 Subject: [PATCH 49/98] #211: Reverts back to 10s startup time, improves option handling of the init template function and pushes user modifications into background. --- environment.mk | 2 +- src/opt/scmi/environment.sh | 2 +- src/opt/scmi/service-unit.sh | 2 +- src/usr/sbin/mysqld-bootstrap | 77 +++++++++++++++++------------------ 4 files changed, 40 insertions(+), 43 deletions(-) diff --git a/environment.mk b/environment.mk index 4e4073e..38a7ddd 100644 --- a/environment.mk +++ b/environment.mk @@ -27,7 +27,7 @@ NO_CACHE ?= false DIST_PATH ?= ./dist # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME ?= 8 +STARTUP_TIME ?= 10 # ------------------------------------------------------------------------------ # Application container configuration diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index e82d643..0c73f6f 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -26,7 +26,7 @@ NO_CACHE="${NO_CACHE:-false}" DIST_PATH="${DIST_PATH:-./dist}" # Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME="${STARTUP_TIME:-8}" +STARTUP_TIME="${STARTUP_TIME:-10}" # ETCD register service settings REGISTER_ETCD_PARAMETERS="${REGISTER_ETCD_PARAMETERS:-}" diff --git a/src/opt/scmi/service-unit.sh b/src/opt/scmi/service-unit.sh index 3ccaeb1..24accaf 100644 --- a/src/opt/scmi/service-unit.sh +++ b/src/opt/scmi/service-unit.sh @@ -25,4 +25,4 @@ readonly SERVICE_UNIT_REGISTER_ENVIRONMENT_KEYS=" # ------------------------------------------------------------------------------ # Variables # ------------------------------------------------------------------------------ -SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-11}" +SERVICE_UNIT_INSTALL_TIMEOUT="${SERVICE_UNIT_INSTALL_TIMEOUT:-13}" diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 3f44451..ccb0a22 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -121,19 +121,12 @@ function __configure_mysql_client_root_password () INT TERM EXIT } -# __get_mysql_init_template \ -# --init-sql="${OPTS_MYSQL_INIT_SQL}" \ -# --user="${OPTS_MYSQL_USER}" \ -# --database="${OPTS_MYSQL_USER_DATABASE}" \ -# --host="${OPTS_MYSQL_USER_HOST}" \ -# --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ -# --compact function __get_mysql_init_template () { local compact="false" local database local host - local init_sql + local init_sql="-- Custom Initialisation SQL" local password_hashed="false" local -a pids local template_database="-- Create database" @@ -146,15 +139,19 @@ function __get_mysql_init_template () while [[ "${#}" -gt 0 ]] do case "${1}" in - --compact) + --compact|--compact=true) compact="true" shift 1 ;; - --database=|--database=*) + --compact=*) + compact="false" + shift 1 + ;; + --database=*) database="${1#*=}" shift 1 ;; - --host=|--host=*) + --host=*) host="${1#*=}" shift 1 ;; @@ -162,11 +159,15 @@ function __get_mysql_init_template () init_sql="${1#*=}" shift 1 ;; - --password-hashed) + --password-hashed|--password-hashed=true) password_hashed="true" shift 1 ;; - --user=|--user=*) + --password-hashed=*) + password_hashed="false" + shift 1 + ;; + --user=*) user="${1#*=}" shift 1 ;; @@ -534,11 +535,6 @@ then )"}" OPTS_MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" - # Adjust the UID/GID values of the service user to match a directory that - # could be a mounted volume - __map_mysql_service_user_to_datadir \ - "${OPTS_MYSQL_DATADIR}" - echo "Initalising MySQL data directory." ${mysqld} \ --datadir="${OPTS_MYSQL_DATADIR}" \ @@ -551,18 +547,19 @@ then & PIDS[0]="${!}" + # Adjust the UID/GID values of the service user to match a directory that + # could be a mounted volume + __map_mysql_service_user_to_datadir \ + "${OPTS_MYSQL_DATADIR}" \ + & + PIDS[1]="${!}" + # MySQL Initialisation Template logs output __get_mysql_init_template \ --user="${OPTS_MYSQL_USER}" \ --database="${OPTS_MYSQL_USER_DATABASE}" \ --host="${OPTS_MYSQL_USER_HOST}" \ - $( - if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]] - then - printf -- \ - '--password-hashed' - fi - ) \ + --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ --init-sql="${OPTS_MYSQL_INIT_SQL}" # Replace compact template placeholders with values @@ -571,13 +568,7 @@ then --user="${OPTS_MYSQL_USER}" \ --database="${OPTS_MYSQL_USER_DATABASE}" \ --host="${OPTS_MYSQL_USER_HOST}" \ - $( - if [[ ${OPTS_MYSQL_USER_PASSWORD_HASHED} == true ]] - then - printf -- \ - '--password-hashed' - fi - ) \ + --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ --init-sql="${OPTS_MYSQL_INIT_SQL}" \ --compact )" @@ -595,7 +586,7 @@ then # Wait for the MySQL system table installation to complete if [[ ${PIDS[0]} -gt 0 ]] then - wait ${PIDS[0]} + wait ${PIDS[0]} fi if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ @@ -607,8 +598,8 @@ then --uid=mysql \ &> /dev/null \ & + PIDS[2]="${!}" fi - PIDS[1]="${!}" echo "Initialising MySQL." ${mysqld} \ @@ -677,12 +668,6 @@ then /tmp/mysql-init{,-template} fi - # Wait for the MySQL certificate setup to complete - if [[ ${PIDS[1]} -gt 0 ]] - then - wait ${PIDS[1]} - fi - # Local root user details printf \ -v DETAILS_MYSQL_USER_CREDENTIALS \ @@ -709,6 +694,18 @@ then "${OPTS_MYSQL_USER_PASSWORD}" fi + # Wait for the service user modifications to complete + if [[ ${PIDS[1]} -gt 0 ]] + then + wait ${PIDS[1]} + fi + + # Wait for the MySQL certificate setup to complete + if [[ ${PIDS[2]} -gt 0 ]] + then + wait ${PIDS[2]} + fi + timer_total="$( awk \ -v timer_end="$( From 83b3fd26888cfd0263216bc05ee8a3b8e5720330 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 11 Mar 2019 22:25:38 +0000 Subject: [PATCH 50/98] #211: Adds improved certificate generation handling which can take the most time to complete. --- src/usr/sbin/mysqld-bootstrap | 77 +++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index ccb0a22..55dbe5e 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -41,7 +41,7 @@ function __configure_mysql_client_root_password () fi trap \ - "rm -rf \"${fifo_path}\"" \ + "rm -rf \"${fifo_path}\";" \ INT TERM EXIT mkfifo \ @@ -52,7 +52,7 @@ function __configure_mysql_client_root_password () {mypasswd_fd}<>"${fifo_path}/mypasswd" trap \ - "exec {mypasswd_fd}>&-" \ + "exec {mypasswd_fd}>&- ;" \ INT TERM EXIT rm -rf \ @@ -195,7 +195,7 @@ function __get_mysql_init_template () fi trap \ - "rm -rf \"${fifo_path}\"" \ + "rm -rf \"${fifo_path}\";" \ INT TERM EXIT mkfifo \ @@ -506,6 +506,38 @@ if ! __is_mysql_datadir_populated "${OPTS_MYSQL_DATADIR}" then readonly mysqld=/usr/sbin/mysqld readonly redacted_value="********" + readonly server_key="${OPTS_MYSQL_DATADIR}"/server-key.pem + server_key_path="" + + if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ + && [[ ! -e ${server_key} ]] + then + echo "Generating MySQL certificates." + server_key_path="$( + mktemp -d + )" + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + '%s %s %s\n' \ + "ERROR: Failed to create directory" \ + "${server_key_path}" + "- aborting." + exit 1 + fi + + trap \ + "rm -rf \"${server_key_path}\";" \ + INT TERM EXIT + + /usr/bin/mysql_ssl_rsa_setup \ + --datadir="${server_key_path}" \ + --uid=mysql \ + &> /dev/null \ + & + PIDS[2]="${!}" + fi # Get passwords from file if applicable if [[ -s ${MYSQL_ROOT_PASSWORD} ]] @@ -583,24 +615,17 @@ then "${mysql_init_template}" \ > /tmp/mysql-init + trap \ + "rm -rf \"${server_key_path}\"; \ + rm -f /tmp/mysql-init;" \ + INT TERM EXIT + # Wait for the MySQL system table installation to complete if [[ ${PIDS[0]} -gt 0 ]] then wait ${PIDS[0]} fi - if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ - && [[ ! -e ${OPTS_MYSQL_DATADIR}/server-key.pem ]] - then - echo "Generating MySQL certificates." - /usr/bin/mysql_ssl_rsa_setup \ - --datadir="${OPTS_MYSQL_DATADIR}" \ - --uid=mysql \ - &> /dev/null \ - & - PIDS[2]="${!}" - fi - echo "Initialising MySQL." ${mysqld} \ --pid-file=/var/run/mysqld/mysqld.pid \ @@ -665,7 +690,11 @@ then fi rm -f \ - /tmp/mysql-init{,-template} + /tmp/mysql-init + + trap \ + "rm -rf \"${server_key_path}\";" \ + INT TERM EXIT fi # Local root user details @@ -706,6 +735,19 @@ then wait ${PIDS[2]} fi + # Finalise certificate setup + if [[ -f ${server_key_path}/server-key.pem ]] + then + mv \ + "${server_key_path}"/server-key.pem \ + "${server_key}" + rm -rf \ + "${server_key_path}" + + trap - \ + INT TERM EXIT + fi + timer_total="$( awk \ -v timer_end="$( @@ -733,4 +775,5 @@ then fi # Release lock -rm -f /var/lock/subsys/mysqld-bootstrap +rm -f \ + /var/lock/subsys/mysqld-bootstrap From 7bdc7e0f9f3825ffaac02d8e43b304f668ed7742 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 11 Mar 2019 22:46:32 +0000 Subject: [PATCH 51/98] #211: Adds native bash replacement of password string. --- src/usr/sbin/mysqld-bootstrap | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 55dbe5e..eacb337 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -16,6 +16,7 @@ function __configure_mysql_client_root_password () { local -r password="${1:-}" + local config local -a pids if [[ -z ${password} ]] @@ -112,9 +113,12 @@ function __configure_mysql_client_root_password () chmod 0600 \ /root/.my.cnf - sed -i \ - -e "s~{{MYSQL_ROOT_PASSWORD}}~${password}~g" \ - /root/.my.cnf + config="$(< /root/.my.cnf)" + + printf -- \ + '%s\n' \ + "${config//'{{MYSQL_ROOT_PASSWORD}}'/${password}}" \ + > /root/.my.cnf fi trap - \ From a10fd6e90ca6f9f0e7a4fcb2f86b647fd7bc299f Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 11 Mar 2019 23:14:33 +0000 Subject: [PATCH 52/98] #211: Adds printf based status messages. --- src/usr/sbin/mysqld-bootstrap | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index eacb337..c3139ee 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -269,7 +269,7 @@ function __get_mysql_init_template () fi pids[1]="${!}" - # Generate the initialisation SQL used secure MySQL + # Generate the initialisation SQL template cat \ <<-EOT > "${fifo_path}"/mysql-init-template & @@ -516,7 +516,8 @@ then if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ && [[ ! -e ${server_key} ]] then - echo "Generating MySQL certificates." + printf -- \ + "Generating MySQL certificates.\n" server_key_path="$( mktemp -d )" @@ -571,7 +572,8 @@ then )"}" OPTS_MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" - echo "Initalising MySQL data directory." + printf -- \ + "Initalising MySQL data directory.\n" ${mysqld} \ --datadir="${OPTS_MYSQL_DATADIR}" \ --initialize-insecure \ @@ -630,7 +632,8 @@ then wait ${PIDS[0]} fi - echo "Initialising MySQL." + printf -- \ + "Initialising MySQL.\n" ${mysqld} \ --pid-file=/var/run/mysqld/mysqld.pid \ --skip-networking \ @@ -668,7 +671,7 @@ then if [[ ${COUNTER} -eq 0 ]] then - printf \ + >&2 printf \ -- "MySQL initilisation failed after %s seconds.\n" \ "${OPTS_MYSQL_INIT_LIMIT}" @@ -678,7 +681,8 @@ then exit 1 else - echo "Stopping MySQL." + printf --\ + "Stopping MySQL.\n" # Prefer mysqladmin shutdown method if password is known if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] From 516048ef3c03694cc8f6ae000feb376cc7807da1 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 11 Mar 2019 23:19:22 +0000 Subject: [PATCH 53/98] #211: Fixes typo in progress message. --- src/usr/sbin/mysqld-bootstrap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index c3139ee..31d0c27 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -573,7 +573,7 @@ then OPTS_MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" printf -- \ - "Initalising MySQL data directory.\n" + "Initialising MySQL data directory.\n" ${mysqld} \ --datadir="${OPTS_MYSQL_DATADIR}" \ --initialize-insecure \ @@ -592,7 +592,7 @@ then & PIDS[1]="${!}" - # MySQL Initialisation Template logs output + # MySQL initialisation template output __get_mysql_init_template \ --user="${OPTS_MYSQL_USER}" \ --database="${OPTS_MYSQL_USER_DATABASE}" \ From 7d8f3a5b382f3187fb2ba11f60308c0a01364c41 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Wed, 13 Mar 2019 02:15:54 +0000 Subject: [PATCH 54/98] #211: Adds main function. --- src/usr/sbin/mysqld-bootstrap | 504 +++++++++++++++++----------------- 1 file changed, 256 insertions(+), 248 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 31d0c27..c1e5313 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -1,12 +1,5 @@ #!/usr/bin/env bash -# Create lock -touch /var/lock/subsys/mysqld-bootstrap - -timer_start="$( - date +%s.%N -)" - source /etc/mysqld-bootstrap.conf # Set MySQL client root user password @@ -252,6 +245,7 @@ function __get_mysql_init_template () then cat <<-EOF & $(<"${fifo_path}"/mysql-init-template) + EOF else cat -s \ @@ -498,290 +492,304 @@ function __map_mysql_service_user_to_datadir () fi } -OPTS_MYSQL_DATADIR="$( - __get_option \ - mysqld \ - datadir \ - "/var/lib/mysql" -)" - -# MySQL initialisation is a one-shot process. -if ! __is_mysql_datadir_populated "${OPTS_MYSQL_DATADIR}" -then - readonly mysqld=/usr/sbin/mysqld - readonly redacted_value="********" - readonly server_key="${OPTS_MYSQL_DATADIR}"/server-key.pem - server_key_path="" - - if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ - && [[ ! -e ${server_key} ]] +function main () +{ + local -r lock_file="/var/lock/subsys/mysqld-bootstrap" + local -r timer_start="$( + date +%s.%N + )" + + # Create lock + touch \ + "${lock_file}" + + OPTS_MYSQL_DATADIR="$( + __get_option \ + mysqld \ + datadir \ + "/var/lib/mysql" + )" + + # MySQL initialisation is a one-shot process. + if ! __is_mysql_datadir_populated "${OPTS_MYSQL_DATADIR}" then - printf -- \ - "Generating MySQL certificates.\n" - server_key_path="$( - mktemp -d - )" + readonly mysqld=/usr/sbin/mysqld + readonly redacted_value="********" + readonly server_key="${OPTS_MYSQL_DATADIR}"/server-key.pem + server_key_path="" - if [[ ${?} -ne 0 ]] + if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ + && [[ ! -e ${server_key} ]] then - >&2 printf -- \ - '%s %s %s\n' \ - "ERROR: Failed to create directory" \ - "${server_key_path}" - "- aborting." - exit 1 + printf -- \ + "Generating MySQL certificates.\n" + server_key_path="$( + mktemp -d + )" + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + '%s %s %s\n' \ + "ERROR: Failed to create directory" \ + "${server_key_path}" + "- aborting." + exit 1 + fi + + trap \ + "rm -rf \"${server_key_path}\";" \ + INT TERM EXIT + + /usr/bin/mysql_ssl_rsa_setup \ + --datadir="${server_key_path}" \ + --uid=mysql \ + &> /dev/null \ + & + PIDS[2]="${!}" fi - trap \ - "rm -rf \"${server_key_path}\";" \ - INT TERM EXIT + # Get passwords from file if applicable + if [[ -s ${MYSQL_ROOT_PASSWORD} ]] + then + MYSQL_ROOT_PASSWORD="$(< "${MYSQL_ROOT_PASSWORD}")" + fi - /usr/bin/mysql_ssl_rsa_setup \ - --datadir="${server_key_path}" \ - --uid=mysql \ - &> /dev/null \ - & - PIDS[2]="${!}" - fi + if [[ -s ${MYSQL_USER_PASSWORD} ]] + then + MYSQL_USER_PASSWORD="$(< "${MYSQL_USER_PASSWORD}")" + fi - # Get passwords from file if applicable - if [[ -s ${MYSQL_ROOT_PASSWORD} ]] - then - MYSQL_ROOT_PASSWORD="$(< "${MYSQL_ROOT_PASSWORD}")" - fi + OPTS_MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" + OPTS_MYSQL_INIT_SQL="${CUSTOM_MYSQL_INIT_SQL:-}" + OPTS_MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-"$( + __get_password + )"}" + OPTS_MYSQL_ROOT_PASSWORD_HASHED="${MYSQL_ROOT_PASSWORD_HASHED:-false}" + OPTS_MYSQL_USER="${MYSQL_USER:-}" + OPTS_MYSQL_USER_DATABASE="${MYSQL_USER_DATABASE:-}" + OPTS_MYSQL_USER_HOST="$( + __get_mysql_user_host \ + "${MYSQL_SUBNET:-127.0.0.1}" + )" + OPTS_MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-"$( + __get_password + )"}" + OPTS_MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" - if [[ -s ${MYSQL_USER_PASSWORD} ]] - then - MYSQL_USER_PASSWORD="$(< "${MYSQL_USER_PASSWORD}")" - fi + printf -- \ + "Initialising MySQL data directory.\n" + ${mysqld} \ + --datadir="${OPTS_MYSQL_DATADIR}" \ + --initialize-insecure \ + --pid-file=/var/run/mysqld/mysqld.pid \ + --skip-name-resolve \ + --skip-networking \ + --tmpdir="${OPTS_MYSQL_DATADIR}" \ + --user=mysql \ + & + PIDS[0]="${!}" - OPTS_MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" - OPTS_MYSQL_INIT_SQL="${CUSTOM_MYSQL_INIT_SQL:-}" - OPTS_MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-"$( - __get_password - )"}" - OPTS_MYSQL_ROOT_PASSWORD_HASHED="${MYSQL_ROOT_PASSWORD_HASHED:-false}" - OPTS_MYSQL_USER="${MYSQL_USER:-}" - OPTS_MYSQL_USER_DATABASE="${MYSQL_USER_DATABASE:-}" - OPTS_MYSQL_USER_HOST="$( - __get_mysql_user_host \ - "${MYSQL_SUBNET:-127.0.0.1}" - )" - OPTS_MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-"$( - __get_password - )"}" - OPTS_MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" - - printf -- \ - "Initialising MySQL data directory.\n" - ${mysqld} \ - --datadir="${OPTS_MYSQL_DATADIR}" \ - --initialize-insecure \ - --pid-file=/var/run/mysqld/mysqld.pid \ - --skip-name-resolve \ - --skip-networking \ - --tmpdir="${OPTS_MYSQL_DATADIR}" \ - --user=mysql \ - & - PIDS[0]="${!}" - - # Adjust the UID/GID values of the service user to match a directory that - # could be a mounted volume - __map_mysql_service_user_to_datadir \ - "${OPTS_MYSQL_DATADIR}" \ - & - PIDS[1]="${!}" - - # MySQL initialisation template output - __get_mysql_init_template \ - --user="${OPTS_MYSQL_USER}" \ - --database="${OPTS_MYSQL_USER_DATABASE}" \ - --host="${OPTS_MYSQL_USER_HOST}" \ - --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ - --init-sql="${OPTS_MYSQL_INIT_SQL}" - - # Replace compact template placeholders with values - mysql_init_template="$( + # Adjust the UID/GID values of the service user to match a directory that + # could be a mounted volume + __map_mysql_service_user_to_datadir \ + "${OPTS_MYSQL_DATADIR}" \ + & + PIDS[1]="${!}" + + # MySQL initialisation template output __get_mysql_init_template \ --user="${OPTS_MYSQL_USER}" \ --database="${OPTS_MYSQL_USER_DATABASE}" \ --host="${OPTS_MYSQL_USER_HOST}" \ --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ - --init-sql="${OPTS_MYSQL_INIT_SQL}" \ - --compact - )" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER}}'/${OPTS_MYSQL_USER}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_DATABASE}}'/${OPTS_MYSQL_USER_DATABASE}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_HOST}}'/${OPTS_MYSQL_USER_HOST}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_PASSWORD}}'/${OPTS_MYSQL_USER_PASSWORD}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_ROOT_PASSWORD}}'/${OPTS_MYSQL_ROOT_PASSWORD}}" - - printf -- \ - '%s\n' \ - "${mysql_init_template}" \ - > /tmp/mysql-init - - trap \ - "rm -rf \"${server_key_path}\"; \ - rm -f /tmp/mysql-init;" \ - INT TERM EXIT + --init-sql="${OPTS_MYSQL_INIT_SQL}" + + # Replace compact template placeholders with values + mysql_init_template="$( + __get_mysql_init_template \ + --user="${OPTS_MYSQL_USER}" \ + --database="${OPTS_MYSQL_USER_DATABASE}" \ + --host="${OPTS_MYSQL_USER_HOST}" \ + --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ + --init-sql="${OPTS_MYSQL_INIT_SQL}" \ + --compact + )" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER}}'/${OPTS_MYSQL_USER}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_DATABASE}}'/${OPTS_MYSQL_USER_DATABASE}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_HOST}}'/${OPTS_MYSQL_USER_HOST}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_PASSWORD}}'/${OPTS_MYSQL_USER_PASSWORD}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_ROOT_PASSWORD}}'/${OPTS_MYSQL_ROOT_PASSWORD}}" - # Wait for the MySQL system table installation to complete - if [[ ${PIDS[0]} -gt 0 ]] - then - wait ${PIDS[0]} - fi + printf -- \ + '%s\n' \ + "${mysql_init_template}" \ + > /tmp/mysql-init - printf -- \ - "Initialising MySQL.\n" - ${mysqld} \ - --pid-file=/var/run/mysqld/mysqld.pid \ - --skip-networking \ - --init-file=/tmp/mysql-init \ - & - - __configure_mysql_client_root_password \ - "${OPTS_MYSQL_ROOT_PASSWORD}" - - # Wait for the MySQL database to be initialised by testing - COUNTER=$(( - 2 * OPTS_MYSQL_INIT_LIMIT - )) - until (( COUNTER == 0 )) - do - sleep 0.5 + trap \ + "rm -rf \"${server_key_path}\"; \ + rm -f /tmp/mysql-init;" \ + INT TERM EXIT - if __have_mysql_access + # Wait for the MySQL system table installation to complete + if [[ ${PIDS[0]} -gt 0 ]] then - # Set the password if it was supplied pre-hashed. - if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] - then - mysql \ - -e "UPDATE mysql.user SET authentication_string = '${OPTS_MYSQL_ROOT_PASSWORD}' WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES;" + wait ${PIDS[0]} + fi - rm -f \ - /root/.{my,mylogin}.cnf - fi + printf -- \ + "Initialising MySQL.\n" + ${mysqld} \ + --pid-file=/var/run/mysqld/mysqld.pid \ + --skip-networking \ + --init-file=/tmp/mysql-init \ + & - break - fi + __configure_mysql_client_root_password \ + "${OPTS_MYSQL_ROOT_PASSWORD}" - (( COUNTER -= 1 )) - done + # Wait for the MySQL database to be initialised by testing + COUNTER=$(( + 2 * OPTS_MYSQL_INIT_LIMIT + )) + until (( COUNTER == 0 )) + do + sleep 0.5 - if [[ ${COUNTER} -eq 0 ]] - then - >&2 printf \ - -- "MySQL initilisation failed after %s seconds.\n" \ - "${OPTS_MYSQL_INIT_LIMIT}" + if __have_mysql_access + then + # Set the password if it was supplied pre-hashed. + if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] + then + mysql \ + -e "UPDATE mysql.user SET authentication_string = '${OPTS_MYSQL_ROOT_PASSWORD}' WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES;" - killall \ - -15 \ - mysqld + rm -f \ + /root/.{my,mylogin}.cnf + fi - exit 1 - else - printf --\ - "Stopping MySQL.\n" + break + fi - # Prefer mysqladmin shutdown method if password is known - if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] + (( COUNTER -= 1 )) + done + + if [[ ${COUNTER} -eq 0 ]] then + >&2 printf \ + -- "MySQL initilisation failed after %s seconds.\n" \ + "${OPTS_MYSQL_INIT_LIMIT}" + killall \ - -w \ -15 \ mysqld - else - mysqladmin \ - --user=root \ - shutdown - fi - rm -f \ - /tmp/mysql-init + exit 1 + else + printf -- \ + "Stopping MySQL.\n" - trap \ - "rm -rf \"${server_key_path}\";" \ - INT TERM EXIT - fi + # Prefer mysqladmin shutdown method if password is known + if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] + then + killall \ + -w \ + -15 \ + mysqld + else + mysqladmin \ + --user=root \ + shutdown + fi - # Local root user details - printf \ - -v DETAILS_MYSQL_USER_CREDENTIALS \ - -- "user : %s@%s, password : %s" \ - root \ - localhost \ - "${redacted_value}" + rm -f \ + /tmp/mysql-init - if [[ -n ${OPTS_MYSQL_USER} ]] \ - && [[ -n ${OPTS_MYSQL_USER_PASSWORD} ]] - then - # Redact operator supplied password - if [[ -n ${MYSQL_USER_PASSWORD} ]] - then - OPTS_MYSQL_USER_PASSWORD="${redacted_value}" + trap \ + "rm -rf \"${server_key_path}\";" \ + INT TERM EXIT fi + # Local root user details printf \ -v DETAILS_MYSQL_USER_CREDENTIALS \ - -- "%s\nuser : %s@%s, password : %s" \ - "${DETAILS_MYSQL_USER_CREDENTIALS}" \ - "${OPTS_MYSQL_USER}" \ - "${OPTS_MYSQL_USER_HOST}" \ - "${OPTS_MYSQL_USER_PASSWORD}" - fi + -- "user : %s@%s, password : %s" \ + root \ + localhost \ + "${redacted_value}" - # Wait for the service user modifications to complete - if [[ ${PIDS[1]} -gt 0 ]] - then - wait ${PIDS[1]} - fi + if [[ -n ${OPTS_MYSQL_USER} ]] \ + && [[ -n ${OPTS_MYSQL_USER_PASSWORD} ]] + then + # Redact operator supplied password + if [[ -n ${MYSQL_USER_PASSWORD} ]] + then + OPTS_MYSQL_USER_PASSWORD="${redacted_value}" + fi - # Wait for the MySQL certificate setup to complete - if [[ ${PIDS[2]} -gt 0 ]] - then - wait ${PIDS[2]} - fi + printf \ + -v DETAILS_MYSQL_USER_CREDENTIALS \ + -- "%s\nuser : %s@%s, password : %s" \ + "${DETAILS_MYSQL_USER_CREDENTIALS}" \ + "${OPTS_MYSQL_USER}" \ + "${OPTS_MYSQL_USER_HOST}" \ + "${OPTS_MYSQL_USER_PASSWORD}" + fi - # Finalise certificate setup - if [[ -f ${server_key_path}/server-key.pem ]] - then - mv \ - "${server_key_path}"/server-key.pem \ - "${server_key}" - rm -rf \ - "${server_key_path}" + # Wait for the service user modifications to complete + if [[ ${PIDS[1]} -gt 0 ]] + then + wait ${PIDS[1]} + fi - trap - \ - INT TERM EXIT - fi + # Wait for the MySQL certificate setup to complete + if [[ ${PIDS[2]} -gt 0 ]] + then + wait ${PIDS[2]} + fi - timer_total="$( - awk \ - -v timer_end="$( - date +%s.%N - )" \ - -v timer_start="${timer_start}" \ - 'BEGIN { print \ - timer_end - timer_start; - }' - )" + # Finalise certificate setup + if [[ -f ${server_key_path}/server-key.pem ]] + then + mv \ + "${server_key_path}"/server-key.pem \ + "${server_key}" + rm -rf \ + "${server_key_path}" - # Send to log file until user password can be stored in configuration file. - tee /var/log/mysqld.log \ - <<-EOT + trap - \ + INT TERM EXIT + fi - ================================================================================ - MySQL Details - -------------------------------------------------------------------------------- - database : ${OPTS_MYSQL_USER_DATABASE:-N/A} - ${DETAILS_MYSQL_USER_CREDENTIALS} - -------------------------------------------------------------------------------- - ${timer_total} + timer_total="$( + awk \ + -v timer_end="$( + date +%s.%N + )" \ + -v timer_start="${timer_start}" \ + 'BEGIN { print \ + timer_end - timer_start; + }' + )" - EOT -fi + # Send to log file until user password can be stored in configuration file. + tee /var/log/mysqld.log \ + <<-EOT + + ================================================================================ + MySQL Details + -------------------------------------------------------------------------------- + database : ${OPTS_MYSQL_USER_DATABASE:-N/A} + ${DETAILS_MYSQL_USER_CREDENTIALS} + -------------------------------------------------------------------------------- + ${timer_total} + + EOT + fi + + # Release lock + rm -f \ + "${lock_file}" +} -# Release lock -rm -f \ - /var/lock/subsys/mysqld-bootstrap +main "${@}" From aea90c91fee5198b886444aac8239cce71170358 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Wed, 13 Mar 2019 12:29:59 +0000 Subject: [PATCH 55/98] #211: Define main function'a internal local variables and simplify naming. --- src/usr/sbin/mysqld-bootstrap | 76 +++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index c1e5313..b557e33 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -495,29 +495,35 @@ function __map_mysql_service_user_to_datadir () function main () { local -r lock_file="/var/lock/subsys/mysqld-bootstrap" + local -r mysqld="/usr/sbin/mysqld" + local -r redacted_value="********" + local -r server_key="${datadir}"/server-key.pem local -r timer_start="$( date +%s.%N )" + local counter + local datadir + local mysql_init_template + local -a pids + local server_key_path + local user_details + # Create lock touch \ "${lock_file}" - OPTS_MYSQL_DATADIR="$( + datadir="$( __get_option \ mysqld \ datadir \ "/var/lib/mysql" )" - # MySQL initialisation is a one-shot process. - if ! __is_mysql_datadir_populated "${OPTS_MYSQL_DATADIR}" + # Initialisation is a one-shot process. + if ! __is_mysql_datadir_populated "${datadir}" then - readonly mysqld=/usr/sbin/mysqld - readonly redacted_value="********" - readonly server_key="${OPTS_MYSQL_DATADIR}"/server-key.pem - server_key_path="" - + # Certificate generation if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ && [[ ! -e ${server_key} ]] then @@ -546,7 +552,7 @@ function main () --uid=mysql \ &> /dev/null \ & - PIDS[2]="${!}" + pids[2]="${!}" fi # Get passwords from file if applicable @@ -580,24 +586,24 @@ function main () printf -- \ "Initialising MySQL data directory.\n" ${mysqld} \ - --datadir="${OPTS_MYSQL_DATADIR}" \ + --datadir="${datadir}" \ --initialize-insecure \ --pid-file=/var/run/mysqld/mysqld.pid \ --skip-name-resolve \ --skip-networking \ - --tmpdir="${OPTS_MYSQL_DATADIR}" \ + --tmpdir="${datadir}" \ --user=mysql \ & - PIDS[0]="${!}" + pids[0]="${!}" - # Adjust the UID/GID values of the service user to match a directory that - # could be a mounted volume + # Adjust the UID/GID values of the service user to match a directory + # that could be a mounted volume __map_mysql_service_user_to_datadir \ - "${OPTS_MYSQL_DATADIR}" \ + "${datadir}" \ & - PIDS[1]="${!}" + pids[1]="${!}" - # MySQL initialisation template output + # Initialisation template output __get_mysql_init_template \ --user="${OPTS_MYSQL_USER}" \ --database="${OPTS_MYSQL_USER_DATABASE}" \ @@ -631,10 +637,10 @@ function main () rm -f /tmp/mysql-init;" \ INT TERM EXIT - # Wait for the MySQL system table installation to complete - if [[ ${PIDS[0]} -gt 0 ]] + # Wait to complete system table installation + if [[ ${pids[0]} -gt 0 ]] then - wait ${PIDS[0]} + wait ${pids[0]} fi printf -- \ @@ -648,11 +654,11 @@ function main () __configure_mysql_client_root_password \ "${OPTS_MYSQL_ROOT_PASSWORD}" - # Wait for the MySQL database to be initialised by testing - COUNTER=$(( + # Wait for initialisation to complete (poll for access) or timeout + counter="$(( 2 * OPTS_MYSQL_INIT_LIMIT - )) - until (( COUNTER == 0 )) + ))" + until (( counter == 0 )) do sleep 0.5 @@ -671,10 +677,10 @@ function main () break fi - (( COUNTER -= 1 )) + (( counter -= 1 )) done - if [[ ${COUNTER} -eq 0 ]] + if [[ ${counter} -eq 0 ]] then >&2 printf \ -- "MySQL initilisation failed after %s seconds.\n" \ @@ -712,7 +718,7 @@ function main () # Local root user details printf \ - -v DETAILS_MYSQL_USER_CREDENTIALS \ + -v user_details \ -- "user : %s@%s, password : %s" \ root \ localhost \ @@ -728,24 +734,24 @@ function main () fi printf \ - -v DETAILS_MYSQL_USER_CREDENTIALS \ + -v user_details \ -- "%s\nuser : %s@%s, password : %s" \ - "${DETAILS_MYSQL_USER_CREDENTIALS}" \ + "${user_details}" \ "${OPTS_MYSQL_USER}" \ "${OPTS_MYSQL_USER_HOST}" \ "${OPTS_MYSQL_USER_PASSWORD}" fi # Wait for the service user modifications to complete - if [[ ${PIDS[1]} -gt 0 ]] + if [[ ${pids[1]} -gt 0 ]] then - wait ${PIDS[1]} + wait ${pids[1]} fi - # Wait for the MySQL certificate setup to complete - if [[ ${PIDS[2]} -gt 0 ]] + # Wait to complete certificate generation + if [[ ${pids[2]} -gt 0 ]] then - wait ${PIDS[2]} + wait ${pids[2]} fi # Finalise certificate setup @@ -780,7 +786,7 @@ function main () MySQL Details -------------------------------------------------------------------------------- database : ${OPTS_MYSQL_USER_DATABASE:-N/A} - ${DETAILS_MYSQL_USER_CREDENTIALS} + ${user_details} -------------------------------------------------------------------------------- ${timer_total} From 7b5381aecffab15df191365d6019162be2c18400 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Wed, 13 Mar 2019 13:04:08 +0000 Subject: [PATCH 56/98] #211: Define main function's local variables used for operator supplied values. --- src/usr/sbin/mysqld-bootstrap | 83 +++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index b557e33..782da0d 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -504,7 +504,16 @@ function main () local counter local datadir + local mysql_init_limit + local mysql_init_sql local mysql_init_template + local mysql_root_password + local mysql_root_password_hashed + local mysql_user + local mysql_user_database + local mysql_user_host + local mysql_user_password + local mysql_user_password_hashed local -a pids local server_key_path local user_details @@ -566,22 +575,22 @@ function main () MYSQL_USER_PASSWORD="$(< "${MYSQL_USER_PASSWORD}")" fi - OPTS_MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" - OPTS_MYSQL_INIT_SQL="${CUSTOM_MYSQL_INIT_SQL:-}" - OPTS_MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-"$( + mysql_init_limit="${MYSQL_INIT_LIMIT:-60}" + mysql_init_sql="${CUSTOM_MYSQL_INIT_SQL:-"-- Custom Initialisation SQL"}" + mysql_root_password="${MYSQL_ROOT_PASSWORD:-"$( __get_password )"}" - OPTS_MYSQL_ROOT_PASSWORD_HASHED="${MYSQL_ROOT_PASSWORD_HASHED:-false}" - OPTS_MYSQL_USER="${MYSQL_USER:-}" - OPTS_MYSQL_USER_DATABASE="${MYSQL_USER_DATABASE:-}" - OPTS_MYSQL_USER_HOST="$( + mysql_root_password_hashed="${MYSQL_ROOT_PASSWORD_HASHED:-false}" + mysql_user="${MYSQL_USER:-}" + mysql_user_database="${MYSQL_USER_DATABASE:-}" + mysql_user_host="$( __get_mysql_user_host \ "${MYSQL_SUBNET:-127.0.0.1}" )" - OPTS_MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-"$( + mysql_user_password="${MYSQL_USER_PASSWORD:-"$( __get_password )"}" - OPTS_MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" + mysql_user_password_hashed="${MYSQL_USER_PASSWORD_HASHED:-false}" printf -- \ "Initialising MySQL data directory.\n" @@ -605,27 +614,27 @@ function main () # Initialisation template output __get_mysql_init_template \ - --user="${OPTS_MYSQL_USER}" \ - --database="${OPTS_MYSQL_USER_DATABASE}" \ - --host="${OPTS_MYSQL_USER_HOST}" \ - --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ - --init-sql="${OPTS_MYSQL_INIT_SQL}" + --user="${mysql_user}" \ + --database="${mysql_user_database}" \ + --host="${mysql_user_host}" \ + --password-hashed="${mysql_user_password_hashed}" \ + --init-sql="${mysql_init_sql}" # Replace compact template placeholders with values mysql_init_template="$( __get_mysql_init_template \ - --user="${OPTS_MYSQL_USER}" \ - --database="${OPTS_MYSQL_USER_DATABASE}" \ - --host="${OPTS_MYSQL_USER_HOST}" \ - --password-hashed="${OPTS_MYSQL_USER_PASSWORD_HASHED}" \ - --init-sql="${OPTS_MYSQL_INIT_SQL}" \ + --user="${mysql_user}" \ + --database="${mysql_user_database}" \ + --host="${mysql_user_host}" \ + --password-hashed="${mysql_user_password_hashed}" \ + --init-sql="${mysql_init_sql}" \ --compact )" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER}}'/${OPTS_MYSQL_USER}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_DATABASE}}'/${OPTS_MYSQL_USER_DATABASE}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_HOST}}'/${OPTS_MYSQL_USER_HOST}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_PASSWORD}}'/${OPTS_MYSQL_USER_PASSWORD}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_ROOT_PASSWORD}}'/${OPTS_MYSQL_ROOT_PASSWORD}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER}}'/${mysql_user}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_DATABASE}}'/${mysql_user_database}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_HOST}}'/${mysql_user_host}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_USER_PASSWORD}}'/${mysql_user_password}}" + mysql_init_template="${mysql_init_template//'{{MYSQL_ROOT_PASSWORD}}'/${mysql_root_password}}" printf -- \ '%s\n' \ @@ -652,11 +661,11 @@ function main () & __configure_mysql_client_root_password \ - "${OPTS_MYSQL_ROOT_PASSWORD}" + "${mysql_root_password}" # Wait for initialisation to complete (poll for access) or timeout counter="$(( - 2 * OPTS_MYSQL_INIT_LIMIT + 2 * mysql_init_limit ))" until (( counter == 0 )) do @@ -665,10 +674,10 @@ function main () if __have_mysql_access then # Set the password if it was supplied pre-hashed. - if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] + if [[ ${mysql_root_password_hashed} == true ]] then mysql \ - -e "UPDATE mysql.user SET authentication_string = '${OPTS_MYSQL_ROOT_PASSWORD}' WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES;" + -e "UPDATE mysql.user SET authentication_string = '${mysql_root_password}' WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES;" rm -f \ /root/.{my,mylogin}.cnf @@ -684,7 +693,7 @@ function main () then >&2 printf \ -- "MySQL initilisation failed after %s seconds.\n" \ - "${OPTS_MYSQL_INIT_LIMIT}" + "${mysql_init_limit}" killall \ -15 \ @@ -696,7 +705,7 @@ function main () "Stopping MySQL.\n" # Prefer mysqladmin shutdown method if password is known - if [[ ${OPTS_MYSQL_ROOT_PASSWORD_HASHED} == true ]] + if [[ ${mysql_root_password_hashed} == true ]] then killall \ -w \ @@ -724,22 +733,22 @@ function main () localhost \ "${redacted_value}" - if [[ -n ${OPTS_MYSQL_USER} ]] \ - && [[ -n ${OPTS_MYSQL_USER_PASSWORD} ]] + if [[ -n ${mysql_user} ]] \ + && [[ -n ${mysql_user_password} ]] then # Redact operator supplied password if [[ -n ${MYSQL_USER_PASSWORD} ]] then - OPTS_MYSQL_USER_PASSWORD="${redacted_value}" + mysql_user_password="${redacted_value}" fi printf \ -v user_details \ -- "%s\nuser : %s@%s, password : %s" \ "${user_details}" \ - "${OPTS_MYSQL_USER}" \ - "${OPTS_MYSQL_USER_HOST}" \ - "${OPTS_MYSQL_USER_PASSWORD}" + "${mysql_user}" \ + "${mysql_user_host}" \ + "${mysql_user_password}" fi # Wait for the service user modifications to complete @@ -785,7 +794,7 @@ function main () ================================================================================ MySQL Details -------------------------------------------------------------------------------- - database : ${OPTS_MYSQL_USER_DATABASE:-N/A} + database : ${mysql_user_database:-N/A} ${user_details} -------------------------------------------------------------------------------- ${timer_total} From e1b6128d73fad0e6d63d26401473b51c89095e6b Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 15 Mar 2019 00:21:33 +0000 Subject: [PATCH 57/98] #211: Adds getter functions for operator supplied environment values. --- src/etc/mysqld-bootstrap.conf | 2 +- src/usr/sbin/mysqld-bootstrap | 308 +++++++++++++++++++++++++++++++--- 2 files changed, 283 insertions(+), 27 deletions(-) diff --git a/src/etc/mysqld-bootstrap.conf b/src/etc/mysqld-bootstrap.conf index 1ca0561..5a64123 100644 --- a/src/etc/mysqld-bootstrap.conf +++ b/src/etc/mysqld-bootstrap.conf @@ -17,4 +17,4 @@ MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" # ------------------------------------------------------------------------------ # Custom SQL run once during initialisation of the database tables # ------------------------------------------------------------------------------ -CUSTOM_MYSQL_INIT_SQL="-- Custom Initialisation SQL can be included in /etc/mysqld-bootstrap.conf" +MYSQL_INIT_SQL="-- Custom Initialisation SQL can be included in /etc/mysqld-bootstrap.conf" diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 782da0d..c0a6762 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -118,6 +118,34 @@ function __configure_mysql_client_root_password () INT TERM EXIT } +function __get_mysql_init_limit () +{ + local -r default_value="${1:-60}" + + local value="${MYSQL_INIT_LIMIT}" + + if ! __is_valid_mysql_init_limit "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + +function __get_mysql_init_sql () +{ + local -r default_value="${1:-"-- Custom Initialisation SQL"}" + + local value="${MYSQL_INIT_SQL}" + + if ! __is_valid_mysql_init_sql "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + function __get_mysql_init_template () { local compact="false" @@ -318,9 +346,88 @@ function __get_mysql_init_template () INT TERM EXIT } +function __get_mysql_root_password () +{ + local -r default_value="${1:-"$( + __get_password + )"}" + + local value="${MYSQL_ROOT_PASSWORD}" + + if [[ -f ${value} ]] + then + value="$(< "${value}")" + fi + + if ! __is_valid_mysql_root_password "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + +function __get_mysql_root_password_hashed () +{ + local -r default_value="${1:-false}" + + local value="${MYSQL_ROOT_PASSWORD_HASHED}" + + if ! __is_valid_mysql_root_password_hashed "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + +function __get_mysql_subnet () +{ + local -r default_value="${1:-127.0.0.1}" + + local value="${MYSQL_SUBNET}" + + if ! __is_valid_mysql_subnet "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + +function __get_mysql_user () +{ + local -r default_value="${1}" + + local value="${MYSQL_USER}" + + if ! __is_valid_mysql_user "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + +function __get_mysql_user_database () +{ + local -r default_value="${1}" + + local value="${MYSQL_USER_DATABASE}" + + if ! __is_valid_mysql_user_database "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + function __get_mysql_user_host () { - local -r client_subnet="${1:-127.0.0.1}" + local -r client_subnet="${1:-"$( + __get_mysql_subnet + )"}" local value @@ -342,6 +449,41 @@ function __get_mysql_user_host () printf -- '%s' "${value}" } +function __get_mysql_user_password () +{ + local -r default_value="${1:-"$( + __get_password + )"}" + + local value="${MYSQL_USER_PASSWORD}" + + if [[ -f ${value} ]] + then + value="$(< "${value}")" + fi + + if ! __is_valid_mysql_user_password "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + +function __get_mysql_user_password_hashed () +{ + local -r default_value="${1:-false}" + + local value="${MYSQL_USER_PASSWORD_HASHED}" + + if ! __is_valid_mysql_user_password_hashed "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + function __get_option () { local -r value="$( @@ -414,6 +556,120 @@ function __is_mysql_datadir_populated () return 1 } +function __is_valid_mysql_init_limit () +{ + local -r non_zero_integer='^[1-9][0-9]*$' + local -r value="${1}" + + if [[ ${value} =~ ${non_zero_integer} ]] + then + return 0 + fi + + return 1 +} + +function __is_valid_mysql_init_sql () +{ + local -r value="${1}" + + if ! [[ -z ${value} ]] + then + return 0 + fi + + return 1 +} + +function __is_valid_mysql_root_password () +{ + local -r value="${1}" + + if ! [[ -z ${value} ]] + then + return 0 + fi + + return 1 +} + +function __is_valid_mysql_root_password_hashed () +{ + local -r boolean_value='^(true|false)$' + local -r value="${1}" + + if [[ ${value} =~ ${boolean_value} ]] + then + return 0 + fi + + return 1 +} + +function __is_valid_mysql_subnet () +{ + local -r subnet_value='^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})?$' + local -r value="${1}" + + if [[ ${value} =~ ${subnet_value} ]] + then + return 0 + fi + + return 1 +} + +function __is_valid_mysql_user () +{ + local -r user_value='^.{1,32}$' + local -r value="${1}" + + if [[ ${value} =~ ${user_value} ]] + then + return 0 + fi + + return 1 +} + +function __is_valid_mysql_user_database () +{ + local -r database_value='^[^\./\\]{0,63}[^\./\\ ]$' + local -r value="${1}" + + if [[ ${value} =~ ${database_value} ]] + then + return 0 + fi + + return 1 +} + +function __is_valid_mysql_user_password () +{ + local -r value="${1}" + + if ! [[ -z ${value} ]] + then + return 0 + fi + + return 1 +} + +function __is_valid_mysql_user_password_hashed () +{ + local -r boolean_value='^(true|false)$' + local -r value="${1}" + + if [[ ${value} =~ ${boolean_value} ]] + then + return 0 + fi + + return 1 +} + function __map_mysql_service_user_to_datadir () { local -r datadir="${1:-"$( @@ -564,33 +820,33 @@ function main () pids[2]="${!}" fi - # Get passwords from file if applicable - if [[ -s ${MYSQL_ROOT_PASSWORD} ]] - then - MYSQL_ROOT_PASSWORD="$(< "${MYSQL_ROOT_PASSWORD}")" - fi - - if [[ -s ${MYSQL_USER_PASSWORD} ]] - then - MYSQL_USER_PASSWORD="$(< "${MYSQL_USER_PASSWORD}")" - fi - - mysql_init_limit="${MYSQL_INIT_LIMIT:-60}" - mysql_init_sql="${CUSTOM_MYSQL_INIT_SQL:-"-- Custom Initialisation SQL"}" - mysql_root_password="${MYSQL_ROOT_PASSWORD:-"$( - __get_password - )"}" - mysql_root_password_hashed="${MYSQL_ROOT_PASSWORD_HASHED:-false}" - mysql_user="${MYSQL_USER:-}" - mysql_user_database="${MYSQL_USER_DATABASE:-}" + mysql_init_limit="$( + __get_mysql_init_limit + )" + mysql_init_sql="$( + __get_mysql_init_sql + )" + mysql_root_password="$( + __get_mysql_root_password + )" + mysql_root_password_hashed="$( + __get_mysql_root_password_hashed + )" + mysql_user="$( + __get_mysql_user + )" + mysql_user_database="$( + __get_mysql_user_database + )" mysql_user_host="$( - __get_mysql_user_host \ - "${MYSQL_SUBNET:-127.0.0.1}" + __get_mysql_user_host + )" + mysql_user_password="$( + __get_mysql_user_password + )" + mysql_user_password_hashed="$( + __get_mysql_user_password_hashed )" - mysql_user_password="${MYSQL_USER_PASSWORD:-"$( - __get_password - )"}" - mysql_user_password_hashed="${MYSQL_USER_PASSWORD_HASHED:-false}" printf -- \ "Initialising MySQL data directory.\n" From 08b91af50971345c9cb75c727d1453db13484732 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 15 Mar 2019 00:37:04 +0000 Subject: [PATCH 58/98] #211: Reduces display width where possible. --- src/usr/sbin/mysqld-bootstrap | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index c0a6762..e1fb159 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -760,9 +760,9 @@ function main () local counter local datadir + local mysql_init local mysql_init_limit local mysql_init_sql - local mysql_init_template local mysql_root_password local mysql_root_password_hashed local mysql_user @@ -877,7 +877,7 @@ function main () --init-sql="${mysql_init_sql}" # Replace compact template placeholders with values - mysql_init_template="$( + mysql_init="$( __get_mysql_init_template \ --user="${mysql_user}" \ --database="${mysql_user_database}" \ @@ -886,15 +886,15 @@ function main () --init-sql="${mysql_init_sql}" \ --compact )" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER}}'/${mysql_user}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_DATABASE}}'/${mysql_user_database}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_HOST}}'/${mysql_user_host}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_USER_PASSWORD}}'/${mysql_user_password}}" - mysql_init_template="${mysql_init_template//'{{MYSQL_ROOT_PASSWORD}}'/${mysql_root_password}}" + mysql_init="${mysql_init//'{{MYSQL_USER}}'/${mysql_user}}" + mysql_init="${mysql_init//'{{MYSQL_USER_DATABASE}}'/${mysql_user_database}}" + mysql_init="${mysql_init//'{{MYSQL_USER_HOST}}'/${mysql_user_host}}" + mysql_init="${mysql_init//'{{MYSQL_USER_PASSWORD}}'/${mysql_user_password}}" + mysql_init="${mysql_init//'{{MYSQL_ROOT_PASSWORD}}'/${mysql_root_password}}" printf -- \ '%s\n' \ - "${mysql_init_template}" \ + "${mysql_init}" \ > /tmp/mysql-init trap \ @@ -933,7 +933,11 @@ function main () if [[ ${mysql_root_password_hashed} == true ]] then mysql \ - -e "UPDATE mysql.user SET authentication_string = '${mysql_root_password}' WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES;" + -e "UPDATE mysql.user \ + SET authentication_string = '${mysql_root_password}' \ + WHERE User = 'root' \ + AND Host = 'localhost'; \ + FLUSH PRIVILEGES;" rm -f \ /root/.{my,mylogin}.cnf @@ -1043,7 +1047,8 @@ function main () }' )" - # Send to log file until user password can be stored in configuration file. + # Send to log file until user password can be stored in configuration + # file. tee /var/log/mysqld.log \ <<-EOT From d299af0b2800ba1fa578a4381ea8152cb36269d0 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 15 Mar 2019 12:22:54 +0000 Subject: [PATCH 59/98] #211: Adds --verbose option to bootstrap script + fix a couple of error messages. --- src/etc/supervisord.d/mysqld-bootstrap.conf | 2 +- src/usr/sbin/mysqld-bootstrap | 188 +++++++++++++------- 2 files changed, 125 insertions(+), 65 deletions(-) diff --git a/src/etc/supervisord.d/mysqld-bootstrap.conf b/src/etc/supervisord.d/mysqld-bootstrap.conf index 83c4bb3..fedd173 100644 --- a/src/etc/supervisord.d/mysqld-bootstrap.conf +++ b/src/etc/supervisord.d/mysqld-bootstrap.conf @@ -1,6 +1,6 @@ [program:mysqld-bootstrap] priority = 6 -command = /usr/sbin/mysqld-bootstrap +command = /usr/sbin/mysqld-bootstrap --verbose autostart = %(ENV_MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP)s startsecs = 0 startretries = 0 diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index e1fb159..5cc3e8c 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -29,7 +29,7 @@ function __configure_mysql_client_root_password () >&2 printf -- \ '%s %s %s\n' \ "ERROR: Failed to create directory" \ - "${fifo_path}" + "${fifo_path}" \ "- aborting." exit 1 fi @@ -214,7 +214,7 @@ function __get_mysql_init_template () >&2 printf -- \ '%s %s %s\n' \ "ERROR: Failed to create directory" \ - "${fifo_path}" + "${fifo_path}" \ "- aborting." exit 1 fi @@ -773,11 +773,23 @@ function main () local -a pids local server_key_path local user_details + local verbose="false" # Create lock touch \ "${lock_file}" + # Parse options + while [[ "${#}" -gt 0 ]] + do + case "${1}" in + -v|--verbose) + verbose="true" + shift 1 + ;; + esac + done + datadir="$( __get_option \ mysqld \ @@ -792,8 +804,11 @@ function main () if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ && [[ ! -e ${server_key} ]] then - printf -- \ - "Generating MySQL certificates.\n" + if [[ ${verbose} == true ]] + then + printf -- \ + "Generating MySQL certificates.\n" + fi server_key_path="$( mktemp -d )" @@ -803,7 +818,7 @@ function main () >&2 printf -- \ '%s %s %s\n' \ "ERROR: Failed to create directory" \ - "${server_key_path}" + "${server_key_path}" \ "- aborting." exit 1 fi @@ -838,18 +853,25 @@ function main () mysql_user_database="$( __get_mysql_user_database )" - mysql_user_host="$( - __get_mysql_user_host - )" - mysql_user_password="$( - __get_mysql_user_password - )" - mysql_user_password_hashed="$( - __get_mysql_user_password_hashed - )" + # User dependent + if [[ -n ${mysql_user} ]] + then + mysql_user_host="$( + __get_mysql_user_host + )" + mysql_user_password="$( + __get_mysql_user_password + )" + mysql_user_password_hashed="$( + __get_mysql_user_password_hashed + )" + fi - printf -- \ - "Initialising MySQL data directory.\n" + if [[ ${verbose} == true ]] + then + printf -- \ + "Initialising MySQL data directory.\n" + fi ${mysqld} \ --datadir="${datadir}" \ --initialize-insecure \ @@ -869,12 +891,15 @@ function main () pids[1]="${!}" # Initialisation template output - __get_mysql_init_template \ - --user="${mysql_user}" \ - --database="${mysql_user_database}" \ - --host="${mysql_user_host}" \ - --password-hashed="${mysql_user_password_hashed}" \ - --init-sql="${mysql_init_sql}" + if [[ ${verbose} == true ]] + then + __get_mysql_init_template \ + --user="${mysql_user}" \ + --database="${mysql_user_database}" \ + --host="${mysql_user_host}" \ + --password-hashed="${mysql_user_password_hashed}" \ + --init-sql="${mysql_init_sql}" + fi # Replace compact template placeholders with values mysql_init="$( @@ -908,8 +933,11 @@ function main () wait ${pids[0]} fi - printf -- \ - "Initialising MySQL.\n" + if [[ ${verbose} == true ]] + then + printf -- \ + "Initialising MySQL.\n" + fi ${mysqld} \ --pid-file=/var/run/mysqld/mysqld.pid \ --skip-networking \ @@ -961,8 +989,11 @@ function main () exit 1 else - printf -- \ - "Stopping MySQL.\n" + if [[ ${verbose} == true ]] + then + printf -- \ + "Stopping MySQL.\n" + fi # Prefer mysqladmin shutdown method if password is known if [[ ${mysql_root_password_hashed} == true ]] @@ -986,15 +1017,17 @@ function main () fi # Local root user details - printf \ - -v user_details \ - -- "user : %s@%s, password : %s" \ - root \ - localhost \ - "${redacted_value}" - - if [[ -n ${mysql_user} ]] \ - && [[ -n ${mysql_user_password} ]] + if [[ ${verbose} == true ]] + then + printf \ + -v user_details \ + -- "user : %s@%s, password : %s" \ + root \ + localhost \ + "${redacted_value}" + fi + + if [[ -n ${mysql_user} ]] then # Redact operator supplied password if [[ -n ${MYSQL_USER_PASSWORD} ]] @@ -1002,13 +1035,23 @@ function main () mysql_user_password="${redacted_value}" fi - printf \ - -v user_details \ - -- "%s\nuser : %s@%s, password : %s" \ - "${user_details}" \ - "${mysql_user}" \ - "${mysql_user_host}" \ - "${mysql_user_password}" + if [[ ${verbose} == true ]] + then + printf \ + -v user_details \ + -- "%s\nuser : %s@%s, password : %s" \ + "${user_details}" \ + "${mysql_user}" \ + "${mysql_user_host}" \ + "${mysql_user_password}" + else + printf \ + -v user_details \ + -- "user : %s@%s, password : %s" \ + "${mysql_user}" \ + "${mysql_user_host}" \ + "${mysql_user_password}" + fi fi # Wait for the service user modifications to complete @@ -1036,31 +1079,48 @@ function main () INT TERM EXIT fi - timer_total="$( - awk \ - -v timer_end="$( - date +%s.%N - )" \ - -v timer_start="${timer_start}" \ - 'BEGIN { print \ - timer_end - timer_start; - }' - )" + if [[ ${verbose} == true ]] + then + timer_total="$( + awk \ + -v timer_end="$( + date +%s.%N + )" \ + -v timer_start="${timer_start}" \ + 'BEGIN { print \ + timer_end - timer_start; + }' + )" - # Send to log file until user password can be stored in configuration - # file. - tee /var/log/mysqld.log \ - <<-EOT + # Send to log file until user password can be stored in + # configuration file. + tee /var/log/mysqld.log \ + <<-EOT + + ================================================================================ + MySQL Details + -------------------------------------------------------------------------------- + database : ${mysql_user_database:-N/A} + ${user_details} + -------------------------------------------------------------------------------- + ${timer_total} + + EOT + elif [[ -n ${mysql_user} ]] \ + && [[ ${mysql_user_password} != "${redacted_value}" ]] + then + # Mininal ouput when required + tee /var/log/mysqld.log \ + <<-EOT - ================================================================================ - MySQL Details - -------------------------------------------------------------------------------- - database : ${mysql_user_database:-N/A} - ${user_details} - -------------------------------------------------------------------------------- - ${timer_total} + ================================================================================ + MySQL Details + -------------------------------------------------------------------------------- + ${user_details} + -------------------------------------------------------------------------------- - EOT + EOT + fi fi # Release lock From 097237635bb7e7f460ba2074f28528a39988a80e Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 15 Mar 2019 18:46:02 +0000 Subject: [PATCH 60/98] #211: Removes output of user password to container logs (not necessary). --- src/usr/sbin/mysqld-bootstrap | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 5cc3e8c..e2103f5 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -1092,9 +1092,7 @@ function main () }' )" - # Send to log file until user password can be stored in - # configuration file. - tee /var/log/mysqld.log \ + cat \ <<-EOT ================================================================================ @@ -1106,11 +1104,9 @@ function main () ${timer_total} EOT - elif [[ -n ${mysql_user} ]] \ - && [[ ${mysql_user_password} != "${redacted_value}" ]] + elif [[ ${mysql_user_password} != "${redacted_value}" ]] then - # Mininal ouput when required - tee /var/log/mysqld.log \ + cat \ <<-EOT ================================================================================ From 81d9d9dc77951cc137104e9f2265217b6b77f78d Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 15 Mar 2019 19:10:20 +0000 Subject: [PATCH 61/98] #211: Improves readability of initialisation SQL template. --- src/usr/sbin/mysqld-bootstrap | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index e2103f5..bdb46d5 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -314,10 +314,9 @@ function __get_mysql_init_template () ${template_privileges} GRANT ALL PRIVILEGES ON *.* - TO 'root'@'localhost' - IDENTIFIED BY '{{MYSQL_ROOT_PASSWORD}}' - WITH GRANT OPTION; + TO 'root'@'localhost' IDENTIFIED BY '{{MYSQL_ROOT_PASSWORD}}' WITH GRANT OPTION; FLUSH PRIVILEGES; + -- ----------------------------------------------------------------------------- EOT pids[2]="${!}" From 6f729319b6a7f7da4f73115a320deb2f03a45ca9 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 15 Mar 2019 21:25:35 +0000 Subject: [PATCH 62/98] #211: Adds functions for initialisation. --- src/usr/sbin/mysqld-bootstrap | 86 +++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index bdb46d5..990071b 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -537,6 +537,52 @@ function __have_mysql_access () return 1 } +function __init_datadir () +{ + local -r directory="${1:-"$( + __get_option \ + mysqld \ + datadir \ + "/var/lib/mysql" + )"}" + + if command -v mysqld_safe &> /dev/null \ + && command -v mysql_install_db &> /dev/null + then + mysql_install_db \ + --force \ + --skip-name-resolve \ + --skip-networking \ + --tmpdir="${directory}" + else + mysqld \ + --datadir="${directory}" \ + --initialize-insecure \ + --pid-file=/var/run/mysqld/mysqld.pid \ + --skip-name-resolve \ + --skip-networking \ + --tmpdir="${directory}" \ + --user=mysql + fi +} + +function __init_mysql () +{ + local -r init_file="${1:-/tmp/mysql-init}" + + if command -v mysqld_safe &> /dev/null + then + mysqld_safe \ + --skip-networking \ + --init-file="${init_file}" + else + mysqld \ + --pid-file=/var/run/mysqld/mysqld.pid \ + --skip-networking \ + --init-file="${init_file}" + fi +} + function __is_mysql_datadir_populated () { local -r directory="${1:-"$( @@ -749,8 +795,8 @@ function __map_mysql_service_user_to_datadir () function main () { + local -r init_file="/tmp/mysql-init" local -r lock_file="/var/lock/subsys/mysqld-bootstrap" - local -r mysqld="/usr/sbin/mysqld" local -r redacted_value="********" local -r server_key="${datadir}"/server-key.pem local -r timer_start="$( @@ -759,7 +805,7 @@ function main () local counter local datadir - local mysql_init + local init_template local mysql_init_limit local mysql_init_sql local mysql_root_password @@ -871,14 +917,8 @@ function main () printf -- \ "Initialising MySQL data directory.\n" fi - ${mysqld} \ - --datadir="${datadir}" \ - --initialize-insecure \ - --pid-file=/var/run/mysqld/mysqld.pid \ - --skip-name-resolve \ - --skip-networking \ - --tmpdir="${datadir}" \ - --user=mysql \ + __init_datadir \ + "${datadir}" \ & pids[0]="${!}" @@ -901,7 +941,7 @@ function main () fi # Replace compact template placeholders with values - mysql_init="$( + init_template="$( __get_mysql_init_template \ --user="${mysql_user}" \ --database="${mysql_user_database}" \ @@ -910,20 +950,20 @@ function main () --init-sql="${mysql_init_sql}" \ --compact )" - mysql_init="${mysql_init//'{{MYSQL_USER}}'/${mysql_user}}" - mysql_init="${mysql_init//'{{MYSQL_USER_DATABASE}}'/${mysql_user_database}}" - mysql_init="${mysql_init//'{{MYSQL_USER_HOST}}'/${mysql_user_host}}" - mysql_init="${mysql_init//'{{MYSQL_USER_PASSWORD}}'/${mysql_user_password}}" - mysql_init="${mysql_init//'{{MYSQL_ROOT_PASSWORD}}'/${mysql_root_password}}" + init_template="${init_template//'{{MYSQL_USER}}'/${mysql_user}}" + init_template="${init_template//'{{MYSQL_USER_DATABASE}}'/${mysql_user_database}}" + init_template="${init_template//'{{MYSQL_USER_HOST}}'/${mysql_user_host}}" + init_template="${init_template//'{{MYSQL_USER_PASSWORD}}'/${mysql_user_password}}" + init_template="${init_template//'{{MYSQL_ROOT_PASSWORD}}'/${mysql_root_password}}" printf -- \ '%s\n' \ - "${mysql_init}" \ - > /tmp/mysql-init + "${init_template}" \ + > "${init_file}" trap \ "rm -rf \"${server_key_path}\"; \ - rm -f /tmp/mysql-init;" \ + rm -f "${init_file}";" \ INT TERM EXIT # Wait to complete system table installation @@ -937,10 +977,8 @@ function main () printf -- \ "Initialising MySQL.\n" fi - ${mysqld} \ - --pid-file=/var/run/mysqld/mysqld.pid \ - --skip-networking \ - --init-file=/tmp/mysql-init \ + __init_mysql \ + "${init_file}" \ & __configure_mysql_client_root_password \ @@ -1008,7 +1046,7 @@ function main () fi rm -f \ - /tmp/mysql-init + "${init_file}" trap \ "rm -rf \"${server_key_path}\";" \ From 09bd807b81502f45f05800495adb24afc9b7fc38 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 15 Mar 2019 23:00:51 +0000 Subject: [PATCH 63/98] #212: Adds version specific methods for setting root password from hash. --- src/usr/sbin/mysqld-bootstrap | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 990071b..8b1c57b 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -997,12 +997,20 @@ function main () # Set the password if it was supplied pre-hashed. if [[ ${mysql_root_password_hashed} == true ]] then - mysql \ - -e "UPDATE mysql.user \ - SET authentication_string = '${mysql_root_password}' \ - WHERE User = 'root' \ - AND Host = 'localhost'; \ - FLUSH PRIVILEGES;" + if command -v mysqld_safe &> /dev/null + then + # MySQL version < 5.7 + mysql \ + -e "SET PASSWORD = '${mysql_root_password}'" + else + # MySQL version >= 5.7 + mysql \ + -e "UPDATE mysql.user \ + SET authentication_string = '${mysql_root_password}' \ + WHERE User = 'root' \ + AND Host = 'localhost'; \ + FLUSH PRIVILEGES;" + fi rm -f \ /root/.{my,mylogin}.cnf From ee53b528efaebe42f10f1db13c39d63b5714beb0 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Mar 2019 01:07:27 +0000 Subject: [PATCH 64/98] #234: Adds improved basic initialisation SQL feature. --- CHANGELOG.md | 5 +- Dockerfile | 4 +- README.md | 32 ++++- default.mk | 2 + environment.mk | 2 + images/docker-logs-mysqld-bootstrap.png | Bin 0 -> 95359 bytes src/etc/mysqld-bootstrap.conf | 20 --- .../systemd/system/centos-ssh-mysql@.service | 4 + src/opt/scmi/default.sh | 2 + src/opt/scmi/environment.sh | 2 + src/opt/scmi/service-unit.sh | 2 + src/usr/sbin/mysqld-bootstrap | 19 ++- src/usr/sbin/mysqld-wrapper | 2 - test/shpec/operation_shpec.sh | 125 ++++++++++++++++++ 14 files changed, 187 insertions(+), 34 deletions(-) create mode 100644 images/docker-logs-mysqld-bootstrap.png delete mode 100644 src/etc/mysqld-bootstrap.conf diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a21f1..fd5bdd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,11 @@ CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. - Adds improvement to pull logic in systemd unit install template. - Adds `SSH_AUTOSTART_SUPERVISOR_STDOUT` with a value "false", disabling startup of `supervisor_stdout`. - Adds improved `healtchcheck`, `sshd-bootstrap` and `sshd-wrapper` scripts. +- Adds `MYSQL_INIT_LIMIT` with a default value of "60" seconds. +- Adds `MYSQL_INIT_SQL` with a default empty value "". +- Deprecates `CUSTOM_MYSQL_INIT_SQL`, use `MYSQL_INIT_SQL` instead. - Removes use of `/etc/services-config` paths. -- Removes code from configuration file `/etc/mysqld-bootstrap.conf`. +- Removes use of `/etc/mysqld-bootstrap.conf`. - Removes X-Fleet section from etcd register template unit-file. - Removes the unused group element from the default container name. - Removes the node element from the default container name. diff --git a/Dockerfile b/Dockerfile index dfbcc0c..9b87666 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ RUN sed -i \ -e "s~{{RELEASE_VERSION}}~${RELEASE_VERSION}~g" \ /etc/systemd/system/centos-ssh-mysql@.service \ && chmod 600 \ - /etc/{my.cnf,mysqld-bootstrap.conf} \ + /etc/my.cnf \ && chmod 644 \ /etc/supervisord.d/mysqld-{bootstrap,wrapper}.conf \ && chmod 700 \ @@ -59,6 +59,8 @@ EXPOSE 3306 # ----------------------------------------------------------------------------- ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="true" \ MYSQL_AUTOSTART_MYSQLD_WRAPPER="true" \ + MYSQL_INIT_LIMIT="60" \ + MYSQL_INIT_SQL="" \ MYSQL_ROOT_PASSWORD="" \ MYSQL_ROOT_PASSWORD_HASHED="false" \ MYSQL_SUBNET="127.0.0.1" \ diff --git a/README.md b/README.md index 8ca753b..c165a90 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,7 @@ $ docker logs mysql.1 On the first run, there will be additional output showing the initialisation SQL template and, before mysqld-bootstrap completes, the MySQL Details which shows the configured database, if applicable, and any associated user credentials. -![Docker Logs - MySQL Initialisation SQL Template](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-7-mysql57-community/images/docker-logs-mysqld-bootstrap-initialisation-sql.png) - -![Docker Logs - MySQL Details](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-7-mysql57-community/images/docker-logs-mysqld-bootstrap-details.png) +![Docker Logs - MySQL Bootstrap](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-7-mysql57-community/images/docker-logs-mysqld-bootstrap.png) The MySQL table data is persistent across container restarts by setting the MySQL data directory `/var/lib/mysql` as a data volume. We didn't specify a name or docker_host path so Docker will give it a unique name and store it in `/var/lib/docker/volumes/`; to find out where the data is stored on the Docker host you can use `docker inspect`. @@ -289,6 +287,34 @@ It may be desirable to prevent the startup of the mysqld-bootstrap and/or mysqld ... ``` +##### MYSQL_INIT_LIMIT + +The default timeout for MySQL initialisation is 60 seconds. Use `MYSQL_INIT_LIMIT` to change this value when necessary. + +``` +... + --env "MYSQL_INIT_LIMIT=120" \ +... +``` + +##### MYSQL_INIT_SQL + +To add custom SQL to the MySQL intitialisation use `MYSQL_INIT_SQL` where the following placeholders are will get replaced with the appropriate values: + +- `{{MYSQL_ROOT_PASSWORD}}` +- `{{MYSQL_USER}}` +- `{{MYSQL_USER_DATABASE}}` +- `{{MYSQL_USER_HOST}}` +- `{{MYSQL_USER_PASSWORD}}` + +*Note:* The backtick "\`" character will need escaping as show in the example. + +``` +... + --env "MYSQL_INIT=CREATE DATABASE \`{{MYSQL_USER_DATABASE}}-1\`; GRANT ALL PRIVILEGES ON \`{{MYSQL_USER_DATABASE}}-%\`.* TO '{{MYSQL_USER}}'@'{{MYSQL_USER_HOST}}';" \ +... +``` + ##### MYSQL_ROOT_PASSWORD On first run the root user is created with an auto-generated password. If you require a specific password, `MYSQL_ROOT_PASSWORD` can be used when running the container. diff --git a/default.mk b/default.mk index 0e3c14c..c28d252 100644 --- a/default.mk +++ b/default.mk @@ -43,6 +43,8 @@ define DOCKER_CONTAINER_PARAMETERS --restart $(DOCKER_RESTART_POLICY) \ --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=$(MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP)" \ --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=$(MYSQL_AUTOSTART_MYSQLD_WRAPPER)" \ +--env "MYSQL_INIT_LIMIT=$(MYSQL_INIT_LIMIT)" \ +--env "MYSQL_INIT_SQL=$(MYSQL_INIT_SQL)" \ --env "MYSQL_ROOT_PASSWORD=$(MYSQL_ROOT_PASSWORD)" \ --env "MYSQL_ROOT_PASSWORD_HASHED=$(MYSQL_ROOT_PASSWORD_HASHED)" \ --env "MYSQL_SUBNET=$(MYSQL_SUBNET)" \ diff --git a/environment.mk b/environment.mk index 38a7ddd..b5e8b9e 100644 --- a/environment.mk +++ b/environment.mk @@ -34,6 +34,8 @@ STARTUP_TIME ?= 10 # ------------------------------------------------------------------------------ MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP ?= true MYSQL_AUTOSTART_MYSQLD_WRAPPER ?= true +MYSQL_INIT_LIMIT ?= 60 +MYSQL_INIT_SQL ?= MYSQL_ROOT_PASSWORD ?= MYSQL_ROOT_PASSWORD_HASHED ?= false MYSQL_SUBNET ?= 127.0.0.1 diff --git a/images/docker-logs-mysqld-bootstrap.png b/images/docker-logs-mysqld-bootstrap.png new file mode 100644 index 0000000000000000000000000000000000000000..cea54c93ce377750e1f6335a4da798a8ec958614 GIT binary patch literal 95359 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 literal 0 HcmV?d00001 diff --git a/src/etc/mysqld-bootstrap.conf b/src/etc/mysqld-bootstrap.conf deleted file mode 100644 index 5a64123..0000000 --- a/src/etc/mysqld-bootstrap.conf +++ /dev/null @@ -1,20 +0,0 @@ -# Set the time to wait for the MySQL initialisation process before exiting. -MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" - -# ------------------------------------------------------------------------------ -# Application container configuration -# ------------------------------------------------------------------------------ -MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP:-true}" -MYSQL_AUTOSTART_MYSQLD_WRAPPER="${MYSQL_AUTOSTART_MYSQLD_WRAPPER:-true}" -MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" -MYSQL_ROOT_PASSWORD_HASHED="${MYSQL_ROOT_PASSWORD_HASHED:-false}" -MYSQL_SUBNET="${MYSQL_SUBNET:-127.0.0.1}" -MYSQL_USER="${MYSQL_USER:-}" -MYSQL_USER_DATABASE="${MYSQL_USER_DATABASE:-}" -MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-}" -MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" - -# ------------------------------------------------------------------------------ -# Custom SQL run once during initialisation of the database tables -# ------------------------------------------------------------------------------ -MYSQL_INIT_SQL="-- Custom Initialisation SQL can be included in /etc/mysqld-bootstrap.conf" diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index 5413ddb..a37d016 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -58,6 +58,8 @@ Environment="DOCKER_PORT_MAP_TCP_3306=3306" Environment="DOCKER_USER=jdeathe" Environment="MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true" Environment="MYSQL_AUTOSTART_MYSQLD_WRAPPER=true" +Environment="MYSQL_INIT_LIMIT=" +Environment="MYSQL_INIT_SQL=" Environment="MYSQL_ROOT_PASSWORD=" Environment="MYSQL_ROOT_PASSWORD_HASHED=false" Environment="MYSQL_SUBNET=127.0.0.1" @@ -123,6 +125,8 @@ ExecStart=/bin/bash -c \ --name %p.%i \ --env \"MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP}\" \ --env \"MYSQL_AUTOSTART_MYSQLD_WRAPPER=${MYSQL_AUTOSTART_MYSQLD_WRAPPER}\" \ + --env \"MYSQL_INIT_LIMIT=${MYSQL_INIT_LIMIT}\" \ + --env \"MYSQL_INIT_SQL=${MYSQL_INIT_SQL}\" \ --env \"MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}\" \ --env \"MYSQL_ROOT_PASSWORD_HASHED=${MYSQL_ROOT_PASSWORD_HASHED}\" \ --env \"MYSQL_SUBNET=${MYSQL_SUBNET}\" \ diff --git a/src/opt/scmi/default.sh b/src/opt/scmi/default.sh index 6309adc..6e91444 100644 --- a/src/opt/scmi/default.sh +++ b/src/opt/scmi/default.sh @@ -48,6 +48,8 @@ DOCKER_CONTAINER_PARAMETERS="--name ${DOCKER_NAME} \ --restart ${DOCKER_RESTART_POLICY} \ --env \"MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP}\" \ --env \"MYSQL_AUTOSTART_MYSQLD_WRAPPER=${MYSQL_AUTOSTART_MYSQLD_WRAPPER}\" \ +--env \"MYSQL_INIT_LIMIT=${MYSQL_INIT_LIMIT}\" \ +--env \"MYSQL_INIT_SQL=${MYSQL_INIT_SQL}\" \ --env \"MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}\" \ --env \"MYSQL_ROOT_PASSWORD_HASHED=${MYSQL_ROOT_PASSWORD_HASHED}\" \ --env \"MYSQL_SUBNET=${MYSQL_SUBNET}\" \ diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index 0c73f6f..ee1ff8d 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -38,6 +38,8 @@ REGISTER_UPDATE_INTERVAL="${REGISTER_UPDATE_INTERVAL:-95}" # ------------------------------------------------------------------------------ MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP:-true}" MYSQL_AUTOSTART_MYSQLD_WRAPPER="${MYSQL_AUTOSTART_MYSQLD_WRAPPER:-true}" +MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" +MYSQL_INIT_SQL="${MYSQL_INIT_SQL:-}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" MYSQL_ROOT_PASSWORD_HASHED="${MYSQL_ROOT_PASSWORD_HASHED:-false}" MYSQL_SUBNET="${MYSQL_SUBNET:-127.0.0.1}" diff --git a/src/opt/scmi/service-unit.sh b/src/opt/scmi/service-unit.sh index 24accaf..d2f2b29 100644 --- a/src/opt/scmi/service-unit.sh +++ b/src/opt/scmi/service-unit.sh @@ -8,6 +8,8 @@ readonly SERVICE_UNIT_ENVIRONMENT_KEYS=" DOCKER_PORT_MAP_TCP_3306 MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP MYSQL_AUTOSTART_MYSQLD_WRAPPER + MYSQL_INIT_LIMIT + MYSQL_INIT_SQL MYSQL_ROOT_PASSWORD MYSQL_ROOT_PASSWORD_HASHED MYSQL_SUBNET diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 8b1c57b..0174813 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source /etc/mysqld-bootstrap.conf - # Set MySQL client root user password # - Prefer to store encrypted via mysql_config_editor. # - Fallback to store as plaintext in secured file. @@ -136,7 +134,8 @@ function __get_mysql_init_sql () { local -r default_value="${1:-"-- Custom Initialisation SQL"}" - local value="${MYSQL_INIT_SQL}" + # CUSTOM_MYSQL_INIT_SQL is deprecated, use MYSQL_INIT_SQL + local value="${MYSQL_INIT_SQL:-"${CUSTOM_MYSQL_INIT_SQL}"}" if ! __is_valid_mysql_init_sql "${value}" then @@ -246,7 +245,7 @@ function __get_mysql_init_template () printf \ -v template_user \ - -- "%s\nCREATE USER '%s'@'%s' \n%s '%s';" \ + -- "%s\nCREATE USER '%s'@'%s'\n%s '%s';" \ "${template_user}" \ "{{MYSQL_USER}}" \ "{{MYSQL_USER_HOST}}" \ @@ -260,7 +259,7 @@ function __get_mysql_init_template () then printf \ -v template_privileges \ - -- "%s\nGRANT %s \nON \`%s\`.* \nTO '%s'@'%s';" \ + -- "%s\nGRANT %s\nON \`%s\`.*\nTO '%s'@'%s';" \ "${template_privileges}" \ "ALL PRIVILEGES" \ "{{MYSQL_USER_DATABASE}}" \ @@ -268,6 +267,12 @@ function __get_mysql_init_template () "{{MYSQL_USER_HOST}}" fi + # Each statement must be on a single line and should not include comments. + # Append a newline character to SQL line ending characters ";". + init_sql="${init_sql//; /;}" + init_sql="${init_sql//;/;$'\n'}" + init_sql="${init_sql/%$'\n'}" + # Listen for the template and output as required if [[ ${compact} == false ]] then @@ -312,8 +317,8 @@ function __get_mysql_init_template () -- Custom Initialisation SQL end -- ----------------------------------------------------------------------------- ${template_privileges} - GRANT ALL PRIVILEGES - ON *.* + GRANT ALL PRIVILEGES + ON *.* TO 'root'@'localhost' IDENTIFIED BY '{{MYSQL_ROOT_PASSWORD}}' WITH GRANT OPTION; FLUSH PRIVILEGES; -- ----------------------------------------------------------------------------- diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index 57c6fc3..49a8fda 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -2,8 +2,6 @@ set -e -source /etc/mysqld-bootstrap.conf - function __is_valid_mysql_autostart_mysqld_bootstrap () { local -r boolean_value='^(true|false)$' diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 8b1859a..730fd88 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -11,6 +11,7 @@ function __destroy () local -r private_network_2="bridge_internal_2" local -r data_volume_1="mysql.2.data-mysql" local -r data_volume_2="mysql.4.data-mysql" + local -r data_volume_3="mysql.6.data-mysql" # Destroy the bridge networks if [[ -n $(docker network ls -q -f name="${private_network_1}") ]]; then @@ -37,6 +38,12 @@ function __destroy () ${data_volume_2} \ &> /dev/null fi + + if [[ -n $(docker volume ls -q -f name="${data_volume_3}") ]]; then + docker volume rm \ + ${data_volume_3} \ + &> /dev/null + fi } function __get_container_port () @@ -98,10 +105,27 @@ function __reset_data_volume () { local -r data_volume_1="mysql.2.data-mysql" local -r data_volume_2="mysql.4.data-mysql" + local -r data_volume_3="mysql.6.data-mysql" local group="${1:-1}" case "${group}" in + 3) + # Destroy the data volume + if [[ -n $(docker volume ls -q -f name="${data_volume_3}") ]]; then + docker volume rm \ + ${data_volume_3} \ + &> /dev/null + fi + + # Create the data volume + if [[ -z $(docker volume ls -q -f name="${data_volume_3}") ]]; then + docker volume create \ + --driver local \ + ${data_volume_3} \ + &> /dev/null + fi + ;; 2) # Destroy the data volume if [[ -n $(docker volume ls -q -f name="${data_volume_2}") ]]; then @@ -143,6 +167,7 @@ function __setup () local -r private_network_2="bridge_internal_2" local -r data_volume_1="mysql.2.data-mysql" local -r data_volume_2="mysql.4.data-mysql" + local -r data_volume_3="mysql.6.data-mysql" # Create the bridge networks if [[ -z $(docker network ls -q -f name="${private_network_1}") ]]; then @@ -178,6 +203,12 @@ function __setup () ${data_volume_2} \ &> /dev/null fi + + if [[ -n $(docker volume ls -q -f name="${data_volume_3}") ]]; then + docker volume rm \ + ${data_volume_3} \ + &> /dev/null + fi } # Custom shpec matcher @@ -499,6 +530,7 @@ function test_custom_configuration () { local -r data_volume_1="mysql.2.data-mysql" local -r data_volume_2="mysql.4.data-mysql" + local -r data_volume_3="mysql.6.data-mysql" local -r private_network_1="bridge_internal_1" local -r private_network_2="bridge_internal_2" local -r redacted_value="********" @@ -510,11 +542,14 @@ function test_custom_configuration () local mysql_user_password_log="" local select_users="" local show_databases="" + local show_tables="" trap "__terminate_container mysql.2 &> /dev/null; \ __terminate_container mysql.3 &> /dev/null; \ __terminate_container mysql.4 &> /dev/null; \ __terminate_container mysql.5 &> /dev/null; \ + __terminate_container mysql.6 &> /dev/null; \ + __terminate_container mysql.7 &> /dev/null; \ __destroy; \ exit 1" \ INT TERM EXIT @@ -1032,6 +1067,96 @@ function test_custom_configuration () &> /dev/null end + describe "Custom intitialisation" + it "Runs a named server container." + docker run \ + --detach \ + --name mysql.6 \ + --network-alias mysql.6 \ + --network ${private_network_1} \ + --env "MYSQL_INIT_LIMIT=30" \ + --env "MYSQL_INIT_SQL=CREATE DATABASE IF NOT EXISTS \`{{MYSQL_USER_DATABASE}}-1\`; GRANT ALL PRIVILEGES ON \`{{MYSQL_USER_DATABASE}}-%\`.* TO '{{MYSQL_USER}}'@'{{MYSQL_USER_HOST}}' IDENTIFIED BY '{{MYSQL_USER_PASSWORD}}'; CREATE TABLE \`{{MYSQL_USER_DATABASE}}-1\`.\`user\` (\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, \`email\` varchar(255), PRIMARY KEY (\`id\`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;" \ + --env "MYSQL_ROOT_PASSWORD=/run/secrets/mysql_root_password" \ + --env "MYSQL_SUBNET=172.172.40.0/255.255.255.0" \ + --env "MYSQL_USER=app" \ + --env "MYSQL_USER_PASSWORD=/run/secrets/mysql_user_password" \ + --env "MYSQL_USER_DATABASE=appdb" \ + --volume ${data_volume_3}:/var/lib/mysql \ + --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/run/secrets:ro \ + jdeathe/centos-ssh-mysql:latest \ + &> /dev/null + + assert equal \ + "${?}" \ + 0 + end + + it "Runs a named client container." + docker run \ + --detach \ + --name mysql.7 \ + --network ${private_network_1} \ + --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ + --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ + jdeathe/centos-ssh-mysql:latest \ + &> /dev/null + + assert equal \ + "${?}" \ + 0 + end + + if ! __is_container_ready \ + mysql.6 \ + ${STARTUP_TIME} \ + "/usr/sbin/mysqld " \ + "[[ -e /var/lib/mysql/ibdata1 ]] \ + && [[ ! -e /var/lock/subsys/mysqld-bootstrap ]] \ + && [[ -s /var/run/mysqld/mysqld.pid ]]" + then + exit 1 + fi + + describe "Client to server connection" + # Set MySQL user password + if docker exec mysql.7 bash command -v mysql_config_editor &> /dev/null + then + docker exec -i mysql.7 sshpass mysql_config_editor set --skip-warn --host=mysql.6 --user=app --password \ + < ${PWD}/${TEST_DIRECTORY}/fixture/secrets/mysql_user_password + else + docker exec mysql.7 bash -c "printf -- '[client]\nuser=app\nhost=mysql.6\npassword={{MYSQL_USER_PASSWORD}}\n' > /root/.my.cnf" + docker exec mysql.7 chown 0:0 /root/.my.cnf + docker exec mysql.7 chmod 0600 /root/.my.cnf + docker exec -i mysql.7 bash -c "IFS= read -r mysql_root_password; sed -i -e \"s~{{MYSQL_ROOT_PASSWORD}}~\${mysql_root_password}~g\" /root/.my.cnf;" \ + < ${PWD}/${TEST_DIRECTORY}/fixture/secrets/mysql_user_password + fi + + it "Can successfully connect." + show_tables="$( + docker exec \ + mysql.7 \ + mysql \ + appdb-1 \ + -N \ + -B \ + -e "SHOW TABLES;" + )" + + assert equal \ + "${show_tables}" \ + "user" + end + end + + __terminate_container \ + mysql.6 \ + &> /dev/null + + __terminate_container \ + mysql.7 \ + &> /dev/null + end + describe "Configure autostart" __terminate_container \ mysql.1 \ From 811453f7cb121a67dfc933cfd0ab749e7f66d66e Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Mar 2019 01:27:24 +0000 Subject: [PATCH 65/98] #234: Fixes mysql user password configuration. --- test/shpec/operation_shpec.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 730fd88..12e8206 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -1127,7 +1127,7 @@ function test_custom_configuration () docker exec mysql.7 bash -c "printf -- '[client]\nuser=app\nhost=mysql.6\npassword={{MYSQL_USER_PASSWORD}}\n' > /root/.my.cnf" docker exec mysql.7 chown 0:0 /root/.my.cnf docker exec mysql.7 chmod 0600 /root/.my.cnf - docker exec -i mysql.7 bash -c "IFS= read -r mysql_root_password; sed -i -e \"s~{{MYSQL_ROOT_PASSWORD}}~\${mysql_root_password}~g\" /root/.my.cnf;" \ + docker exec -i mysql.7 bash -c "IFS= read -r mysql_user_password; sed -i -e \"s~{{MYSQL_USER_PASSWORD}}~\${mysql_user_password}~g\" /root/.my.cnf;" \ < ${PWD}/${TEST_DIRECTORY}/fixture/secrets/mysql_user_password fi From d16706d6349ffd37f5bf24155f22755a3155b9f2 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 18 Mar 2019 02:29:48 +0000 Subject: [PATCH 66/98] Release changes for 1.10.0 and 2.2.0. --- CHANGELOG.md | 2 +- Dockerfile | 2 +- README.md | 36 ++++++++++++++++++------------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd5bdd0..d5d8531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Summary of release changes for Version 2. CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. -### 2.2.0 - Unreleased +### 2.2.0 - 2019-03-18 - Updates source image to [2.5.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.5.1). - Updates and restructures Dockerfile. diff --git a/Dockerfile b/Dockerfile index 9b87666..eee5c73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM jdeathe/centos-ssh:2.5.1 -ARG RELEASE_VERSION="2.1.1" +ARG RELEASE_VERSION="2.2.0" # ------------------------------------------------------------------------------ # Base install of required packages diff --git a/README.md b/README.md index c165a90..9e43f90 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ Includes Automated password generation and an option for custom initialisation S ## Overview & links -The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-community` Docker tag. It is recommended to select a specific release tag - the convention is `centos-7-mysql57-community-2.1.1` or `2.1.1` for the [2.1.1](https://github.com/jdeathe/centos-ssh-mysql/tree/2.1.1) release tag. +The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-community` Docker tag. It is recommended to select a specific release tag - the convention is `centos-7-mysql57-community-2.2.0` or `2.2.0` for the [2.2.0](https://github.com/jdeathe/centos-ssh-mysql/tree/2.2.0) release tag. ### Tags and respective `Dockerfile` links -- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.1.1`, `2.1.1` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, `centos-6-1.9.1`, `1.9.1` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.2.0`, `2.2.0` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- `centos-6`, `centos-6-1.10.0`, `1.10.0` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) The Dockerfile can be used to build a base image that is the bases for several other docker images. @@ -43,7 +43,7 @@ $ docker run -d \ --name mysql.1 \ -p 3306:3306 \ -v /var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.1.1 + jdeathe/centos-ssh-mysql:2.2.0 ``` Now you can verify it is initialised and running successfully by inspecting the container's logs. @@ -118,10 +118,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.1.1 \ + jdeathe/centos-ssh-mysql:2.2.0 \ /usr/sbin/scmi install \ --chroot=/media/root \ - --tag=2.1.1 \ + --tag=2.2.0 \ --name=mysql.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -135,10 +135,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.1.1 \ + jdeathe/centos-ssh-mysql:2.2.0 \ /usr/sbin/scmi uninstall \ --chroot=/media/root \ - --tag=2.1.1 \ + --tag=2.2.0 \ --name=mysql.1 \ --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' ``` @@ -152,10 +152,10 @@ $ docker run \ --rm \ --privileged \ --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.1.1 \ + jdeathe/centos-ssh-mysql:2.2.0 \ /usr/sbin/scmi install \ --chroot=/media/root \ - --tag=2.1.1 \ + --tag=2.2.0 \ --name=mysql.1 \ --manager=systemd \ --register \ @@ -182,7 +182,7 @@ To see detailed information about the image run `scmi` with the `--info` option. $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:2.1.1 + jdeathe/centos-ssh-mysql:2.2.0 ) --info" ``` @@ -192,7 +192,7 @@ To perform an installation using the docker name `mysql.2` simply use the `--nam $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:2.1.1 + jdeathe/centos-ssh-mysql:2.2.0 ) --name=mysql.2" ``` @@ -202,7 +202,7 @@ To uninstall use the *same command* that was used to install but with the `unins $ eval "sudo -E $( docker inspect \ -f "{{.ContainerConfig.Labels.uninstall}}" \ - jdeathe/centos-ssh-mysql:2.1.1 + jdeathe/centos-ssh-mysql:2.2.0 ) --name=mysql.2" ``` @@ -215,7 +215,7 @@ To see detailed information about the image run `scmi` with the `--info` option. ``` $ sudo -E atomic install \ -n mysql.3 \ - jdeathe/centos-ssh-mysql:2.1.1 \ + jdeathe/centos-ssh-mysql:2.2.0 \ --info ``` @@ -224,14 +224,14 @@ To perform an installation using the docker name `mysql.3` simply use the `-n` o ``` $ sudo -E atomic install \ -n mysql.3 \ - jdeathe/centos-ssh-mysql:2.1.1 + jdeathe/centos-ssh-mysql:2.2.0 ``` Alternatively, you could use the `scmi` options `--name` or `-n` for naming the container. ``` $ sudo -E atomic install \ - jdeathe/centos-ssh-mysql:2.1.1 \ + jdeathe/centos-ssh-mysql:2.2.0 \ --name mysql.3 ``` @@ -240,7 +240,7 @@ To uninstall use the *same command* that was used to install but with the `unins ``` $ sudo -E atomic uninstall \ -n mysql.3 \ - jdeathe/centos-ssh-mysql:2.1.1 + jdeathe/centos-ssh-mysql:2.2.0 ``` #### Using environment variables @@ -259,7 +259,7 @@ $ docker run \ --env "MYSQL_USER_PASSWORD=" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume mysql.1.data-mysql:/var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.1.1 + jdeathe/centos-ssh-mysql:2.2.0 ``` The environmental variable `MYSQL_SUBNET` is optional but can be used to generate users with access to databases outside the `localhost`, (the default for the root user). In the example, the subnet definition `0.0.0.0/0.0.0.0` allows connections from any network which is equivalent to the wildcard symbol, `%`, in MySQL GRANT definitions. From 880fa65eeb791f6296d2e63fee38997912dbd6f6 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 29 Jun 2019 22:13:09 +0100 Subject: [PATCH 67/98] #244: Updates and simplifies documentation. --- CHANGELOG.md | 11 ++- Dockerfile | 2 +- README-short.txt | 2 +- README.md | 220 ++++++++--------------------------------------- command-keys.md | 67 --------------- 5 files changed, 48 insertions(+), 254 deletions(-) delete mode 100644 command-keys.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d8531..fcdd790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Change Log -## centos-7-mysql57-community +## 2 - centos-7-mysql57-community -Summary of release changes for Version 2. +Summary of release changes. -CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server. +### 2.3.0 - Unreleased + +- Updates CHANGELOG.md to simplify maintenance. +- Updates README.md to simplify contents and improve readability. +- Updates README-short.txt to apply to all image variants. +- Updates Dockerfile `org.deathe.description` metadata LABEL for consistency. ### 2.2.0 - 2019-03-18 diff --git a/Dockerfile b/Dockerfile index eee5c73..d1b53b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -103,7 +103,7 @@ jdeathe/centos-ssh-mysql:${RELEASE_VERSION} \ org.deathe.license="MIT" \ org.deathe.vendor="jdeathe" \ org.deathe.url="https://github.com/jdeathe/centos-ssh-mysql" \ - org.deathe.description="CentOS-7 7.5.1804 x86_64 - MySQL 5.7 Community Server." + org.deathe.description="MySQL Community Server 5.7 - CentOS-7 7.5.1804 x86_64." HEALTHCHECK \ --interval=1s \ diff --git a/README-short.txt b/README-short.txt index fc53ce3..dbdc6c8 100644 --- a/README-short.txt +++ b/README-short.txt @@ -1 +1 @@ -CentOS-7 7.5.1804 x86_64 - MySQL Community Server. \ No newline at end of file +MySQL Community Server - CentOS. \ No newline at end of file diff --git a/README.md b/README.md index 9e43f90..77a383e 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,22 @@ -centos-ssh-mysql -================ +## Tags and respective `Dockerfile` links -Docker Image including: -- CentOS-6 6.10 x86_64, MySQL 5.1. -- CentOS-7 7.5.1804 x86_64, MySQL 5.7 Community Server. +- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.2.0`, [`2.2.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.2.0) [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- `centos-6`, `centos-6-1.10.0`, [`1.10.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.10.0) [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) -Includes Automated password generation and an option for custom initialisation SQL. Supports custom configuration via environment variables. +## Overview -## Overview & links +This build uses the base image [jdeathe/centos-ssh](https://github.com/jdeathe/centos-ssh) so inherits it's features but with `sshd` disabled by default. [Supervisor](http://supervisord.org/) is used to start the [`mysqld`](https://www.mysql.com/products/community/) daemon when a docker container based on this image is run. -The latest CentOS-7 based release can be pulled from the `centos-7-mysql57-community` Docker tag. It is recommended to select a specific release tag - the convention is `centos-7-mysql57-community-2.2.0` or `2.2.0` for the [2.2.0](https://github.com/jdeathe/centos-ssh-mysql/tree/2.2.0) release tag. +Includes automated password generation and an option for custom initialisation SQL. -### Tags and respective `Dockerfile` links +### Image variants -- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.2.0`, `2.2.0` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, `centos-6-1.10.0`, `1.10.0` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- [MySQL Community Server 5.7 - CentOS-7](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community) +- [MySQL 5.1 - CentOS-6](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6) -The Dockerfile can be used to build a base image that is the bases for several other docker images. +## Quick start -Included in the build are the [SCL](https://www.softwarecollections.org/), [EPEL](http://fedoraproject.org/wiki/EPEL) and [IUS](https://ius.io) repositories. Installed packages include [OpenSSH](http://www.openssh.com/portable.html) secure shell, [vim-minimal](http://www.vim.org/), [MySQL Server and client programs](http://www.mysql.com) are installed along with python-setuptools, [supervisor](http://supervisord.org/) and [supervisor-stdout](https://github.com/coderanger/supervisor-stdout). - -Supervisor is used to start the mysqld server daemon when a docker container based on this image is run. To enable simple viewing of stdout for the service's subprocess, supervisor-stdout is included. This allows you to see output from the supervisord controlled subprocesses with `docker logs {container-name}`. - -If enabling and configuring SSH access, it is by public key authentication and, by default, the [Vagrant](http://www.vagrantup.com/) [insecure private key](https://github.com/mitchellh/vagrant/blob/master/keys/vagrant) is required. - -### SSH Alternatives - -SSH is not required in order to access a terminal for the running container. The simplest method is to use the docker exec command to run bash (or sh) as follows: - -``` -$ docker exec -it {container-name-or-id} bash -``` - -For cases where access to docker exec is not possible the preferred method is to use Command Keys and the nsenter command. See [command-keys.md](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/command-keys.md) for details on how to set this up. - -## Quick Example +> For production use, it is recommended to select a specific release tag as shown in the examples. Run up a container named `mysql.1` from the docker image `jdeathe/centos-ssh-mysql` on port 3306 of your docker host. @@ -46,7 +28,14 @@ $ docker run -d \ jdeathe/centos-ssh-mysql:2.2.0 ``` -Now you can verify it is initialised and running successfully by inspecting the container's logs. +Verify the named container's process status and health. + +``` +$ docker ps -a \ + -f "name=mysql.1" +``` + +Verify successful initialisation of the named container. ``` $ docker logs mysql.1 @@ -56,7 +45,7 @@ On the first run, there will be additional output showing the initialisation SQL ![Docker Logs - MySQL Bootstrap](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-7-mysql57-community/images/docker-logs-mysqld-bootstrap.png) -The MySQL table data is persistent across container restarts by setting the MySQL data directory `/var/lib/mysql` as a data volume. We didn't specify a name or docker_host path so Docker will give it a unique name and store it in `/var/lib/docker/volumes/`; to find out where the data is stored on the Docker host you can use `docker inspect`. +The MySQL table data is persistent across container restarts by setting the MySQL data directory `/var/lib/mysql` as a data volume. To locate the path where data is stored on the Docker host use `docker inspect`. ``` $ docker inspect \ @@ -64,14 +53,16 @@ $ docker inspect \ mysql.1 ``` -To access the interactive MySQL SQL shell run the following: +To access the interactive MySQL SQL shell. ``` -$ docker exec -it mysql.1 mysql +$ docker exec -it \ + mysql.1 \ + mysql ``` ### Sakila Example -To import the Sakila example database from the [MySQL Documentation](https://dev.mysql.com/doc/index-other.html) and view the first 2 records from the film table. +Import the Sakila example database from the [MySQL Documentation](https://dev.mysql.com/doc/index-other.html) and view the first 2 records from the film table. #### Import Schema @@ -92,7 +83,7 @@ $ docker exec -i mysql.1 mysql \ ) ``` -#### Select 2 Records from the film Table +#### Select Records ``` $ docker exec mysql.1 mysql \ @@ -103,154 +94,18 @@ $ docker exec mysql.1 mysql \ ### Running -To run the a docker container from this image you can use the standard docker commands. Alternatively, you can use the embedded (Service Container Manager Interface) [scmi](https://github.com/jdeathe/centos-ssh/blob/centos-7/src/usr/sbin/scmi) that is included in the image since `1.7.1` or, if you have a checkout of the [source repository](https://github.com/jdeathe/centos-ssh-mysql), and have make installed the Makefile provides targets to build, install, start, stop etc. where environment variables can be used to configure the container options and set custom docker run parameters. - -#### SCMI Installation Examples - -The following example uses docker to run the SCMI install command to create and start a container named `mysql.1`. To use SCMI it requires the use of the `--privileged` docker run parameter and the docker host's root directory mounted as a volume with the container's mount directory also being set in the `scmi` `--chroot` option. The `--setopt` option is used to add extra parameters to the default docker run command template; in the following example a named configuration volume is added which allows the SSH host keys to persist after the first container initialisation. Not that the placeholder `{{NAME}}` can be used in this option and is replaced with the container's name. - -*Note:* In most cases you will want to create an initial database, database user, (optionally a static password), and define the user's network access. If you don't define these settings using the appropriate environment variables on first run, the settings will not be parsed by the bootstrap initialisation process and only local root access will be available. To re-initialise a container that uses a named data volume mapped to /var/lib/mysql terminate the container and the data volume to allow it to be recreated. - -##### SCMI Install - -``` -$ docker run \ - --rm \ - --privileged \ - --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.2.0 \ - /usr/sbin/scmi install \ - --chroot=/media/root \ - --tag=2.2.0 \ - --name=mysql.1 \ - --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' -``` - -##### SCMI Uninstall - -To uninstall the previous example simply run the same docker run command with the scmi `uninstall` command. - -``` -$ docker run \ - --rm \ - --privileged \ - --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.2.0 \ - /usr/sbin/scmi uninstall \ - --chroot=/media/root \ - --tag=2.2.0 \ - --name=mysql.1 \ - --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' -``` - -##### SCMI Systemd Support - -If your docker host has systemd (and optionally etcd) installed then `scmi` provides a method to install the container as a systemd service unit. This provides some additional features for managing a group of instances on a single docker host and has the option to use an etcd backed service registry. Using a systemd unit file allows the System Administrator to use a Drop-In to override the settings of a unit-file template used to create service instances. To use the systemd method of installation use the `-m` or `--manager` option of `scmi` and to include the optional etcd register companion unit use the `--register` option. - -``` -$ docker run \ - --rm \ - --privileged \ - --volume /:/media/root \ - jdeathe/centos-ssh-mysql:2.2.0 \ - /usr/sbin/scmi install \ - --chroot=/media/root \ - --tag=2.2.0 \ - --name=mysql.1 \ - --manager=systemd \ - --register \ - --env='MYSQL_SUBNET="0.0.0.0/0.0.0.0"' \ - --env='MYSQL_USER="app-user"' \ - --env='MYSQL_USER_PASSWORD="Passw0rd"' \ - --env='MYSQL_USER_DATABASE="app-db"' \ - --setopt='--volume {{NAME}}.data-mysql:/var/lib/mysql' -``` - -##### SCMI Fleet Support - -**_Deprecation Notice:_** The fleet project is no longer maintained. The fleet `--manager` option has been deprecated in `scmi`. - -If your docker host has systemd, fleetd (and optionally etcd) installed then `scmi` provides a method to schedule the container to run on the cluster. This provides some additional features for managing a group of instances on a [fleet](https://github.com/coreos/fleet) cluster and has the option to use an etcd backed service registry. To use the fleet method of installation use the `-m` or `--manager` option of `scmi` and to include the optional etcd register companion unit use the `--register` option. +To run the a docker container from this image you can use the standard docker commands as shown in the example below. Alternatively, there's a [docker-compose](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/docker-compose.yml) example. -##### SCMI Image Information - -Since release `1.7.1` the install template has been added to the image metadata. Using docker inspect you can access `scmi` to simplify install/uninstall tasks. - -To see detailed information about the image run `scmi` with the `--info` option. To see all available `scmi` options run with the `--help` option. - -``` -$ eval "sudo -E $( - docker inspect \ - -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:2.2.0 - ) --info" -``` - -To perform an installation using the docker name `mysql.2` simply use the `--name` or `-n` option. - -``` -$ eval "sudo -E $( - docker inspect \ - -f "{{.ContainerConfig.Labels.install}}" \ - jdeathe/centos-ssh-mysql:2.2.0 - ) --name=mysql.2" -``` - -To uninstall use the *same command* that was used to install but with the `uninstall` Label. - -``` -$ eval "sudo -E $( - docker inspect \ - -f "{{.ContainerConfig.Labels.uninstall}}" \ - jdeathe/centos-ssh-mysql:2.2.0 - ) --name=mysql.2" -``` - -##### SCMI on Atomic Host - -With the addition of install/uninstall image labels it is possible to use [Project Atomic's](http://www.projectatomic.io/) `atomic install` command to simplify install/uninstall tasks on [CentOS Atomic](https://wiki.centos.org/SpecialInterestGroup/Atomic) Hosts. - -To see detailed information about the image run `scmi` with the `--info` option. To see all available `scmi` options run with the `--help` option. - -``` -$ sudo -E atomic install \ - -n mysql.3 \ - jdeathe/centos-ssh-mysql:2.2.0 \ - --info -``` - -To perform an installation using the docker name `mysql.3` simply use the `-n` option of the `atomic install` command. - -``` -$ sudo -E atomic install \ - -n mysql.3 \ - jdeathe/centos-ssh-mysql:2.2.0 -``` - -Alternatively, you could use the `scmi` options `--name` or `-n` for naming the container. - -``` -$ sudo -E atomic install \ - jdeathe/centos-ssh-mysql:2.2.0 \ - --name mysql.3 -``` - -To uninstall use the *same command* that was used to install but with the `uninstall` Label. - -``` -$ sudo -E atomic uninstall \ - -n mysql.3 \ - jdeathe/centos-ssh-mysql:2.2.0 -``` +For production use, it is recommended to select a specific release tag as shown in the examples. #### Using environment variables -The following example sets up a custom MySQL database, user and user password on first run. This will only work when MySQL runs the initialisation process and values must be specified for MYSQL_USER and MYSQL_USER_DATABASE. If MYSQL_USER_PASSWORD is not specified or left empty a random password will be generated. +The following example sets up a custom MySQL database, user and user password on first run. This will only work when MySQL runs the initialisation process and values must be specified for `MYSQL_USER` and `MYSQL_USER_DATABASE`. If `MYSQL_USER_PASSWORD` is not specified or left empty a random password will be generated. ``` $ docker stop mysql.1 && \ - docker rm mysql.1 -$ docker run \ + docker rm mysql.1 && \ + docker run \ --detach \ --name mysql.1 \ --publish 3306:3306 \ @@ -264,7 +119,7 @@ $ docker run \ The environmental variable `MYSQL_SUBNET` is optional but can be used to generate users with access to databases outside the `localhost`, (the default for the root user). In the example, the subnet definition `0.0.0.0/0.0.0.0` allows connections from any network which is equivalent to the wildcard symbol, `%`, in MySQL GRANT definitions. -Now you can verify it is initialised and running successfully by inspecting the container's logs: +Verify it's initialised and running successfully by inspecting the container's logs: ``` $ docker logs mysql.1 @@ -274,7 +129,7 @@ $ docker logs mysql.1 There are several environmental variables defined at runtime these allow the operator to customise the running container. -*Note:* Most of these settings are only evaluated during the first run of a named container; if the data volume already exists and contains database table data then changing these values will have no effect. +> *Note:* Most of these settings are only evaluated during the first run of a named container; if the data volume already exists and contains database table data then changing these values will have no effect. ##### MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP & MYSQL_AUTOSTART_MYSQLD_WRAPPER @@ -307,7 +162,7 @@ To add custom SQL to the MySQL intitialisation use `MYSQL_INIT_SQL` where the fo - `{{MYSQL_USER_HOST}}` - `{{MYSQL_USER_PASSWORD}}` -*Note:* The backtick "\`" character will need escaping as show in the example. +> *Note:* The backtick "\`" character will need escaping as show in the example. ``` ... @@ -344,11 +199,12 @@ To indicate `MYSQL_ROOT_PASSWORD` is a pre-hashed value instead of the default p ... ``` -*Note:* To generate a pre-hashed password you could use the following MySQL command. +To generate a pre-hashed password use the following MySQL query, substituting `{{password}}` with the required password. ``` -$ docker exec mysql.1 mysql -NB \ - -e "SELECT PASSWORD('{mysql_user_password}');" +$ docker exec mysql.1 \ + mysql -NB \ + -e "SELECT PASSWORD('{{password}}');" ``` ##### MYSQL_USER diff --git a/command-keys.md b/command-keys.md deleted file mode 100644 index 13bfc19..0000000 --- a/command-keys.md +++ /dev/null @@ -1,67 +0,0 @@ -# Command Keys - -Using command keys to access containers (without sshd). - -Access docker containers using docker host SSH public key authentication and nsenter command to start up a bash terminal inside a container. In the following example the container name is "mysql.1" - -## Create a unique public/private key pair for each container - -``` -$ cd ~/.ssh/ && ssh-keygen -q -t rsa -f id-rsa.mysql.1 -``` - -## Prefix the public key with the nsenter command - -``` -$ sed -i '' \ - '1s#^#command="sudo nsenter -m -u -i -n -p -t $(docker inspect --format \\\"{{ .State.Pid }}\\\" mysql.1) /bin/bash" #' \ - ~/.ssh/id-rsa.mysql.1.pub -``` - -## Upload the public key to the docker host VM - -The host in this example is core-01.local that has SSH public key authentication enabled using the Vagrant insecure private key. - -### Generic Linux Host Example - -``` -$ cat ~/.ssh/id-rsa.mysql.1.pub | ssh -i ~/.vagrant.d/insecure_private_key \ - core@core-01.local \ - "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" -``` - -### CoreOS Host Example - -``` -$ cat ~/.ssh/id-rsa.mysql.1.pub | ssh -i ~/.vagrant.d/insecure_private_key \ - core@core-01.local \ - update-ssh-keys -a core@mysql.1 -``` - -### Usage - -``` -$ ssh -i ~/.ssh/id-rsa.mysql.1 \ - core@core-01.local \ - -o StrictHostKeyChecking=no -``` - -#### SSH Config - -To simplify the command required to access the running container we can add an entry to the SSH configuration file ```~/.ssh/config``` as follows: - -``` -Host core-01.mysql.1 - HostName core-01.local - Port 22 - User core - StrictHostKeyChecking no - IdentitiesOnly yes - IdentityFile ~/.ssh/id-rsa.mysql.1 -``` - -With the above entry in place we can now run the following to access the running container: - -``` -$ ssh core-01.mysql.1 -``` From e4160863800e2e2c5f88c20f109c0eedf59a4461 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 30 Jun 2019 12:09:31 +0100 Subject: [PATCH 68/98] #243: Updates order of supervisord config directives for consistency. --- CHANGELOG.md | 1 + src/etc/supervisord.d/mysqld-bootstrap.conf | 8 ++++---- src/etc/supervisord.d/mysqld-wrapper.conf | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcdd790..1844af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Summary of release changes. - Updates README.md to simplify contents and improve readability. - Updates README-short.txt to apply to all image variants. - Updates Dockerfile `org.deathe.description` metadata LABEL for consistency. +- Updates order of supervisord configuration directives for consistency. ### 2.2.0 - 2019-03-18 diff --git a/src/etc/supervisord.d/mysqld-bootstrap.conf b/src/etc/supervisord.d/mysqld-bootstrap.conf index fedd173..3ebb73b 100644 --- a/src/etc/supervisord.d/mysqld-bootstrap.conf +++ b/src/etc/supervisord.d/mysqld-bootstrap.conf @@ -1,10 +1,10 @@ [program:mysqld-bootstrap] -priority = 6 -command = /usr/sbin/mysqld-bootstrap --verbose +autorestart = false autostart = %(ENV_MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP)s +command = /usr/sbin/mysqld-bootstrap --verbose +priority = 6 +redirect_stderr = true startsecs = 0 startretries = 0 -autorestart = false -redirect_stderr = true stdout_logfile = /dev/stdout stdout_logfile_maxbytes = 0 diff --git a/src/etc/supervisord.d/mysqld-wrapper.conf b/src/etc/supervisord.d/mysqld-wrapper.conf index d978dc2..ba007ff 100644 --- a/src/etc/supervisord.d/mysqld-wrapper.conf +++ b/src/etc/supervisord.d/mysqld-wrapper.conf @@ -1,9 +1,9 @@ [program:mysqld-wrapper] -priority = 100 -command = /usr/sbin/mysqld-wrapper -autostart = %(ENV_MYSQL_AUTOSTART_MYSQLD_WRAPPER)s -startsecs = 0 autorestart = true +autostart = %(ENV_MYSQL_AUTOSTART_MYSQLD_WRAPPER)s +command = /usr/sbin/mysqld-wrapper +priority = 100 redirect_stderr = true +startsecs = 0 stdout_logfile = /dev/stdout stdout_logfile_maxbytes = 0 From 6cbb53fc3b27a5891d646ca5439268cda335658d Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 30 Jun 2019 17:03:59 +0100 Subject: [PATCH 69/98] #249: Updates mysql community server packages. --- CHANGELOG.md | 1 + Dockerfile | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1844af6..ace557e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Summary of release changes. ### 2.3.0 - Unreleased +- Updates `mysql-community-server` package to 5.7.26-1. - Updates CHANGELOG.md to simplify maintenance. - Updates README.md to simplify contents and improve readability. - Updates README-short.txt to apply to all image variants. diff --git a/Dockerfile b/Dockerfile index d1b53b7..932baef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,10 +19,10 @@ RUN { printf -- \ && yum -y install \ --setopt=tsflags=nodocs \ --disableplugin=fastestmirror \ - mysql-community-client-5.7.25-1.el7 \ - mysql-community-common-5.7.25-1.el7 \ - mysql-community-libs-5.7.25-1.el7 \ - mysql-community-server-5.7.25-1.el7 \ + mysql-community-client-5.7.26-1.el7 \ + mysql-community-common-5.7.26-1.el7 \ + mysql-community-libs-5.7.26-1.el7 \ + mysql-community-server-5.7.26-1.el7 \ psmisc-22.20-15.el7 \ sshpass-1.06-2.el7 \ && yum versionlock add \ From 9227d1e3b8bd93a4649f21f125ff71b6a0e6b711 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 4 Jul 2019 17:13:24 +0100 Subject: [PATCH 70/98] #247: Updates source image to 2.6.0. --- .env.example | 1 + CHANGELOG.md | 13 + Dockerfile | 23 +- Makefile | 287 +++++++++++------- README.md | 14 +- default.mk | 7 +- docker-compose.yml | 1 + environment.mk | 24 +- ...ootstrap.conf => 20-mysqld-bootstrap.conf} | 7 +- ...ld-wrapper.conf => 50-mysqld-wrapper.conf} | 9 +- .../system/centos-ssh-mysql.register@.service | 2 +- .../systemd/system/centos-ssh-mysql@.service | 10 +- src/opt/scmi/default.sh | 5 +- src/opt/scmi/environment.sh | 27 +- src/opt/scmi/service-unit.sh | 5 +- src/usr/bin/healthcheck | 54 +++- src/usr/sbin/mysqld-bootstrap | 229 ++++++++++---- src/usr/sbin/mysqld-wrapper | 131 ++++++-- test/shpec/operation_shpec.sh | 16 +- 19 files changed, 580 insertions(+), 285 deletions(-) rename src/etc/supervisord.d/{mysqld-bootstrap.conf => 20-mysqld-bootstrap.conf} (61%) rename src/etc/supervisord.d/{mysqld-wrapper.conf => 50-mysqld-wrapper.conf} (51%) diff --git a/.env.example b/.env.example index 242d41e..32406ba 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,4 @@ MYSQL_USER= MYSQL_USER_DATABASE= MYSQL_USER_PASSWORD= MYSQL_USER_PASSWORD_HASHED=false +SYSTEM_TIMEZONE=UTC diff --git a/CHANGELOG.md b/CHANGELOG.md index ace557e..0cd931b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,25 @@ Summary of release changes. ### 2.3.0 - Unreleased +- Updates source image to [2.6.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.6.0). - Updates `mysql-community-server` package to 5.7.26-1. - Updates CHANGELOG.md to simplify maintenance. - Updates README.md to simplify contents and improve readability. - Updates README-short.txt to apply to all image variants. - Updates Dockerfile `org.deathe.description` metadata LABEL for consistency. - Updates order of supervisord configuration directives for consistency. +- Updates supervisord configuration to send error log output to stderr. +- Updates bootstrap supervisord configuration file/priority to `20-mysqld-bootstrap.conf`/`20`. +- Updates wrapper supervisord configuration file/priority to `50-mysqld-wrapper.conf`/`50`. +- Fixes docker host connection status check in Makefile. +- Adds `inspect`, `reload` and `top` Makefile targets. +- Adds improved `clean` Makefile target; includes exited containers and dangling images. +- Adds `SYSTEM_TIMEZONE` handling to Makefile, scmi, systemd unit and docker-compose templates. +- Adds system time zone validation to healthcheck. +- Adds lock/state file to bootstrap/wrapper scripts. +- Removes `MYSQL_AUTOSTART_MYSQL_BOOTSTRAP`, replaced with `ENABLE_MYSQL_BOOTSTRAP`. +- Removes `MYSQL_AUTOSTART_MYSQL_WRAPPER`, replaced with `ENABLE_MYSQL_WRAPPER`. +- Removes support for long image tags (i.e. centos-7-mysql57-community-2.x.x). ### 2.2.0 - 2019-03-18 diff --git a/Dockerfile b/Dockerfile index 932baef..3da4bf4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM jdeathe/centos-ssh:2.5.1 +FROM jdeathe/centos-ssh:2.6.0 ARG RELEASE_VERSION="2.2.0" @@ -15,7 +15,7 @@ RUN { printf -- \ 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql'; \ } > /etc/yum.repos.d/mysql-community.repo \ && rpm --import \ - https://repo.mysql.com/RPM-GPG-KEY-mysql \ + https://repo.mysql.com/RPM-GPG-KEY-mysql \ && yum -y install \ --setopt=tsflags=nodocs \ --disableplugin=fastestmirror \ @@ -48,17 +48,20 @@ RUN sed -i \ && chmod 600 \ /etc/my.cnf \ && chmod 644 \ - /etc/supervisord.d/mysqld-{bootstrap,wrapper}.conf \ + /etc/supervisord.d/{20-mysqld-bootstrap,50-mysqld-wrapper}.conf \ && chmod 700 \ /usr/{bin/healthcheck,sbin/mysqld-{bootstrap,wrapper}} EXPOSE 3306 -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Set default environment variables -# ----------------------------------------------------------------------------- -ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="true" \ - MYSQL_AUTOSTART_MYSQLD_WRAPPER="true" \ +# ------------------------------------------------------------------------------ +ENV \ + ENABLE_MYSQLD_BOOTSTRAP="true" \ + ENABLE_MYSQLD_WRAPPER="true" \ + ENABLE_SSHD_BOOTSTRAP="false" \ + ENABLE_SSHD_WRAPPER="false" \ MYSQL_INIT_LIMIT="60" \ MYSQL_INIT_SQL="" \ MYSQL_ROOT_PASSWORD="" \ @@ -68,9 +71,7 @@ ENV MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="true" \ MYSQL_USER_DATABASE="" \ MYSQL_USER_PASSWORD="" \ MYSQL_USER_PASSWORD_HASHED="false" \ - SSH_AUTOSTART_SSHD="false" \ - SSH_AUTOSTART_SSHD_BOOTSTRAP="false" \ - SSH_AUTOSTART_SUPERVISOR_STDOUT="false" + SYSTEM_TIMEZONE="UTC" # ------------------------------------------------------------------------------ # Set image metadata @@ -103,7 +104,7 @@ jdeathe/centos-ssh-mysql:${RELEASE_VERSION} \ org.deathe.license="MIT" \ org.deathe.vendor="jdeathe" \ org.deathe.url="https://github.com/jdeathe/centos-ssh-mysql" \ - org.deathe.description="MySQL Community Server 5.7 - CentOS-7 7.5.1804 x86_64." + org.deathe.description="MySQL 5.7 Community Server - CentOS-7 7.6.1810 x86_64." HEALTHCHECK \ --interval=1s \ diff --git a/Makefile b/Makefile index 5ebb343..6416f1b 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ Targets: distclean Clean up distribution artifacts. exec COMMAND [ARG...] Run command in a the running container. help Show this help. + inspect [-f \"FORMAT\"] Return low-level information on the container. install Terminate running container and run the docker create template. images Show container's image details. @@ -32,14 +33,19 @@ Targets: pull Pull the release image from the registry. Requires the DOCKER_IMAGE_TAG variable. ps Display the details of the container process. + reload Send SIGHUP to the PID 1 container process. restart Restarts the container. rm Force remove the container. + rm-exited Force remove all containers in the exited state. rmi Untag (remove) the image. + rmi-dangling Untag (remove) images not referenced by any + container. run Execute the run container template. start Start the container in the created state. stop Stop the container when in a running state. terminate Unpause, stop and remove the container. test Run all test cases. + top [ps OPTIONS] Display the running processes of the container. unpause Unpause the container when in a paused state. Variables: @@ -60,6 +66,8 @@ Variables: artifacts are placed. - NO_CACHE When true, no cache will be used while running the build target. + - RELOAD_SIGNAL Default signal is SIGHUP. Use to set an alternative + signal value. - STARTUP_TIME Defines the number of seconds expected to complete the startup process, including the bootstrap where applicable. @@ -69,7 +77,7 @@ endef include environment.mk include default.mk -# UI constants +.DEFAULT_GOAL := build COLOUR_NEGATIVE := \033[1;31m COLOUR_POSITIVE := \033[1;32m COLOUR_RESET := \033[0m @@ -106,25 +114,22 @@ PREFIX_SUB_STEP_POSITIVE := $(shell \ "$(PREFIX_SUB_STEP)" \ "$(COLOUR_RESET)"; \ ) - -.DEFAULT_GOAL := build - -# Package prerequisites docker := $(shell \ command -v docker \ ) -xz := $(shell \ - command -v xz \ +docker-status := $(shell \ + if ! docker version > /dev/null; \ + then \ + printf -- 'ERROR'; \ + else \ + printf -- 'OK'; \ + fi \ ) - -# Testing prerequisites shpec := $(shell \ command -v shpec \ ) - -# Used to test docker host is accessible -get-docker-info := $(shell \ - $(docker) info \ +xz := $(shell \ + command -v xz \ ) define get-docker-image-id @@ -166,6 +171,7 @@ endef distclean \ exec \ help \ + inspect \ install \ images \ load \ @@ -174,14 +180,18 @@ endef pause \ pull \ ps \ + reload \ restart \ rm \ + rm-exited \ rmi \ + rmi-dangling \ run \ start \ stop \ terminate \ test \ + top \ unpause _prerequisites: @@ -193,38 +203,34 @@ ifeq ($(xz),) $(error "Please install the xz package.") endif -ifeq ($(get-docker-info),) - $(error "Unable to connect to docker host.") +ifneq ($(docker-status),OK) + $(error "Docker server host error.") endif _require-docker-container: @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; \ then \ - printf -- '%sThis operation requires the %s container.\n' \ + >&2 printf -- '%sThis operation requires the %s container.\n' \ "$(PREFIX_STEP_NEGATIVE)" \ - "$(DOCKER_NAME)" \ - >&2; \ - printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "$(DOCKER_NAME)"; \ + >&2 printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ "$(PREFIX_SUB_STEP)" \ "$(DOCKER_NAME)" \ - "install" \ - >&2; \ + "install"; \ exit 1; \ fi _require-docker-container-not: @ if [[ -n $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; \ then \ - printf -- '%sThis operation requires the %s container %s.\n' \ + >&2 printf -- '%sThis operation requires the %s container %s.\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "$(DOCKER_NAME)" \ - "be removed or renamed" \ - >&2; \ - printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "be removed or renamed"; \ + >&2 printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ "$(PREFIX_SUB_STEP)" \ "$(DOCKER_NAME)" \ - "rm" \ - >&2; \ + "rm"; \ exit 1; \ fi @@ -234,16 +240,14 @@ _require-docker-container-not-status-paused: --filter "status=paused" \ ) ]]; \ then \ - printf -- '%sThis operation requires the %s container %s.\n' \ + >&2 printf -- '%sThis operation requires the %s container %s.\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "$(DOCKER_NAME)" \ - "to be unpaused" \ - >&2; \ - printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "to be unpaused"; \ + >&2 printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ "$(PREFIX_SUB_STEP)" \ "$(DOCKER_NAME)" \ - "unpause" \ - >&2; \ + "unpause"; \ exit 1; \ fi @@ -253,16 +257,14 @@ _require-docker-container-status-created: --filter "status=created" \ ) ]]; \ then \ - printf -- '%sThis operation requires the %s container %s.\n' \ + >&2 printf -- '%sThis operation requires the %s container %s.\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "$(DOCKER_NAME)" \ - "to be created" \ - >&2; \ - printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "to be created"; \ + >&2 printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ "$(PREFIX_SUB_STEP)" \ "$(DOCKER_NAME)" \ - "install" \ - >&2; \ + "install"; \ exit 1; \ fi @@ -272,16 +274,14 @@ _require-docker-container-status-exited: --filter "status=exited" \ ) ]]; \ then \ - printf -- '%sThis operation requires the %s container %s.\n' \ + >&2 printf -- '%sThis operation requires the %s container %s.\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "$(DOCKER_NAME)" \ - "to be exited" \ - >&2; \ - printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "to be exited"; \ + >&2 printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ "$(PREFIX_SUB_STEP)" \ "$(DOCKER_NAME)" \ - "stop" \ - >&2; \ + "stop"; \ exit 1; \ fi @@ -291,16 +291,14 @@ _require-docker-container-status-paused: --filter "status=paused" \ ) ]]; \ then \ - printf -- '%sThis operation requires the %s container %s.\n' \ + >&2 printf -- '%sThis operation requires the %s container %s.\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "$(DOCKER_NAME)" \ - "to be paused" \ - >&2; \ - printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "to be paused"; \ + >&2 printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ "$(PREFIX_SUB_STEP)" \ "$(DOCKER_NAME)" \ - "pause" \ - >&2; \ + "pause"; \ exit 1; \ fi @@ -310,42 +308,37 @@ _require-docker-container-status-running: --filter "status=running" \ ) ]]; \ then \ - printf -- '%sThis operation requires the %s container %s.\n' \ + >&2 printf -- '%sThis operation requires the %s container %s.\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "$(DOCKER_NAME)" \ - "to be running" \ - >&2; \ - printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ + "to be running"; \ + >&2 printf -- '%sTry: DOCKER_NAME=%s make %s\n' \ "$(PREFIX_SUB_STEP)" \ "$(DOCKER_NAME)" \ - "start" \ - >&2; \ + "start"; \ exit 1; \ fi _require-docker-image-tag: @ if ! [[ "$(DOCKER_IMAGE_TAG)" =~ $(DOCKER_IMAGE_TAG_PATTERN) ]]; \ then \ - printf -- '%sInvalid %s value: %s\n' \ + >&2 printf -- '%sInvalid %s value: %s\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "DOCKER_IMAGE_TAG" \ - "$(DOCKER_IMAGE_TAG)" \ - >&2; \ + "$(DOCKER_IMAGE_TAG)"; \ exit 1; \ fi _require-docker-release-tag: @ if ! [[ "$(DOCKER_IMAGE_TAG)" =~ $(DOCKER_IMAGE_RELEASE_TAG_PATTERN) ]]; \ then \ - printf -- '%sInvalid %s value: %s\n' \ + >&2 printf -- '%sInvalid %s value: %s\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "DOCKER_IMAGE_TAG" \ - "$(DOCKER_IMAGE_TAG)" \ - >&2; \ - printf -- '%s%s\n' \ + "$(DOCKER_IMAGE_TAG)"; \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP)" \ - "A release tag is required for this operation." \ - >&2; \ + "A release tag is required for this operation."; \ exit 1; \ fi @@ -359,18 +352,16 @@ _require-package-path: fi; \ if [[ ! $${?} -eq 0 ]]; \ then \ - printf -- '%s%s: %s\n' \ + >&2 printf -- '%s%s: %s\n' \ "$(PREFIX_STEP_NEGATIVE)" \ "Failed to make package path" \ - "$(DIST_PATH)" \ - >&2; \ + "$(DIST_PATH)"; \ exit 1; \ elif [[ -z $(DIST_PATH) ]]; \ then \ - printf -- '%sUndefined %s\n' \ + >&2 printf -- '%sUndefined %s\n' \ "$(PREFIX_STEP_NEGATIVE)" \ - "DIST_PATH" \ - >&2; \ + "DIST_PATH"; \ exit 1; \ fi @@ -415,10 +406,9 @@ build: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Build complete"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Build error" \ - >&2; \ + "Build error"; \ exit 1; \ fi @@ -426,7 +416,9 @@ clean: \ _prerequisites \ | \ terminate \ - rmi + rm-exited \ + rmi \ + rmi-dangling create: \ _prerequisites \ @@ -456,10 +448,9 @@ create: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Container created"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Container creation failed" \ - >&2; \ + "Container creation failed"; \ exit 1; \ fi @@ -506,10 +497,9 @@ dist: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Package saved"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Package save error" \ - >&2; \ + "Package save error"; \ exit 1; \ fi; \ fi @@ -547,10 +537,9 @@ distclean: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Package cleanup complete"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Package cleanup failed" \ - >&2; \ + "Package cleanup failed"; \ exit 1; \ fi; \ else \ @@ -560,7 +549,9 @@ distclean: \ fi exec: \ - _prerequisites + _prerequisites \ + _require-docker-container \ + _require-docker-container-status-running @ $(docker) exec -it $(DOCKER_NAME) $(filter-out $@, $(MAKECMDGOALS)) %:; @: @@ -572,17 +563,29 @@ images: \ help: \ _usage +inspect: \ + _prerequisites \ + _require-docker-container \ + _require-docker-container-status-running + @ $(docker) inspect \ + --type=container \ + $(filter-out $@, $(MAKECMDGOALS)) \ + $(DOCKER_NAME) +%:; @: + install: | \ _prerequisites \ terminate \ create logs: \ - _prerequisites + _prerequisites \ + _require-docker-container @ $(docker) logs $(DOCKER_NAME) logs-delayed: \ - _prerequisites + _prerequisites \ + _require-docker-container @ sleep $(STARTUP_TIME) @ $(MAKE) logs @@ -608,15 +611,13 @@ load: \ "$(DOCKER_IMAGE_TAG)"; \ if [[ ! -s $($@_dist_path)/$($@_dist_file) ]]; \ then \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Package not found" \ - >&2; \ - printf -- '%sTry: DOCKER_IMAGE_TAG=%s make %s\n' \ + "Package not found"; \ + >&2 printf -- '%sTry: DOCKER_IMAGE_TAG=%s make %s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ "$(DOCKER_IMAGE_TAG)" \ - "dist" \ - >&2; \ + "dist"; \ exit 1; \ else \ $(xz) -dc \ @@ -632,6 +633,7 @@ load: \ pause: \ _prerequisites \ + _require-docker-container \ _require-docker-container-status-running @ printf -- '%s%s\n' \ "$(PREFIX_STEP)" \ @@ -660,10 +662,9 @@ pull: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Image pulled"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Error pulling image" \ - >&2; \ + "Error pulling image"; \ exit 1; \ fi @@ -673,6 +674,19 @@ ps: \ @ $(docker) ps -as \ --filter "name=$(DOCKER_NAME)" +reload: \ + _prerequisites \ + _require-docker-container \ + _require-docker-container-status-running + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Reloading container" + @ $(docker) exec $(DOCKER_NAME) \ + kill -$(RELOAD_SIGNAL) 1 + @ printf -- '%s%s\n' \ + "$(PREFIX_SUB_STEP_POSITIVE)" \ + "Container reloaded" + restart: \ _prerequisites \ _require-docker-container \ @@ -710,14 +724,33 @@ rm: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Container removed"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Container removal failed" \ - >&2; \ + "Container removal failed"; \ exit 1; \ fi; \ fi +rm-exited: \ + _prerequisites + @ if [[ -z $$($(docker) ps -aq \ + --filter "status=exited" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Exited containers removal skipped"; \ + else \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Removing exited containers"; \ + $(docker) rm -f \ + $$($(docker) ps -aq \ + --filter "status=exited" \ + ) \ + 1> /dev/null; \ + fi + rmi: \ _prerequisites \ _require-docker-image-tag \ @@ -742,10 +775,9 @@ rmi: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Image untagged"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Error untagging image" \ - >&2; \ + "Error untagging image"; \ exit 1; \ fi; \ else \ @@ -754,6 +786,26 @@ rmi: \ "Untagging image skipped"; \ fi +rmi-dangling: \ + _prerequisites + @ if [[ -z $$($(docker) images -q \ + --filter "dangling=true" \ + ) ]]; \ + then \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Untagging dangling images skipped"; \ + else \ + printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Untagging dangling images"; \ + $(docker) rmi \ + $$($(docker) images -q \ + --filter "dangling=true" \ + ) \ + 1> /dev/null; \ + fi + run: \ _prerequisites \ _require-docker-image-tag @@ -783,10 +835,9 @@ run: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Container running"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Container run failed" \ - >&2; \ + "Container run failed"; \ exit 1; \ fi @@ -816,15 +867,15 @@ start: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Container started"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Container start failed" \ - >&2; \ + "Container start failed"; \ exit 1; \ fi stop: \ _prerequisites \ + _require-docker-container \ _require-docker-container-not-status-paused \ _require-docker-container-status-running @ printf -- '%s%s\n' \ @@ -847,14 +898,20 @@ stop: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Container stopped"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Error stopping container" \ - >&2; \ + "Error stopping container"; \ exit 1; \ fi; \ fi +top: \ + _prerequisites \ + _require-docker-container \ + _require-docker-container-status-running + @ $(docker) top $(DOCKER_NAME) $(filter-out $@, $(MAKECMDGOALS)) +%:; @: + terminate: \ _prerequisites @ if [[ -z $$($(docker) ps -aq --filter "name=$(DOCKER_NAME)") ]]; \ @@ -905,10 +962,9 @@ terminate: \ "$(PREFIX_SUB_STEP_POSITIVE)" \ "Container terminated"; \ else \ - printf -- '%s%s\n' \ + >&2 printf -- '%s%s\n' \ "$(PREFIX_SUB_STEP_NEGATIVE)" \ - "Container termination failed" \ - >&2; \ + "Container termination failed"; \ exit 1; \ fi; \ fi @@ -926,6 +982,7 @@ test: \ unpause: \ _prerequisites \ + _require-docker-container \ _require-docker-container-status-paused @ printf -- '%s%s\n' \ "$(PREFIX_STEP)" \ diff --git a/README.md b/README.md index 77a383e..6c32305 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Tags and respective `Dockerfile` links -- `centos-7-mysql57-community`, `centos-7-mysql57-community-2.2.0`, [`2.2.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.2.0) [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, `centos-6-1.10.0`, [`1.10.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.10.0) [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- `centos-7-mysql57-community`, [`2.2.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.2.0) [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- `centos-6`, [`1.10.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.10.0) [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) ## Overview @@ -11,7 +11,7 @@ Includes automated password generation and an option for custom initialisation S ### Image variants -- [MySQL Community Server 5.7 - CentOS-7](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community) +- [MySQL 5.7 Community Server - CentOS-7](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community) - [MySQL 5.1 - CentOS-6](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6) ## Quick start @@ -131,14 +131,14 @@ There are several environmental variables defined at runtime these allow the ope > *Note:* Most of these settings are only evaluated during the first run of a named container; if the data volume already exists and contains database table data then changing these values will have no effect. -##### MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP & MYSQL_AUTOSTART_MYSQLD_WRAPPER +##### ENABLE_MYSQLD_BOOTSTRAP & ENABLE_MYSQLD_WRAPPER -It may be desirable to prevent the startup of the mysqld-bootstrap and/or mysqld-wrapper scripts. For example, when using an image built from this Dockerfile as the source for another Dockerfile you could disable both mysqld-wrapper and mysqld from startup by setting `MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP` and `MYSQL_AUTOSTART_MYSQLD_WRAPPER` to `false`. The benefit of this is to reduce the number of running processes in the final container. Another use for this would be to make use of the packages installed in the image such as `mysql` and `mysqladmin`; effectively making the container a MySQL client. +It may be desirable to prevent the startup of the mysqld-bootstrap and/or mysqld-wrapper scripts. For example, when using an image built from this Dockerfile as the source for another Dockerfile you could disable both mysqld-wrapper and mysqld from startup by setting `ENABLE_MYSQLD_BOOTSTRAP` and `ENABLE_MYSQLD_WRAPPER` to `false`. The benefit of this is to reduce the number of running processes in the final container. Another use for this would be to make use of the packages installed in the image such as `mysql` and `mysqladmin`; effectively making the container a MySQL client. ``` ... - --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ - --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ + --env "ENABLE_MYSQLD_BOOTSTRAP=false" \ + --env "ENABLE_MYSQLD_WRAPPER=false" \ ... ``` diff --git a/default.mk b/default.mk index c28d252..bbbf735 100644 --- a/default.mk +++ b/default.mk @@ -41,8 +41,8 @@ DOCKER_PUBLISH := $(shell \ define DOCKER_CONTAINER_PARAMETERS --name $(DOCKER_NAME) \ --restart $(DOCKER_RESTART_POLICY) \ ---env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=$(MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP)" \ ---env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=$(MYSQL_AUTOSTART_MYSQLD_WRAPPER)" \ +--env "ENABLE_MYSQLD_BOOTSTRAP=$(ENABLE_MYSQLD_BOOTSTRAP)" \ +--env "ENABLE_MYSQLD_WRAPPER=$(ENABLE_MYSQLD_WRAPPER)" \ --env "MYSQL_INIT_LIMIT=$(MYSQL_INIT_LIMIT)" \ --env "MYSQL_INIT_SQL=$(MYSQL_INIT_SQL)" \ --env "MYSQL_ROOT_PASSWORD=$(MYSQL_ROOT_PASSWORD)" \ @@ -51,5 +51,6 @@ define DOCKER_CONTAINER_PARAMETERS --env "MYSQL_USER=$(MYSQL_USER)" \ --env "MYSQL_USER_DATABASE=$(MYSQL_USER_DATABASE)" \ --env "MYSQL_USER_PASSWORD=$(MYSQL_USER_PASSWORD)" \ ---env "MYSQL_USER_PASSWORD_HASHED=$(MYSQL_USER_PASSWORD_HASHED)" +--env "MYSQL_USER_PASSWORD_HASHED=$(MYSQL_USER_PASSWORD_HASHED)" \ +--env "SYSTEM_TIMEZONE=$(SYSTEM_TIMEZONE)" endef diff --git a/docker-compose.yml b/docker-compose.yml index a924ba5..1bb627b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,7 @@ services: MYSQL_USER_DATABASE: "${MYSQL_USER_DATABASE}" MYSQL_USER_PASSWORD: "${MYSQL_USER_PASSWORD}" MYSQL_USER_PASSWORD_HASHED: "${MYSQL_USER_PASSWORD_HASHED}" + SYSTEM_TIMEZONE: "${SYSTEM_TIMEZONE}" image: "jdeathe/centos-ssh-mysql:latest" ports: - "3306:3306" diff --git a/environment.mk b/environment.mk index b5e8b9e..2a262e4 100644 --- a/environment.mk +++ b/environment.mk @@ -1,39 +1,30 @@ # ------------------------------------------------------------------------------ # Constants # ------------------------------------------------------------------------------ -DOCKER_USER := jdeathe DOCKER_IMAGE_NAME := centos-ssh-mysql +DOCKER_IMAGE_RELEASE_TAG_PATTERN := ^[1-2]\.[0-9]+\.[0-9]+$ +DOCKER_IMAGE_TAG_PATTERN := ^(latest|[1-2]\.[0-9]+\.[0-9]+)$ +DOCKER_USER := jdeathe SHPEC_ROOT := test/shpec -# Tag validation patterns -DOCKER_IMAGE_TAG_PATTERN := ^(latest|centos-6|centos-7-mysql57-community|(([1-2]|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+))$ -DOCKER_IMAGE_RELEASE_TAG_PATTERN := ^(1|2|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+$ - # ------------------------------------------------------------------------------ # Variables # ------------------------------------------------------------------------------ - -# Docker image/container settings +DIST_PATH ?= ./dist DOCKER_CONTAINER_OPTS ?= DOCKER_IMAGE_TAG ?= latest DOCKER_NAME ?= mysql.1 DOCKER_PORT_MAP_TCP_3306 ?= 3306 DOCKER_RESTART_POLICY ?= always - -# Docker build --no-cache parameter NO_CACHE ?= false - -# Directory path for release packages -DIST_PATH ?= ./dist - -# Number of seconds expected to complete container startup including bootstrap. +RELOAD_SIGNAL ?= HUP STARTUP_TIME ?= 10 # ------------------------------------------------------------------------------ # Application container configuration # ------------------------------------------------------------------------------ -MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP ?= true -MYSQL_AUTOSTART_MYSQLD_WRAPPER ?= true +ENABLE_MYSQLD_BOOTSTRAP ?= true +ENABLE_MYSQLD_WRAPPER ?= true MYSQL_INIT_LIMIT ?= 60 MYSQL_INIT_SQL ?= MYSQL_ROOT_PASSWORD ?= @@ -43,3 +34,4 @@ MYSQL_USER ?= MYSQL_USER_DATABASE ?= MYSQL_USER_PASSWORD ?= MYSQL_USER_PASSWORD_HASHED ?= false +SYSTEM_TIMEZONE ?= UTC diff --git a/src/etc/supervisord.d/mysqld-bootstrap.conf b/src/etc/supervisord.d/20-mysqld-bootstrap.conf similarity index 61% rename from src/etc/supervisord.d/mysqld-bootstrap.conf rename to src/etc/supervisord.d/20-mysqld-bootstrap.conf index 3ebb73b..4e898d1 100644 --- a/src/etc/supervisord.d/mysqld-bootstrap.conf +++ b/src/etc/supervisord.d/20-mysqld-bootstrap.conf @@ -1,10 +1,11 @@ [program:mysqld-bootstrap] autorestart = false -autostart = %(ENV_MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP)s +autostart = %(ENV_ENABLE_MYSQLD_BOOTSTRAP)s command = /usr/sbin/mysqld-bootstrap --verbose -priority = 6 -redirect_stderr = true +priority = 20 startsecs = 0 startretries = 0 +stderr_logfile = /dev/stderr +stderr_logfile_maxbytes = 0 stdout_logfile = /dev/stdout stdout_logfile_maxbytes = 0 diff --git a/src/etc/supervisord.d/mysqld-wrapper.conf b/src/etc/supervisord.d/50-mysqld-wrapper.conf similarity index 51% rename from src/etc/supervisord.d/mysqld-wrapper.conf rename to src/etc/supervisord.d/50-mysqld-wrapper.conf index ba007ff..f3a7563 100644 --- a/src/etc/supervisord.d/mysqld-wrapper.conf +++ b/src/etc/supervisord.d/50-mysqld-wrapper.conf @@ -1,9 +1,10 @@ [program:mysqld-wrapper] autorestart = true -autostart = %(ENV_MYSQL_AUTOSTART_MYSQLD_WRAPPER)s +autostart = %(ENV_ENABLE_MYSQLD_WRAPPER)s command = /usr/sbin/mysqld-wrapper -priority = 100 -redirect_stderr = true -startsecs = 0 +priority = 50 +startsecs = 10 +stderr_logfile = /dev/stderr +stderr_logfile_maxbytes = 0 stdout_logfile = /dev/stdout stdout_logfile_maxbytes = 0 diff --git a/src/etc/systemd/system/centos-ssh-mysql.register@.service b/src/etc/systemd/system/centos-ssh-mysql.register@.service index 9b71319..a47421b 100644 --- a/src/etc/systemd/system/centos-ssh-mysql.register@.service +++ b/src/etc/systemd/system/centos-ssh-mysql.register@.service @@ -41,7 +41,7 @@ # ------------------------------------------------------------------------------ [Unit] -Description=centos-mysql etcd registration // %p@%i +Description=centos-ssh-mysql etcd registration // %p@%i After=etcd.service After=etcd2.service After={{SERVICE_UNIT_NAME}}@%i.service diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index a37d016..be1bd47 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -56,8 +56,8 @@ Environment="DOCKER_IMAGE_PACKAGE_PATH=/var/opt/scmi/packages" Environment="DOCKER_IMAGE_TAG={{RELEASE_VERSION}}" Environment="DOCKER_PORT_MAP_TCP_3306=3306" Environment="DOCKER_USER=jdeathe" -Environment="MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=true" -Environment="MYSQL_AUTOSTART_MYSQLD_WRAPPER=true" +Environment="ENABLE_MYSQLD_BOOTSTRAP=true" +Environment="ENABLE_MYSQLD_WRAPPER=true" Environment="MYSQL_INIT_LIMIT=" Environment="MYSQL_INIT_SQL=" Environment="MYSQL_ROOT_PASSWORD=" @@ -67,6 +67,7 @@ Environment="MYSQL_USER=" Environment="MYSQL_USER_DATABASE=" Environment="MYSQL_USER_PASSWORD=" Environment="MYSQL_USER_PASSWORD_HASHED=false" +Environment="SYSTEM_TIMEZONE=UTC" # Initialisation: Load image from local storage if available, otherwise pull. ExecStartPre=/bin/bash -c \ @@ -123,8 +124,8 @@ ExecStartPre=-/bin/bash -c \ ExecStart=/bin/bash -c \ "exec /usr/bin/docker run \ --name %p.%i \ - --env \"MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP}\" \ - --env \"MYSQL_AUTOSTART_MYSQLD_WRAPPER=${MYSQL_AUTOSTART_MYSQLD_WRAPPER}\" \ + --env \"ENABLE_MYSQLD_BOOTSTRAP=${ENABLE_MYSQLD_BOOTSTRAP}\" \ + --env \"ENABLE_MYSQLD_WRAPPER=${ENABLE_MYSQLD_WRAPPER}\" \ --env \"MYSQL_INIT_LIMIT=${MYSQL_INIT_LIMIT}\" \ --env \"MYSQL_INIT_SQL=${MYSQL_INIT_SQL}\" \ --env \"MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}\" \ @@ -134,6 +135,7 @@ ExecStart=/bin/bash -c \ --env \"MYSQL_USER_DATABASE=${MYSQL_USER_DATABASE}\" \ --env \"MYSQL_USER_PASSWORD=${MYSQL_USER_PASSWORD}\" \ --env \"MYSQL_USER_PASSWORD_HASHED=${MYSQL_USER_PASSWORD_HASHED}\" \ + --env \"SYSTEM_TIMEZONE=${SYSTEM_TIMEZONE}\" \ $(if [[ ${DOCKER_PORT_MAP_TCP_3306} != NULL ]]; \ then \ if /bin/grep -qE \ diff --git a/src/opt/scmi/default.sh b/src/opt/scmi/default.sh index 6e91444..50e2388 100644 --- a/src/opt/scmi/default.sh +++ b/src/opt/scmi/default.sh @@ -46,8 +46,8 @@ fi # Common parameters of create and run targets DOCKER_CONTAINER_PARAMETERS="--name ${DOCKER_NAME} \ --restart ${DOCKER_RESTART_POLICY} \ ---env \"MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP}\" \ ---env \"MYSQL_AUTOSTART_MYSQLD_WRAPPER=${MYSQL_AUTOSTART_MYSQLD_WRAPPER}\" \ +--env \"ENABLE_MYSQLD_BOOTSTRAP=${ENABLE_MYSQLD_BOOTSTRAP}\" \ +--env \"ENABLE_MYSQLD_WRAPPER=${ENABLE_MYSQLD_WRAPPER}\" \ --env \"MYSQL_INIT_LIMIT=${MYSQL_INIT_LIMIT}\" \ --env \"MYSQL_INIT_SQL=${MYSQL_INIT_SQL}\" \ --env \"MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}\" \ @@ -57,4 +57,5 @@ DOCKER_CONTAINER_PARAMETERS="--name ${DOCKER_NAME} \ --env \"MYSQL_USER_DATABASE=${MYSQL_USER_DATABASE}\" \ --env \"MYSQL_USER_PASSWORD=${MYSQL_USER_PASSWORD}\" \ --env \"MYSQL_USER_PASSWORD_HASHED=${MYSQL_USER_PASSWORD_HASHED}\" \ +--env \"SYSTEM_TIMEZONE=${SYSTEM_TIMEZONE}\" \ ${DOCKER_PUBLISH}" diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index ee1ff8d..7f194a8 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -1,43 +1,31 @@ # ------------------------------------------------------------------------------ # Constants # ------------------------------------------------------------------------------ -readonly DOCKER_USER=jdeathe readonly DOCKER_IMAGE_NAME=centos-ssh-mysql - -# Tag validation patterns -readonly DOCKER_IMAGE_TAG_PATTERN='^(latest|centos-6|centos-7-mysql57-community|(([1-2]|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+))$' -readonly DOCKER_IMAGE_RELEASE_TAG_PATTERN='^(1|2|centos-(6-1|7-mysql57-community-2))\.[0-9]+\.[0-9]+$' +readonly DOCKER_IMAGE_RELEASE_TAG_PATTERN='^[1-2]\.[0-9]+\.[0-9]+$' +readonly DOCKER_IMAGE_TAG_PATTERN='^(latest|[1-2]\.[0-9]+\.[0-9]+)$' +readonly DOCKER_USER=jdeathe # ------------------------------------------------------------------------------ # Variables # ------------------------------------------------------------------------------ - -# Docker image/container settings +DIST_PATH="${DIST_PATH:-./dist}" DOCKER_CONTAINER_OPTS="${DOCKER_CONTAINER_OPTS:-}" DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG:-latest}" DOCKER_NAME="${DOCKER_NAME:-mysql.1}" DOCKER_PORT_MAP_TCP_3306="${DOCKER_PORT_MAP_TCP_3306:-3306}" DOCKER_RESTART_POLICY="${DOCKER_RESTART_POLICY:-always}" - -# Docker build --no-cache parameter NO_CACHE="${NO_CACHE:-false}" - -# Directory path for release packages -DIST_PATH="${DIST_PATH:-./dist}" - -# Number of seconds expected to complete container startup including bootstrap. -STARTUP_TIME="${STARTUP_TIME:-10}" - -# ETCD register service settings REGISTER_ETCD_PARAMETERS="${REGISTER_ETCD_PARAMETERS:-}" REGISTER_TTL="${REGISTER_TTL:-120}" REGISTER_UPDATE_INTERVAL="${REGISTER_UPDATE_INTERVAL:-95}" +STARTUP_TIME="${STARTUP_TIME:-10}" # ------------------------------------------------------------------------------ # Application container configuration # ------------------------------------------------------------------------------ -MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP="${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP:-true}" -MYSQL_AUTOSTART_MYSQLD_WRAPPER="${MYSQL_AUTOSTART_MYSQLD_WRAPPER:-true}" +ENABLE_MYSQLD_BOOTSTRAP="${ENABLE_MYSQLD_BOOTSTRAP:-true}" +ENABLE_MYSQLD_WRAPPER="${ENABLE_MYSQLD_WRAPPER:-true}" MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" MYSQL_INIT_SQL="${MYSQL_INIT_SQL:-}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" @@ -47,3 +35,4 @@ MYSQL_USER="${MYSQL_USER:-}" MYSQL_USER_DATABASE="${MYSQL_USER_DATABASE:-}" MYSQL_USER_PASSWORD="${MYSQL_USER_PASSWORD:-}" MYSQL_USER_PASSWORD_HASHED="${MYSQL_USER_PASSWORD_HASHED:-false}" +SYSTEM_TIMEZONE="${SYSTEM_TIMEZONE:-UTC}" diff --git a/src/opt/scmi/service-unit.sh b/src/opt/scmi/service-unit.sh index d2f2b29..c7ee3c1 100644 --- a/src/opt/scmi/service-unit.sh +++ b/src/opt/scmi/service-unit.sh @@ -6,8 +6,8 @@ readonly SERVICE_UNIT_ENVIRONMENT_KEYS=" DOCKER_IMAGE_PACKAGE_PATH DOCKER_IMAGE_TAG DOCKER_PORT_MAP_TCP_3306 - MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP - MYSQL_AUTOSTART_MYSQLD_WRAPPER + ENABLE_MYSQLD_BOOTSTRAP + ENABLE_MYSQLD_WRAPPER MYSQL_INIT_LIMIT MYSQL_INIT_SQL MYSQL_ROOT_PASSWORD @@ -17,6 +17,7 @@ readonly SERVICE_UNIT_ENVIRONMENT_KEYS=" MYSQL_USER_DATABASE MYSQL_USER_PASSWORD MYSQL_USER_PASSWORD_HASHED + SYSTEM_TIMEZONE " readonly SERVICE_UNIT_REGISTER_ENVIRONMENT_KEYS=" REGISTER_ETCD_PARAMETERS diff --git a/src/usr/bin/healthcheck b/src/usr/bin/healthcheck index 5a1308e..fd4c388 100755 --- a/src/usr/bin/healthcheck +++ b/src/usr/bin/healthcheck @@ -1,5 +1,19 @@ #!/usr/bin/env bash +function __get_system_timezone () +{ + local -r default_value="${1:-UTC}" + + local value="${SYSTEM_TIMEZONE}" + + if ! __is_valid_system_timezone "${value}" + then + value="${default_value}" + fi + + printf -- '%s' "${value}" +} + function __have_mysql_access () { local -r database="${3:-mysql}" @@ -61,6 +75,24 @@ function __is_mysql_data_directory_populated () return 1 } +function __is_valid_system_timezone () +{ + __is_valid_zone "${@}" +} + +function __is_valid_zone () +{ + local zone="${1}" + + if [[ -n ${zone} ]] \ + && [[ -f /usr/share/zoneinfo/${zone} ]] + then + return 0 + fi + + return 1 +} + function __last_check_passed () { local -i status=0 @@ -105,10 +137,10 @@ function __print_message () case "${type}" in error) - prefix="[ERROR] " + prefix="ERROR: " ;; info) - prefix="[INFO] " + prefix="INFO: " ;; *) message="${type}" @@ -200,6 +232,12 @@ function main () local -r mysqld="/usr/sbin/mysqld" local -r pattern_seconds_in_minute='^([1-9]|[1-5][0-9]|60)$' local -r redacted_value="********" + local -r system_timezone="$( + __get_system_timezone + )" + local -r zone="$( + system-timezone -qq + )" local QUIET="false" local -i interval=10 @@ -250,8 +288,16 @@ function main () exit 1 fi + if [[ ${system_timezone} != "${zone}" ]] + then + __print_message \ + "error" \ + "system-timezone zone mismatch." + exit 1 + fi + # mysqld-bootstrap - if [[ ${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP} == true ]] + if [[ ${ENABLE_MYSQLD_BOOTSTRAP} == true ]] then if [[ -e /var/lock/subsys/mysqld-bootstrap ]] \ || ! __is_mysql_data_directory_populated @@ -264,7 +310,7 @@ function main () fi # mysqld-wrapper - if [[ ${MYSQL_AUTOSTART_MYSQLD_WRAPPER} == true ]] + if [[ ${ENABLE_MYSQLD_WRAPPER} == true ]] then if ! ps axo command \ | grep -qE "^${mysqld} " diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 0174813..aa1a515 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -1,11 +1,25 @@ #!/usr/bin/env bash +set -e + +function __cleanup () +{ + local -r exit_code="${?}" + + __delete_lock + + if [[ ${exit_code} -eq 0 ]] + then + __create_state + fi +} + # Set MySQL client root user password # - Prefer to store encrypted via mysql_config_editor. # - Fallback to store as plaintext in secured file. function __configure_mysql_client_root_password () { - local -r password="${1:-}" + local -r password="${1}" local config local -a pids @@ -33,8 +47,9 @@ function __configure_mysql_client_root_password () fi trap \ - "rm -rf \"${fifo_path}\";" \ - INT TERM EXIT + "rm -rf \"${fifo_path}\"; \ + __cleanup;" \ + EXIT INT TERM mkfifo \ -m 0600 \ @@ -44,8 +59,9 @@ function __configure_mysql_client_root_password () {mypasswd_fd}<>"${fifo_path}/mypasswd" trap \ - "exec {mypasswd_fd}>&- ;" \ - INT TERM EXIT + "exec {mypasswd_fd}>&- ; \ + __cleanup;" \ + EXIT INT TERM rm -rf \ "${fifo_path}" @@ -62,8 +78,9 @@ function __configure_mysql_client_root_password () trap \ "exec {mypasswd_fd}>&- ; \ - [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]};" \ - INT TERM EXIT + [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]}; \ + __cleanup" \ + EXIT INT TERM printf -- \ '%s\n' \ @@ -75,8 +92,9 @@ function __configure_mysql_client_root_password () trap \ "exec {mypasswd_fd}>&- ; \ [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]}; \ - [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]};" \ - INT TERM EXIT + [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]}; \ + __cleanup;" \ + EXIT INT TERM if [[ ${pids[1]} -gt 0 ]] then @@ -90,8 +108,10 @@ function __configure_mysql_client_root_password () exit 1 fi - exec \ - {mypasswd_fd}>&- + trap - \ + EXIT INT TERM + + exec {mypasswd_fd}>&- else tee /root/.my.cnf &> /dev/null \ <<-EOT @@ -111,9 +131,30 @@ function __configure_mysql_client_root_password () "${config//'{{MYSQL_ROOT_PASSWORD}}'/${password}}" \ > /root/.my.cnf fi +} - trap - \ - INT TERM EXIT +function __create_lock () +{ + if [[ -n ${lock_file} ]] + then + touch "${lock_file}" + fi +} + +function __create_state () +{ + if [[ -n ${state_file} ]] + then + touch "${state_file}" + fi +} + +function __delete_lock () +{ + if [[ -f ${lock_file} ]] + then + rm -f "${lock_file}" + fi } function __get_mysql_init_limit () @@ -159,7 +200,6 @@ function __get_mysql_init_template () local template_user="-- Create user" local user - # Parse options while [[ "${#}" -gt 0 ]] do case "${1}" in @@ -219,8 +259,9 @@ function __get_mysql_init_template () fi trap \ - "rm -rf \"${fifo_path}\";" \ - INT TERM EXIT + "rm -rf \"${fifo_path}\"; \ + __cleanup;" \ + EXIT INT TERM mkfifo \ -m 0600 \ @@ -278,7 +319,7 @@ function __get_mysql_init_template () then cat <<-EOF & $(<"${fifo_path}"/mysql-init-template) - + EOF else cat -s \ @@ -292,6 +333,7 @@ function __get_mysql_init_template () '{ ORS=( /;$/ ? RS:FS ) } 1' \ & $(<"${fifo_path}"/mysql-init-template) + EOF fi pids[1]="${!}" @@ -299,7 +341,7 @@ function __get_mysql_init_template () # Generate the initialisation SQL template cat \ <<-EOT > "${fifo_path}"/mysql-init-template & - + -- ============================================================================= -- Initialisation SQL -- ----------------------------------------------------------------------------- @@ -328,8 +370,9 @@ function __get_mysql_init_template () trap \ "rm -rf \"${fifo_path}\"; \ [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]}; \ - [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]};" \ - INT TERM EXIT + [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]}; \ + __cleanup;" \ + EXIT INT TERM if [[ ${pids[1]} -gt 0 ]] then @@ -347,7 +390,7 @@ function __get_mysql_init_template () "${fifo_path}" trap - \ - INT TERM EXIT + EXIT INT TERM } function __get_mysql_root_password () @@ -511,10 +554,35 @@ function __get_password () printf -- '%s' "${password}" } +function __get_timer_total () +{ + local -r timer_end="$( + date -u +%s.%N + )" + local -r timer_start="${1}" + + if [[ -z ${timer_start} ]] \ + || [[ ${timer_start//.} -gt ${timer_end//.} ]] + then + >&2 printf -- \ + 'ERROR: invalid timer start: %s\n' \ + "${timer_start}" + printf -- \ + '0.000000' + else + awk \ + -v timer_end="${timer_end}" \ + -v timer_start="${timer_start}" \ + 'BEGIN { print \ + timer_end - timer_start; + }' + fi +} + function __have_mysql_access () { local -r database="${3:-mysql}" - local -r password="${2:-}" + local -r password="${2}" local -r user="${1:-root}" if [[ ${user} == root ]] @@ -578,13 +646,13 @@ function __init_mysql () if command -v mysqld_safe &> /dev/null then mysqld_safe \ - --skip-networking \ - --init-file="${init_file}" + --init-file="${init_file}" \ + --skip-networking else mysqld \ + --init-file="${init_file}" \ --pid-file=/var/run/mysqld/mysqld.pid \ - --skip-networking \ - --init-file="${init_file}" + --skip-networking fi } @@ -803,9 +871,9 @@ function main () local -r init_file="/tmp/mysql-init" local -r lock_file="/var/lock/subsys/mysqld-bootstrap" local -r redacted_value="********" - local -r server_key="${datadir}"/server-key.pem + local -r state_file="/var/lib/misc/mysqld-bootstrap" local -r timer_start="$( - date +%s.%N + date -u +%s.%N )" local counter @@ -821,15 +889,11 @@ function main () local mysql_user_password local mysql_user_password_hashed local -a pids + local server_key local server_key_path local user_details local verbose="false" - # Create lock - touch \ - "${lock_file}" - - # Parse options while [[ "${#}" -gt 0 ]] do case "${1}" in @@ -840,12 +904,36 @@ function main () esac done + if [[ -f ${state_file} ]] + then + if [[ ${verbose} == true ]] + then + printf -- \ + 'INFO: %s finished - skipping.\n' \ + "${0##*/}" + fi + exit 0 + fi + + if [[ -f ${lock_file} ]] + then + >&2 printf -- \ + 'ERROR: %s lock detected - aborting.\n' \ + "${0##*/}" + exit 1 + fi + + trap "__cleanup" \ + EXIT INT TERM + __create_lock + datadir="$( __get_option \ mysqld \ datadir \ "/var/lib/mysql" )" + server_key="${datadir}"/server-key.pem # Initialisation is a one-shot process. if ! __is_mysql_datadir_populated "${datadir}" @@ -874,14 +962,15 @@ function main () fi trap \ - "rm -rf \"${server_key_path}\";" \ - INT TERM EXIT + "rm -rf \"${server_key_path}\"; \ + __cleanup;" \ + EXIT INT TERM /usr/bin/mysql_ssl_rsa_setup \ --datadir="${server_key_path}" \ --uid=mysql \ - &> /dev/null \ - & + 2> /dev/null \ + & pids[2]="${!}" fi @@ -934,17 +1023,6 @@ function main () & pids[1]="${!}" - # Initialisation template output - if [[ ${verbose} == true ]] - then - __get_mysql_init_template \ - --user="${mysql_user}" \ - --database="${mysql_user_database}" \ - --host="${mysql_user_host}" \ - --password-hashed="${mysql_user_password_hashed}" \ - --init-sql="${mysql_init_sql}" - fi - # Replace compact template placeholders with values init_template="$( __get_mysql_init_template \ @@ -955,6 +1033,12 @@ function main () --init-sql="${mysql_init_sql}" \ --compact )" + + trap \ + "rm -rf \"${server_key_path}\"; \ + __cleanup;" \ + EXIT INT TERM + init_template="${init_template//'{{MYSQL_USER}}'/${mysql_user}}" init_template="${init_template//'{{MYSQL_USER_DATABASE}}'/${mysql_user_database}}" init_template="${init_template//'{{MYSQL_USER_HOST}}'/${mysql_user_host}}" @@ -968,8 +1052,9 @@ function main () trap \ "rm -rf \"${server_key_path}\"; \ - rm -f "${init_file}";" \ - INT TERM EXIT + rm -f \"${init_file}\"; \ + __cleanup;" \ + EXIT INT TERM # Wait to complete system table installation if [[ ${pids[0]} -gt 0 ]] @@ -981,6 +1066,20 @@ function main () then printf -- \ "Initialising MySQL.\n" + + # Initialisation template output + __get_mysql_init_template \ + --user="${mysql_user}" \ + --database="${mysql_user_database}" \ + --host="${mysql_user_host}" \ + --password-hashed="${mysql_user_password_hashed}" \ + --init-sql="${mysql_init_sql}" + + trap \ + "rm -rf \"${server_key_path}\"; \ + rm -f \"${init_file}\"; \ + __cleanup;" \ + EXIT INT TERM fi __init_mysql \ "${init_file}" \ @@ -989,6 +1088,12 @@ function main () __configure_mysql_client_root_password \ "${mysql_root_password}" + trap \ + "rm -rf \"${server_key_path}\"; \ + rm -f \"${init_file}\"; \ + __cleanup;" \ + EXIT INT TERM + # Wait for initialisation to complete (poll for access) or timeout counter="$(( 2 * mysql_init_limit @@ -1062,8 +1167,9 @@ function main () "${init_file}" trap \ - "rm -rf \"${server_key_path}\";" \ - INT TERM EXIT + "rm -rf \"${server_key_path}\"; \ + __cleanup;" \ + EXIT INT TERM fi # Local root user details @@ -1125,21 +1231,15 @@ function main () rm -rf \ "${server_key_path}" - trap - \ - INT TERM EXIT + trap "__cleanup" \ + EXIT INT TERM fi if [[ ${verbose} == true ]] then timer_total="$( - awk \ - -v timer_end="$( - date +%s.%N - )" \ - -v timer_start="${timer_start}" \ - 'BEGIN { print \ - timer_end - timer_start; - }' + __get_timer_total \ + "${timer_start}" )" cat \ @@ -1169,9 +1269,8 @@ function main () fi fi - # Release lock - rm -f \ - "${lock_file}" + # Trigger cleanup trap. + exit 0 } main "${@}" diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index 49a8fda..a72c724 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -2,26 +2,34 @@ set -e -function __is_valid_mysql_autostart_mysqld_bootstrap () +function __cleanup () { - local -r boolean_value='^(true|false)$' - local -r value="${1}" + __delete_lock +} - if [[ ${value} =~ ${boolean_value} ]] +function __create_lock () +{ + if [[ -n ${lock_file} ]] then - return 0 + touch "${lock_file}" fi +} - return 1 +function __delete_lock () +{ + if [[ -f ${lock_file} ]] + then + rm -f "${lock_file}" + fi } -function __get_mysql_autostart_mysqld_bootstrap () +function __get_mysql_init_limit () { - local -r default_value="${1:-true}" + local -r default_value="${1:-60}" - local value="${MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP}" + local value="${MYSQL_INIT_LIMIT}" - if ! __is_valid_mysql_autostart_mysqld_bootstrap "${value}" + if ! __is_valid_mysql_init_limit "${value}" then value="${default_value}" fi @@ -29,31 +37,112 @@ function __get_mysql_autostart_mysqld_bootstrap () printf -- '%s' "${value}" } +function __get_options () +{ + local -r options="${1}" + local -r pid_file="/var/run/mysqld/mysqld.pid" + + printf -- \ + '--pid-file=%s%s%s' \ + "${pid_file}" \ + "${options:+" "}" \ + "${options}" +} + +function __is_valid_mysql_init_limit () +{ + local -r non_zero_integer='^[1-9][0-9]*$' + local -r value="${1}" + + if [[ ${value} =~ ${non_zero_integer} ]] + then + return 0 + fi + + return 1 +} + function main () { - local -r autostart_bootstrap="$( - __get_mysql_autostart_mysqld_bootstrap - )" local -r bin="/usr/sbin/mysqld" - local -r lock_file="/var/lock/subsys/mysqld-bootstrap" + local -r bootstrap_state_file="/var/lib/misc/mysqld-bootstrap" + local -r bootstrap_timeout="$(( + __get_mysql_init_limit + 10 + ))" + local -r lock_file="/var/lock/subsys/mysqld-wrapper" local -r nice="/bin/nice" local -r niceness="10" - local -r options="--pid-file=/var/run/mysqld/mysqld.pid" - if [[ ${autostart_bootstrap} == false ]] + local options + local verbose="false" + + while [[ "${#}" -gt 0 ]] + do + case "${1}" in + -v|--verbose) + verbose="true" + shift 1 + ;; + esac + done + + if [[ -f ${lock_file} ]] + then + >&2 printf -- \ + 'ERROR: %s lock detected - aborting\n' \ + "${0##*/}" + exit 1 + fi + + trap __cleanup \ + EXIT INT TERM + __create_lock + + options="$( + __get_options + )" + + if [[ ${verbose} == true ]] then - # block. - sleep infinity + printf -- \ + 'INFO: %s waiting on %s\n' \ + "${0##*/}" \ + "${bootstrap_state_file##*/}" fi - while true + set +e + until [[ -f ${bootstrap_state_file} ]] do - sleep 0.1 - if [[ ! -e ${lock_file} ]] + if ! inotifywait -qq \ + -e "create" \ + -t "${bootstrap_timeout}" \ + "${bootstrap_state_file%/*}" then break fi done + set -e + + if ! [[ -f ${bootstrap_state_file} ]] + then + >&2 printf -- \ + 'ERROR: %s timed out waiting on %s\n' \ + "${0##*/}" \ + "${bootstrap_state_file##*/}" + exit 1 + fi + + if [[ ${verbose} == true ]] + then + printf -- \ + 'INFO: %s starting %s\n' \ + "${0##*/}" \ + "${bin##*/}" + fi + + __cleanup + trap - \ + EXIT INT TERM exec ${nice} \ -n ${niceness} \ diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 12e8206..b170b31 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -589,8 +589,8 @@ function test_custom_configuration () --detach \ --name mysql.3 \ --network ${private_network_1} \ - --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ - --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ + --env "ENABLE_MYSQLD_BOOTSTRAP=false" \ + --env "ENABLE_MYSQLD_WRAPPER=false" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -955,8 +955,8 @@ function test_custom_configuration () --detach \ --name mysql.5 \ --network ${private_network_2} \ - --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ - --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ + --env "ENABLE_MYSQLD_BOOTSTRAP=false" \ + --env "ENABLE_MYSQLD_WRAPPER=false" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -1096,8 +1096,8 @@ function test_custom_configuration () --detach \ --name mysql.7 \ --network ${private_network_1} \ - --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ - --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ + --env "ENABLE_MYSQLD_BOOTSTRAP=false" \ + --env "ENABLE_MYSQLD_WRAPPER=false" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -1165,7 +1165,7 @@ function test_custom_configuration () docker run \ --detach \ --name mysql.1 \ - --env "MYSQL_AUTOSTART_MYSQLD_BOOTSTRAP=false" \ + --env "ENABLE_MYSQLD_BOOTSTRAP=false" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -1193,7 +1193,7 @@ function test_custom_configuration () docker run \ --detach \ --name mysql.1 \ - --env "MYSQL_AUTOSTART_MYSQLD_WRAPPER=false" \ + --env "ENABLE_MYSQLD_WRAPPER=false" \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null From 97e46a81fc9d0dab1884362d83c05d69f1cbb9da Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 4 Jul 2019 22:05:18 +0100 Subject: [PATCH 71/98] #247: Update test case for unhealthy health status. --- test/shpec/operation_shpec.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index b170b31..cc59f4b 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -1312,7 +1312,7 @@ function test_healthcheck () -v event_lag="${event_lag_seconds}" \ -v interval="${interval_seconds}" \ -v retries="${retries}" \ - 'BEGIN { print event_lag + (interval * retries); }' + 'BEGIN { print (2 * event_lag) + (interval * retries); }' )" health_status="$( From 3489c14ffd3bc0f8be7d6a75e11db334bd64ff58 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 4 Jul 2019 22:19:53 +0100 Subject: [PATCH 72/98] #247: Removes unnecessary use of 'FLUSH PRIVILEGES' SQL. --- CHANGELOG.md | 1 + src/usr/sbin/mysqld-bootstrap | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cd931b..0a97670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Summary of release changes. - Removes `MYSQL_AUTOSTART_MYSQL_BOOTSTRAP`, replaced with `ENABLE_MYSQL_BOOTSTRAP`. - Removes `MYSQL_AUTOSTART_MYSQL_WRAPPER`, replaced with `ENABLE_MYSQL_WRAPPER`. - Removes support for long image tags (i.e. centos-7-mysql57-community-2.x.x). +- Removes unnecessary use of `FLUSH PRIVILEGES` in intitialisation SQL. ### 2.2.0 - 2019-03-18 diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index aa1a515..0a66cec 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -362,7 +362,6 @@ function __get_mysql_init_template () GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY '{{MYSQL_ROOT_PASSWORD}}' WITH GRANT OPTION; - FLUSH PRIVILEGES; -- ----------------------------------------------------------------------------- EOT pids[2]="${!}" @@ -1118,8 +1117,7 @@ function main () -e "UPDATE mysql.user \ SET authentication_string = '${mysql_root_password}' \ WHERE User = 'root' \ - AND Host = 'localhost'; \ - FLUSH PRIVILEGES;" + AND Host = 'localhost';" fi rm -f \ From 340883b206534427217f57f0716c3cce9965b6c6 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 4 Jul 2019 23:11:15 +0100 Subject: [PATCH 73/98] #247: Adds all auto-generated tls/ssl pem files to data directory and log to stderr. --- CHANGELOG.md | 2 ++ src/etc/my.cnf | 1 - src/usr/sbin/mysqld-bootstrap | 27 ++++++++++++--------------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a97670..87301ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,12 @@ Summary of release changes. - Adds `SYSTEM_TIMEZONE` handling to Makefile, scmi, systemd unit and docker-compose templates. - Adds system time zone validation to healthcheck. - Adds lock/state file to bootstrap/wrapper scripts. +- Adds all necessary auto-generated TLS/SSL pem files to data directory during bootstrap. - Removes `MYSQL_AUTOSTART_MYSQL_BOOTSTRAP`, replaced with `ENABLE_MYSQL_BOOTSTRAP`. - Removes `MYSQL_AUTOSTART_MYSQL_WRAPPER`, replaced with `ENABLE_MYSQL_WRAPPER`. - Removes support for long image tags (i.e. centos-7-mysql57-community-2.x.x). - Removes unnecessary use of `FLUSH PRIVILEGES` in intitialisation SQL. +- Removes `log-error=/var/log/mysqld.log` from default configuration; log to stderr. ### 2.2.0 - 2019-03-18 diff --git a/src/etc/my.cnf b/src/etc/my.cnf index 6e1f746..063c76a 100644 --- a/src/etc/my.cnf +++ b/src/etc/my.cnf @@ -6,7 +6,6 @@ default-character-set=utf8 datadir=/var/lib/mysql socket=/var/run/mysqld/mysql.sock pid-file=/var/run/mysqld/mysqld.pid -log-error=/var/log/mysqld.log user=mysql symbolic-links=0 diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 0a66cec..647bb11 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -625,7 +625,8 @@ function __init_datadir () --force \ --skip-name-resolve \ --skip-networking \ - --tmpdir="${directory}" + --tmpdir="${directory}" \ + 2> /dev/null else mysqld \ --datadir="${directory}" \ @@ -634,7 +635,8 @@ function __init_datadir () --skip-name-resolve \ --skip-networking \ --tmpdir="${directory}" \ - --user=mysql + --user=mysql \ + 2> /dev/null fi } @@ -646,12 +648,14 @@ function __init_mysql () then mysqld_safe \ --init-file="${init_file}" \ - --skip-networking + --skip-networking \ + 2> /dev/null else mysqld \ --init-file="${init_file}" \ --pid-file=/var/run/mysqld/mysqld.pid \ - --skip-networking + --skip-networking \ + 2> /dev/null fi } @@ -888,7 +892,6 @@ function main () local mysql_user_password local mysql_user_password_hashed local -a pids - local server_key local server_key_path local user_details local verbose="false" @@ -932,14 +935,13 @@ function main () datadir \ "/var/lib/mysql" )" - server_key="${datadir}"/server-key.pem # Initialisation is a one-shot process. if ! __is_mysql_datadir_populated "${datadir}" then # Certificate generation if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ - && [[ ! -e ${server_key} ]] + && [[ ! -e ${datadir}/server-key.pem ]] then if [[ ${verbose} == true ]] then @@ -1223,14 +1225,9 @@ function main () # Finalise certificate setup if [[ -f ${server_key_path}/server-key.pem ]] then - mv \ - "${server_key_path}"/server-key.pem \ - "${server_key}" - rm -rf \ - "${server_key_path}" - - trap "__cleanup" \ - EXIT INT TERM + mv -f \ + "${server_key_path}"/*.pem \ + "${datadir}"/ fi if [[ ${verbose} == true ]] From 6b92a8e48bccc5fcc837836b301f01f73babd60e Mon Sep 17 00:00:00 2001 From: James Deathe Date: Thu, 4 Jul 2019 23:12:07 +0100 Subject: [PATCH 74/98] #247: Adds verbose logging output to mysqld-wrapper. --- src/etc/supervisord.d/50-mysqld-wrapper.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etc/supervisord.d/50-mysqld-wrapper.conf b/src/etc/supervisord.d/50-mysqld-wrapper.conf index f3a7563..58361d8 100644 --- a/src/etc/supervisord.d/50-mysqld-wrapper.conf +++ b/src/etc/supervisord.d/50-mysqld-wrapper.conf @@ -1,7 +1,7 @@ [program:mysqld-wrapper] autorestart = true autostart = %(ENV_ENABLE_MYSQLD_WRAPPER)s -command = /usr/sbin/mysqld-wrapper +command = /usr/sbin/mysqld-wrapper --verbose priority = 50 startsecs = 10 stderr_logfile = /dev/stderr From b6a0eee66500e74d5aacfa01bcb913f93b9e9ed0 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 5 Jul 2019 00:09:23 +0100 Subject: [PATCH 75/98] #247: Removes stderr output from test cases checking docker logs output. --- test/shpec/operation_shpec.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index cc59f4b..0a3af81 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -316,6 +316,7 @@ function test_basic_operations () mysql_root_password="$( docker logs \ mysql.1 \ + 2> /dev/null \ | grep 'user : root@localhost' \ | sed -e 's~^.*,.*password : \([^ ,:]*\).*$~\1~' )" @@ -627,6 +628,7 @@ function test_custom_configuration () mysql_root_password_log="$( docker logs \ mysql.2 \ + 2> /dev/null \ | grep 'user : root@localhost' \ | sed -e 's~^.*,.*password : \([^ ,:]*\).*$~\1~' )" @@ -640,6 +642,7 @@ function test_custom_configuration () mysql_user_password_log="$( docker logs \ mysql.2 \ + 2> /dev/null \ | grep 'user : app-user@172.172.40.0/255.255.255.0' \ | sed -e 's~^.*,.*password : \([^ ,:]*\).*$~\1~' )" From e0a074550825a485e5515bc9c1a860d270924ca0 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 5 Jul 2019 00:24:14 +0100 Subject: [PATCH 76/98] #247: Removes stderr output from test cases checking docker logs output. --- test/shpec/operation_shpec.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index 0a3af81..c04bd6a 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -379,6 +379,7 @@ function test_basic_operations () it "Shows N/A in MySQL Details." docker logs \ mysql.1 \ + 2> /dev/null \ | grep -q 'database : N/A' \ &> /dev/null @@ -430,6 +431,7 @@ function test_basic_operations () it "Has database name in MySQL Details." docker logs \ mysql.1 \ + 2> /dev/null \ | grep -q 'database : my-db' \ &> /dev/null @@ -493,6 +495,7 @@ function test_basic_operations () it "Has user in MySQL Details." docker logs \ mysql.1 \ + 2> /dev/null \ | grep -q 'user : my-user@localhost' \ &> /dev/null @@ -615,6 +618,7 @@ function test_custom_configuration () it "Has the database name." docker logs \ mysql.2 \ + 2> /dev/null \ | grep -q 'database : app-db' \ &> /dev/null @@ -1181,8 +1185,10 @@ function test_custom_configuration () fi it "Can disable mysqld-bootstrap." - docker logs mysql.1 \ - | grep -qE 'INFO success: mysqld-bootstrap entered RUNNING state' + docker logs \ + mysql.1 \ + 2> /dev/null \ + | grep -qE 'INFO success: mysqld-bootstrap entered RUNNING state' assert equal \ "${?}" \ From c5d99062b5cd1b17e2e975cea023a65ea668bb02 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 5 Jul 2019 02:41:05 +0100 Subject: [PATCH 77/98] #247: Adds improved handling of previously initialised datadir + improved logging output. --- CHANGELOG.md | 1 + src/usr/sbin/mysqld-bootstrap | 211 ++++++++++++++++++++-------------- 2 files changed, 123 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87301ee..082d0db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Summary of release changes. - Updates supervisord configuration to send error log output to stderr. - Updates bootstrap supervisord configuration file/priority to `20-mysqld-bootstrap.conf`/`20`. - Updates wrapper supervisord configuration file/priority to `50-mysqld-wrapper.conf`/`50`. +- Updates bootstrap with better handling of previously initialised `datadir`. - Fixes docker host connection status check in Makefile. - Adds `inspect`, `reload` and `top` Makefile targets. - Adds improved `clean` Makefile target; includes exited containers and dangling images. diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 647bb11..f326ede 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -39,10 +39,9 @@ function __configure_mysql_client_root_password () if [[ ${?} -ne 0 ]] then >&2 printf -- \ - '%s %s %s\n' \ - "ERROR: Failed to create directory" \ - "${fifo_path}" \ - "- aborting." + 'ERROR: %s failed to create directory %s - aborting\n' \ + "${0##*/}" \ + "${fifo_path}" exit 1 fi @@ -104,7 +103,8 @@ function __configure_mysql_client_root_password () if [[ ${?} -ne 0 ]] then >&2 printf -- \ - "ERROR: Unable to set MySQL root password - aborting.\n" + 'ERROR: %s failed to set client root password - aborting\n' \ + "${0##*/}" exit 1 fi @@ -236,10 +236,7 @@ function __get_mysql_init_template () shift 1 ;; *) - >&2 printf -- \ - 'ERROR: Unknown option %s\n' \ - "${1}" - return 1 + shift 1 ;; esac done @@ -251,10 +248,9 @@ function __get_mysql_init_template () if [[ ${?} -ne 0 ]] then >&2 printf -- \ - '%s %s %s\n' \ - "ERROR: Failed to create directory" \ - "${fifo_path}" \ - "- aborting." + 'ERROR: %s failed to create directory %s - aborting\n' \ + "${0##*/}" + "${fifo_path}" exit 1 fi @@ -381,7 +377,8 @@ function __get_mysql_init_template () if [[ ${?} -ne 0 ]] then >&2 printf -- \ - "ERROR: Unable to gererate MySQL init template - aborting.\n" + 'ERROR: %s failed to generate mysql-init-template - aborting\n' \ + "${0##*/}" exit 1 fi @@ -564,7 +561,8 @@ function __get_timer_total () || [[ ${timer_start//.} -gt ${timer_end//.} ]] then >&2 printf -- \ - 'ERROR: invalid timer start: %s\n' \ + 'ERROR: %s invalid timer start: %s\n' \ + "${0##*/}" "${timer_start}" printf -- \ '0.000000' @@ -911,7 +909,7 @@ function main () if [[ ${verbose} == true ]] then printf -- \ - 'INFO: %s finished - skipping.\n' \ + 'INFO: %s finished - skipping\n' \ "${0##*/}" fi exit 0 @@ -920,7 +918,7 @@ function main () if [[ -f ${lock_file} ]] then >&2 printf -- \ - 'ERROR: %s lock detected - aborting.\n' \ + 'ERROR: %s lock detected - aborting\n' \ "${0##*/}" exit 1 fi @@ -935,82 +933,114 @@ function main () datadir \ "/var/lib/mysql" )" + mysql_init_limit="$( + __get_mysql_init_limit + )" + mysql_init_sql="$( + __get_mysql_init_sql + )" + mysql_root_password="$( + __get_mysql_root_password + )" + mysql_root_password_hashed="$( + __get_mysql_root_password_hashed + )" + mysql_user="$( + __get_mysql_user + )" + mysql_user_database="$( + __get_mysql_user_database + )" + + # User dependent + if [[ -n ${mysql_user} ]] + then + mysql_user_host="$( + __get_mysql_user_host + )" + mysql_user_password="$( + __get_mysql_user_password + )" + mysql_user_password_hashed="$( + __get_mysql_user_password_hashed + )" + fi + + # Certificate generation + if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ + && [[ ! -e ${datadir}/server-key.pem ]] + then + if [[ ${verbose} == true ]] + then + printf -- \ + 'INFO: %s generating certificates\n' \ + "${0##*/}" + fi + server_key_path="$( + mktemp -d + )" + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + 'ERROR: %s failed to create directory %s - aborting\n' \ + "${0##*/}" \ + "${server_key_path}" + exit 1 + fi + + trap \ + "rm -rf \"${server_key_path}\"; \ + __cleanup;" \ + EXIT INT TERM + + /usr/bin/mysql_ssl_rsa_setup \ + --datadir="${server_key_path}" \ + --uid=mysql \ + 2> /dev/null \ + & + pids[2]="${!}" + fi # Initialisation is a one-shot process. - if ! __is_mysql_datadir_populated "${datadir}" + if __is_mysql_datadir_populated "${datadir}" then - # Certificate generation - if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ - && [[ ! -e ${datadir}/server-key.pem ]] + if [[ ${verbose} == true ]] then - if [[ ${verbose} == true ]] - then - printf -- \ - "Generating MySQL certificates.\n" - fi - server_key_path="$( - mktemp -d - )" + printf -- \ + 'INFO: %s skipping data directory initialisation\n' \ + "${0##*/}" + fi - if [[ ${?} -ne 0 ]] - then - >&2 printf -- \ - '%s %s %s\n' \ - "ERROR: Failed to create directory" \ - "${server_key_path}" \ - "- aborting." - exit 1 - fi + if [[ ${mysql_root_password_hashed} != true ]] + then + __configure_mysql_client_root_password \ + "${mysql_root_password}" + fi + # Restore appropriate trap + if [[ -d ${server_key_path} ]] + then trap \ "rm -rf \"${server_key_path}\"; \ __cleanup;" \ EXIT INT TERM - - /usr/bin/mysql_ssl_rsa_setup \ - --datadir="${server_key_path}" \ - --uid=mysql \ - 2> /dev/null \ - & - pids[2]="${!}" + else + trap "__cleanup;" \ + EXIT INT TERM fi - mysql_init_limit="$( - __get_mysql_init_limit - )" - mysql_init_sql="$( - __get_mysql_init_sql - )" - mysql_root_password="$( - __get_mysql_root_password - )" - mysql_root_password_hashed="$( - __get_mysql_root_password_hashed - )" - mysql_user="$( - __get_mysql_user - )" - mysql_user_database="$( - __get_mysql_user_database - )" - # User dependent - if [[ -n ${mysql_user} ]] + # Wait to complete certificate generation + if [[ ${pids[2]} -gt 0 ]] then - mysql_user_host="$( - __get_mysql_user_host - )" - mysql_user_password="$( - __get_mysql_user_password - )" - mysql_user_password_hashed="$( - __get_mysql_user_password_hashed - )" + wait ${pids[2]} fi - + else if [[ ${verbose} == true ]] then printf -- \ - "Initialising MySQL data directory.\n" + 'INFO: %s initialising data directory\n' \ + "${0##*/}" fi __init_datadir \ "${datadir}" \ @@ -1066,7 +1096,8 @@ function main () if [[ ${verbose} == true ]] then printf -- \ - "Initialising MySQL.\n" + 'INFO: %s initialising\n' \ + "${0##*/}" # Initialisation template output __get_mysql_init_template \ @@ -1134,9 +1165,9 @@ function main () if [[ ${counter} -eq 0 ]] then - >&2 printf \ - -- "MySQL initilisation failed after %s seconds.\n" \ - "${mysql_init_limit}" + >&2 printf -- \ + 'ERROR: %s initilisation timed out - aborting\n' \ + "${0##*/}" killall \ -15 \ @@ -1147,7 +1178,8 @@ function main () if [[ ${verbose} == true ]] then printf -- \ - "Stopping MySQL.\n" + 'INFO: %s stopping mysqld\n' \ + "${0##*/}" fi # Prefer mysqladmin shutdown method if password is known @@ -1222,14 +1254,6 @@ function main () wait ${pids[2]} fi - # Finalise certificate setup - if [[ -f ${server_key_path}/server-key.pem ]] - then - mv -f \ - "${server_key_path}"/*.pem \ - "${datadir}"/ - fi - if [[ ${verbose} == true ]] then timer_total="$( @@ -1264,6 +1288,15 @@ function main () fi fi + # Finalise certificate setup + if [[ -d ${server_key_path} ]] \ + && [[ -f ${server_key_path}/server-key.pem ]] + then + mv -f \ + "${server_key_path}"/*.pem \ + "${datadir}"/ + fi + # Trigger cleanup trap. exit 0 } From 4ed3e00a580321c231fddb4717904c31dbb4e435 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 5 Jul 2019 02:44:03 +0100 Subject: [PATCH 78/98] #247: Adds note in docker-compose example about requirement for MYSQL_ROOT_PASSWORD. --- CHANGELOG.md | 3 ++- docker-compose.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 082d0db..0c7ffab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,6 @@ Summary of release changes. - Updates supervisord configuration to send error log output to stderr. - Updates bootstrap supervisord configuration file/priority to `20-mysqld-bootstrap.conf`/`20`. - Updates wrapper supervisord configuration file/priority to `50-mysqld-wrapper.conf`/`50`. -- Updates bootstrap with better handling of previously initialised `datadir`. - Fixes docker host connection status check in Makefile. - Adds `inspect`, `reload` and `top` Makefile targets. - Adds improved `clean` Makefile target; includes exited containers and dangling images. @@ -24,6 +23,8 @@ Summary of release changes. - Adds system time zone validation to healthcheck. - Adds lock/state file to bootstrap/wrapper scripts. - Adds all necessary auto-generated TLS/SSL pem files to data directory during bootstrap. +- Adds improved bootstrap handling of previously initialised datadir. +- Adds a note in example docker-compose.yml that `MYSQL_ROOT_PASSWORD` is required when using a data volume. - Removes `MYSQL_AUTOSTART_MYSQL_BOOTSTRAP`, replaced with `ENABLE_MYSQL_BOOTSTRAP`. - Removes `MYSQL_AUTOSTART_MYSQL_WRAPPER`, replaced with `ENABLE_MYSQL_WRAPPER`. - Removes support for long image tags (i.e. centos-7-mysql57-community-2.x.x). diff --git a/docker-compose.yml b/docker-compose.yml index 1bb627b..72e658e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ # # Setup: # Copy .env.example to .env and modify values as required. +# MYSQL_ROOT_PASSWORD value is required when using a persistent data volume. # docker-compose build # docker-compose down # From 7f60ad501d907a934f51450f75dca4cf4c23dbcf Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 5 Jul 2019 15:19:16 +0100 Subject: [PATCH 79/98] #247: Adds previously undeclared local variables. --- src/usr/sbin/mysqld-bootstrap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index f326ede..2354859 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -22,6 +22,8 @@ function __configure_mysql_client_root_password () local -r password="${1}" local config + local fifo_path + local mypasswd_fd local -a pids if [[ -z ${password} ]] @@ -190,6 +192,7 @@ function __get_mysql_init_template () { local compact="false" local database + local fifo_path local host local init_sql="-- Custom Initialisation SQL" local password_hashed="false" From fc09e606bc29d3762531d16fc6118f0e665775c1 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 6 Jul 2019 11:31:07 +0100 Subject: [PATCH 80/98] #247: Adds more restrictive permissions to MySQL init-file path. --- src/usr/sbin/mysqld-bootstrap | 51 ++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 2354859..5f389a2 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -645,6 +645,15 @@ function __init_mysql () { local -r init_file="${1:-/tmp/mysql-init}" + if [[ ! -f ${init_file} ]] + then + >&2 printf -- \ + 'ERROR: %s invalid --init-file: %s\n' \ + "${0##*/}" + "${init_file}" + exit 1 + fi + if command -v mysqld_safe &> /dev/null then mysqld_safe \ @@ -872,7 +881,6 @@ function __map_mysql_service_user_to_datadir () function main () { - local -r init_file="/tmp/mysql-init" local -r lock_file="/var/lock/subsys/mysqld-bootstrap" local -r redacted_value="********" local -r state_file="/var/lib/misc/mysqld-bootstrap" @@ -882,6 +890,8 @@ function main () local counter local datadir + local init_file + local init_path local init_template local mysql_init_limit local mysql_init_sql @@ -1079,17 +1089,37 @@ function main () init_template="${init_template//'{{MYSQL_USER_PASSWORD}}'/${mysql_user_password}}" init_template="${init_template//'{{MYSQL_ROOT_PASSWORD}}'/${mysql_root_password}}" - printf -- \ - '%s\n' \ - "${init_template}" \ - > "${init_file}" + init_path="$( + mktemp -d + )" + + if [[ ${?} -ne 0 ]] + then + >&2 printf -- \ + 'ERROR: %s failed to create directory %s - aborting\n' \ + "${0##*/}" \ + "${init_path}" + exit 1 + fi trap \ "rm -rf \"${server_key_path}\"; \ - rm -f \"${init_file}\"; \ + rm -rf \"${init_path}\"; \ __cleanup;" \ EXIT INT TERM + chmod 750 \ + "${init_path}" + chown root:mysql \ + "${init_path}" + + init_file="${init_path}/mysql-init" + + printf -- \ + '%s\n' \ + "${init_template}" \ + > "${init_file}" + # Wait to complete system table installation if [[ ${pids[0]} -gt 0 ]] then @@ -1112,10 +1142,11 @@ function main () trap \ "rm -rf \"${server_key_path}\"; \ - rm -f \"${init_file}\"; \ + rm -rf \"${init_path}\"; \ __cleanup;" \ EXIT INT TERM fi + __init_mysql \ "${init_file}" \ & @@ -1125,7 +1156,7 @@ function main () trap \ "rm -rf \"${server_key_path}\"; \ - rm -f \"${init_file}\"; \ + rm -rf \"${init_path}\"; \ __cleanup;" \ EXIT INT TERM @@ -1198,8 +1229,8 @@ function main () shutdown fi - rm -f \ - "${init_file}" + rm -rf \ + "${init_path}" trap \ "rm -rf \"${server_key_path}\"; \ From 9ce7c1afeaa52d32dd3f66492d2365c0acd092b3 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 7 Jul 2019 12:26:43 +0100 Subject: [PATCH 81/98] #247: Adds randomly named init-file and set permissions to limit access. --- src/usr/sbin/mysqld-bootstrap | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 5f389a2..1e03546 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -643,7 +643,7 @@ function __init_datadir () function __init_mysql () { - local -r init_file="${1:-/tmp/mysql-init}" + local -r init_file="${1}" if [[ ! -f ${init_file} ]] then @@ -1113,18 +1113,15 @@ function main () chown root:mysql \ "${init_path}" - init_file="${init_path}/mysql-init" - - printf -- \ - '%s\n' \ - "${init_template}" \ - > "${init_file}" + init_file="$( + mktemp \ + --tmpdir="${init_path}" + )" - # Wait to complete system table installation - if [[ ${pids[0]} -gt 0 ]] - then - wait ${pids[0]} - fi + chmod 640 \ + "${init_file}" + chown root:mysql \ + "${init_file}" if [[ ${verbose} == true ]] then @@ -1147,6 +1144,17 @@ function main () EXIT INT TERM fi + printf -- \ + '%s\n' \ + "${init_template}" \ + > "${init_file}" + + # Wait to complete system table installation + if [[ ${pids[0]} -gt 0 ]] + then + wait ${pids[0]} + fi + __init_mysql \ "${init_file}" \ & From 5a1e15882c529e293dae9dde01ad74df32b62cdc Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 7 Jul 2019 23:33:24 +0100 Subject: [PATCH 82/98] #247: Adds fix for timeout value in wrapper script. --- src/usr/sbin/mysqld-wrapper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index a72c724..49cf8b3 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -67,7 +67,7 @@ function main () local -r bin="/usr/sbin/mysqld" local -r bootstrap_state_file="/var/lib/misc/mysqld-bootstrap" local -r bootstrap_timeout="$(( - __get_mysql_init_limit + 10 + $(__get_mysql_init_limit) + 10 ))" local -r lock_file="/var/lock/subsys/mysqld-wrapper" local -r nice="/bin/nice" From 2ddb35a0e4365a46ee710108845adb9daa687876 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 7 Jul 2019 23:39:27 +0100 Subject: [PATCH 83/98] #247: Fixes issue with trap __cleanup routine; must be first trap command in order to capture intended exit code. --- src/usr/sbin/mysqld-bootstrap | 82 ++++++++++------------------------- 1 file changed, 22 insertions(+), 60 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 1e03546..47c3408 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -47,9 +47,8 @@ function __configure_mysql_client_root_password () exit 1 fi - trap \ - "rm -rf \"${fifo_path}\"; \ - __cleanup;" \ + trap "__cleanup; \ + rm -rf \"${fifo_path}\";" \ EXIT INT TERM mkfifo \ @@ -59,9 +58,8 @@ function __configure_mysql_client_root_password () exec \ {mypasswd_fd}<>"${fifo_path}/mypasswd" - trap \ - "exec {mypasswd_fd}>&- ; \ - __cleanup;" \ + trap "__cleanup; \ + exec {mypasswd_fd}>&- ;" \ EXIT INT TERM rm -rf \ @@ -77,10 +75,9 @@ function __configure_mysql_client_root_password () & pids[1]="${!}" - trap \ - "exec {mypasswd_fd}>&- ; \ - [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]}; \ - __cleanup" \ + trap "__cleanup; \ + exec {mypasswd_fd}>&- ; \ + [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]};" \ EXIT INT TERM printf -- \ @@ -90,11 +87,10 @@ function __configure_mysql_client_root_password () & pids[2]="${!}" - trap \ - "exec {mypasswd_fd}>&- ; \ + trap "__cleanup; \ + exec {mypasswd_fd}>&- ; \ [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]}; \ - [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]}; \ - __cleanup;" \ + [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]};" \ EXIT INT TERM if [[ ${pids[1]} -gt 0 ]] @@ -257,9 +253,8 @@ function __get_mysql_init_template () exit 1 fi - trap \ - "rm -rf \"${fifo_path}\"; \ - __cleanup;" \ + trap "__cleanup; \ + rm -rf \"${fifo_path}\";" \ EXIT INT TERM mkfifo \ @@ -365,11 +360,10 @@ function __get_mysql_init_template () EOT pids[2]="${!}" - trap \ - "rm -rf \"${fifo_path}\"; \ + trap "__cleanup; \ + rm -rf \"${fifo_path}\"; \ [[ ${pids[1]} -gt 0 ]] && kill -9 ${pids[1]}; \ - [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]}; \ - __cleanup;" \ + [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]};" \ EXIT INT TERM if [[ ${pids[1]} -gt 0 ]] @@ -1002,9 +996,8 @@ function main () exit 1 fi - trap \ - "rm -rf \"${server_key_path}\"; \ - __cleanup;" \ + trap "__cleanup; \ + rm -rf \"${server_key_path}\";" \ EXIT INT TERM /usr/bin/mysql_ssl_rsa_setup \ @@ -1031,18 +1024,6 @@ function main () "${mysql_root_password}" fi - # Restore appropriate trap - if [[ -d ${server_key_path} ]] - then - trap \ - "rm -rf \"${server_key_path}\"; \ - __cleanup;" \ - EXIT INT TERM - else - trap "__cleanup;" \ - EXIT INT TERM - fi - # Wait to complete certificate generation if [[ ${pids[2]} -gt 0 ]] then @@ -1078,11 +1059,6 @@ function main () --compact )" - trap \ - "rm -rf \"${server_key_path}\"; \ - __cleanup;" \ - EXIT INT TERM - init_template="${init_template//'{{MYSQL_USER}}'/${mysql_user}}" init_template="${init_template//'{{MYSQL_USER_DATABASE}}'/${mysql_user_database}}" init_template="${init_template//'{{MYSQL_USER_HOST}}'/${mysql_user_host}}" @@ -1102,10 +1078,9 @@ function main () exit 1 fi - trap \ - "rm -rf \"${server_key_path}\"; \ - rm -rf \"${init_path}\"; \ - __cleanup;" \ + trap "__cleanup; \ + rm -rf \"${server_key_path}\"; \ + rm -rf \"${init_path}\";" \ EXIT INT TERM chmod 750 \ @@ -1136,12 +1111,6 @@ function main () --host="${mysql_user_host}" \ --password-hashed="${mysql_user_password_hashed}" \ --init-sql="${mysql_init_sql}" - - trap \ - "rm -rf \"${server_key_path}\"; \ - rm -rf \"${init_path}\"; \ - __cleanup;" \ - EXIT INT TERM fi printf -- \ @@ -1162,12 +1131,6 @@ function main () __configure_mysql_client_root_password \ "${mysql_root_password}" - trap \ - "rm -rf \"${server_key_path}\"; \ - rm -rf \"${init_path}\"; \ - __cleanup;" \ - EXIT INT TERM - # Wait for initialisation to complete (poll for access) or timeout counter="$(( 2 * mysql_init_limit @@ -1240,9 +1203,8 @@ function main () rm -rf \ "${init_path}" - trap \ - "rm -rf \"${server_key_path}\"; \ - __cleanup;" \ + trap "__cleanup; \ + rm -rf \"${server_key_path}\";" \ EXIT INT TERM fi From cdd52ddeefa408267ad255f987cb4750cb99e6ba Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sun, 7 Jul 2019 23:46:17 +0100 Subject: [PATCH 84/98] #247: Adds MySQL initialisation errors to log output. --- src/usr/sbin/mysqld-bootstrap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 47c3408..67b83dd 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -652,14 +652,14 @@ function __init_mysql () then mysqld_safe \ --init-file="${init_file}" \ - --skip-networking \ - 2> /dev/null + --log-warnings=0 \ + --skip-networking else mysqld \ --init-file="${init_file}" \ + --log_error_verbosity=1 \ --pid-file=/var/run/mysqld/mysqld.pid \ - --skip-networking \ - 2> /dev/null + --skip-networking fi } From 8ddb1cc92363a4354a1f0b40f2f8873d38bcf5f2 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 8 Jul 2019 00:05:09 +0100 Subject: [PATCH 85/98] #247: Restore traps where necessary. --- src/usr/sbin/mysqld-bootstrap | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 67b83dd..d661e36 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -1024,6 +1024,17 @@ function main () "${mysql_root_password}" fi + # Restore appropriate trap + if [[ -d ${server_key_path} ]] + then + trap "__cleanup; \ + rm -rf \"${server_key_path}\";" \ + EXIT INT TERM + else + trap "__cleanup;" \ + EXIT INT TERM + fi + # Wait to complete certificate generation if [[ ${pids[2]} -gt 0 ]] then @@ -1059,6 +1070,10 @@ function main () --compact )" + trap "__cleanup; \ + rm -rf \"${server_key_path}\";" \ + EXIT INT TERM + init_template="${init_template//'{{MYSQL_USER}}'/${mysql_user}}" init_template="${init_template//'{{MYSQL_USER_DATABASE}}'/${mysql_user_database}}" init_template="${init_template//'{{MYSQL_USER_HOST}}'/${mysql_user_host}}" @@ -1111,6 +1126,11 @@ function main () --host="${mysql_user_host}" \ --password-hashed="${mysql_user_password_hashed}" \ --init-sql="${mysql_init_sql}" + + trap "__cleanup; \ + rm -rf \"${server_key_path}\"; \ + rm -rf \"${init_path}\";" \ + EXIT INT TERM fi printf -- \ @@ -1131,6 +1151,11 @@ function main () __configure_mysql_client_root_password \ "${mysql_root_password}" + trap "__cleanup; \ + rm -rf \"${server_key_path}\"; \ + rm -rf \"${init_path}\";" \ + EXIT INT TERM + # Wait for initialisation to complete (poll for access) or timeout counter="$(( 2 * mysql_init_limit From 272a4c9f7863b8f4b8119ba9ebe975131e6731f7 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 8 Jul 2019 00:21:08 +0100 Subject: [PATCH 86/98] #247: Fixes capturing MySQL initialisation timeout error. --- src/usr/sbin/mysqld-bootstrap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index d661e36..14db975 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -1160,6 +1160,7 @@ function main () counter="$(( 2 * mysql_init_limit ))" + set +e until (( counter == 0 )) do sleep 0.5 @@ -1192,6 +1193,7 @@ function main () (( counter -= 1 )) done + set -e if [[ ${counter} -eq 0 ]] then From 70e54cb22e2f2f7e6e1c8f9548eb8cd8f6175950 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 8 Jul 2019 01:16:44 +0100 Subject: [PATCH 87/98] #247: Updates default value of MYSQL_INIT_LIMIT to 10 seconds; init-file SQL should not be long running. --- .env.example | 2 ++ CHANGELOG.md | 3 +++ Dockerfile | 2 +- README.md | 4 ++-- docker-compose.yml | 2 ++ environment.mk | 2 +- src/etc/supervisord.d/50-mysqld-wrapper.conf | 2 +- src/etc/systemd/system/centos-ssh-mysql@.service | 2 +- src/opt/scmi/environment.sh | 2 +- 9 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 32406ba..343617b 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ +MYSQL_INIT_LIMIT=10 +MYSQL_INIT_SQL= MYSQL_ROOT_PASSWORD= MYSQL_ROOT_PASSWORD_HASHED=false MYSQL_SUBNET=127.0.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c7ffab..eaa02a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ Summary of release changes. - Updates supervisord configuration to send error log output to stderr. - Updates bootstrap supervisord configuration file/priority to `20-mysqld-bootstrap.conf`/`20`. - Updates wrapper supervisord configuration file/priority to `50-mysqld-wrapper.conf`/`50`. +- Updates default value of `MYSQL_INIT_LIMIT` to 10 from 60 seconds. - Fixes docker host connection status check in Makefile. +- Fixes default `MYSQL_INIT_LIMIT` value in systemd unit file template. - Adds `inspect`, `reload` and `top` Makefile targets. - Adds improved `clean` Makefile target; includes exited containers and dangling images. - Adds `SYSTEM_TIMEZONE` handling to Makefile, scmi, systemd unit and docker-compose templates. @@ -25,6 +27,7 @@ Summary of release changes. - Adds all necessary auto-generated TLS/SSL pem files to data directory during bootstrap. - Adds improved bootstrap handling of previously initialised datadir. - Adds a note in example docker-compose.yml that `MYSQL_ROOT_PASSWORD` is required when using a data volume. +- Adds `MYSQL_INIT_LIMIT` and `MYSQL_INIT_SQL` to docker-compose example. - Removes `MYSQL_AUTOSTART_MYSQL_BOOTSTRAP`, replaced with `ENABLE_MYSQL_BOOTSTRAP`. - Removes `MYSQL_AUTOSTART_MYSQL_WRAPPER`, replaced with `ENABLE_MYSQL_WRAPPER`. - Removes support for long image tags (i.e. centos-7-mysql57-community-2.x.x). diff --git a/Dockerfile b/Dockerfile index 3da4bf4..13c65bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,7 +62,7 @@ ENV \ ENABLE_MYSQLD_WRAPPER="true" \ ENABLE_SSHD_BOOTSTRAP="false" \ ENABLE_SSHD_WRAPPER="false" \ - MYSQL_INIT_LIMIT="60" \ + MYSQL_INIT_LIMIT="10" \ MYSQL_INIT_SQL="" \ MYSQL_ROOT_PASSWORD="" \ MYSQL_ROOT_PASSWORD_HASHED="false" \ diff --git a/README.md b/README.md index 6c32305..9632213 100644 --- a/README.md +++ b/README.md @@ -144,11 +144,11 @@ It may be desirable to prevent the startup of the mysqld-bootstrap and/or mysqld ##### MYSQL_INIT_LIMIT -The default timeout for MySQL initialisation is 60 seconds. Use `MYSQL_INIT_LIMIT` to change this value when necessary. +The default timeout for MySQL initialisation is 10 seconds. Use `MYSQL_INIT_LIMIT` to change this value when necessary. ``` ... - --env "MYSQL_INIT_LIMIT=120" \ + --env "MYSQL_INIT_LIMIT=30" \ ... ``` diff --git a/docker-compose.yml b/docker-compose.yml index 72e658e..2172b7b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,8 @@ services: context: "." dockerfile: "Dockerfile" environment: + MYSQL_INIT_LIMIT: "${MYSQL_INIT_LIMIT}" + MYSQL_INIT_SQL: "${MYSQL_INIT_SQL}" MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" MYSQL_ROOT_PASSWORD_HASHED: "${MYSQL_ROOT_PASSWORD_HASHED}" MYSQL_SUBNET: "${MYSQL_SUBNET}" diff --git a/environment.mk b/environment.mk index 2a262e4..0f45fbd 100644 --- a/environment.mk +++ b/environment.mk @@ -25,7 +25,7 @@ STARTUP_TIME ?= 10 # ------------------------------------------------------------------------------ ENABLE_MYSQLD_BOOTSTRAP ?= true ENABLE_MYSQLD_WRAPPER ?= true -MYSQL_INIT_LIMIT ?= 60 +MYSQL_INIT_LIMIT ?= 10 MYSQL_INIT_SQL ?= MYSQL_ROOT_PASSWORD ?= MYSQL_ROOT_PASSWORD_HASHED ?= false diff --git a/src/etc/supervisord.d/50-mysqld-wrapper.conf b/src/etc/supervisord.d/50-mysqld-wrapper.conf index 58361d8..ad99eb8 100644 --- a/src/etc/supervisord.d/50-mysqld-wrapper.conf +++ b/src/etc/supervisord.d/50-mysqld-wrapper.conf @@ -3,7 +3,7 @@ autorestart = true autostart = %(ENV_ENABLE_MYSQLD_WRAPPER)s command = /usr/sbin/mysqld-wrapper --verbose priority = 50 -startsecs = 10 +startsecs = 30 stderr_logfile = /dev/stderr stderr_logfile_maxbytes = 0 stdout_logfile = /dev/stdout diff --git a/src/etc/systemd/system/centos-ssh-mysql@.service b/src/etc/systemd/system/centos-ssh-mysql@.service index be1bd47..e247498 100644 --- a/src/etc/systemd/system/centos-ssh-mysql@.service +++ b/src/etc/systemd/system/centos-ssh-mysql@.service @@ -58,7 +58,7 @@ Environment="DOCKER_PORT_MAP_TCP_3306=3306" Environment="DOCKER_USER=jdeathe" Environment="ENABLE_MYSQLD_BOOTSTRAP=true" Environment="ENABLE_MYSQLD_WRAPPER=true" -Environment="MYSQL_INIT_LIMIT=" +Environment="MYSQL_INIT_LIMIT=10" Environment="MYSQL_INIT_SQL=" Environment="MYSQL_ROOT_PASSWORD=" Environment="MYSQL_ROOT_PASSWORD_HASHED=false" diff --git a/src/opt/scmi/environment.sh b/src/opt/scmi/environment.sh index 7f194a8..25512ef 100644 --- a/src/opt/scmi/environment.sh +++ b/src/opt/scmi/environment.sh @@ -26,7 +26,7 @@ STARTUP_TIME="${STARTUP_TIME:-10}" # ------------------------------------------------------------------------------ ENABLE_MYSQLD_BOOTSTRAP="${ENABLE_MYSQLD_BOOTSTRAP:-true}" ENABLE_MYSQLD_WRAPPER="${ENABLE_MYSQLD_WRAPPER:-true}" -MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-60}" +MYSQL_INIT_LIMIT="${MYSQL_INIT_LIMIT:-10}" MYSQL_INIT_SQL="${MYSQL_INIT_SQL:-}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" MYSQL_ROOT_PASSWORD_HASHED="${MYSQL_ROOT_PASSWORD_HASHED:-false}" From d1f45fa4f49b4c1edc44a054995b071cb3df022f Mon Sep 17 00:00:00 2001 From: James Deathe Date: Mon, 8 Jul 2019 09:03:19 +0100 Subject: [PATCH 88/98] #247: Removes initialisation template output from logs. --- CHANGELOG.md | 1 + README.md | 4 +-- .../docker-logs-mysqld-bootstrap-v2.3.0.png | Bin 0 -> 165627 bytes src/usr/sbin/mysqld-bootstrap | 25 +++++------------- 4 files changed, 9 insertions(+), 21 deletions(-) create mode 100644 images/docker-logs-mysqld-bootstrap-v2.3.0.png diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa02a4..522c3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Summary of release changes. - Removes support for long image tags (i.e. centos-7-mysql57-community-2.x.x). - Removes unnecessary use of `FLUSH PRIVILEGES` in intitialisation SQL. - Removes `log-error=/var/log/mysqld.log` from default configuration; log to stderr. +- Removes initialisation template output from logs; `MYSQL_INIT_SQL` is operator defined. ### 2.2.0 - 2019-03-18 diff --git a/README.md b/README.md index 9632213..f2c9d93 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Verify successful initialisation of the named container. $ docker logs mysql.1 ``` -On the first run, there will be additional output showing the initialisation SQL template and, before mysqld-bootstrap completes, the MySQL Details which shows the configured database, if applicable, and any associated user credentials. +On first run, MySQL Details are returned. This includes the configured database, if applicable, and any associated user credentials. -![Docker Logs - MySQL Bootstrap](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-7-mysql57-community/images/docker-logs-mysqld-bootstrap.png) +![Docker Logs - MySQL Bootstrap](https://raw.github.com/jdeathe/centos-ssh-mysql/centos-7-mysql57-community/images/docker-logs-mysqld-bootstrap-v2.3.0.png) The MySQL table data is persistent across container restarts by setting the MySQL data directory `/var/lib/mysql` as a data volume. To locate the path where data is stored on the Docker host use `docker inspect`. diff --git a/images/docker-logs-mysqld-bootstrap-v2.3.0.png b/images/docker-logs-mysqld-bootstrap-v2.3.0.png new file mode 100644 index 0000000000000000000000000000000000000000..63d3b6db7093423bbf2c215280f18763bb70a198 GIT binary patch literal 165627 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 `#D}IP|oR69hxQ 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 Date: Mon, 8 Jul 2019 21:21:58 +0100 Subject: [PATCH 89/98] #247: Adds docker-compose usage example of running SQL file from stdin. --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2172b7b..bbaec84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ # Usage: # docker-compose exec mysql mysqladmin -p --help # docker-compose exec mysql mysql -p +# docker exec -i $(docker-compose ps -q mysql) mysql < setup.sql # # Reset - bring down services + delete volume data: # docker-compose down -v From b1f5ddb7c8493bbd3ee457167a3487637924b661 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Tue, 9 Jul 2019 08:56:18 +0100 Subject: [PATCH 90/98] 247: Output only stderr / errors during MySQL data directory initialisation. --- src/usr/sbin/mysqld-bootstrap | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index 0130ef7..f49dd7c 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -618,20 +618,21 @@ function __init_datadir () then mysql_install_db \ --force \ + --log-warnings=0 \ --skip-name-resolve \ --skip-networking \ --tmpdir="${directory}" \ - 2> /dev/null + > /dev/null else mysqld \ --datadir="${directory}" \ --initialize-insecure \ + --log_error_verbosity=1 \ --pid-file=/var/run/mysqld/mysqld.pid \ --skip-name-resolve \ --skip-networking \ --tmpdir="${directory}" \ - --user=mysql \ - 2> /dev/null + --user=mysql fi } From bb3e2beb899722b02629a7aa07c11ffeb69f1df8 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Tue, 9 Jul 2019 08:59:49 +0100 Subject: [PATCH 91/98] #247: Fix updated default value of MYSQL_INIT_LIMIT. --- src/usr/sbin/mysqld-wrapper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index 49cf8b3..120633e 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -25,7 +25,7 @@ function __delete_lock () function __get_mysql_init_limit () { - local -r default_value="${1:-60}" + local -r default_value="${1:-10}" local value="${MYSQL_INIT_LIMIT}" From 5b434d807b7bf7d26a0c4e3b14659ef8e79e917b Mon Sep 17 00:00:00 2001 From: James Deathe Date: Tue, 9 Jul 2019 09:03:29 +0100 Subject: [PATCH 92/98] #247: Adds wrapper improvements for consistency between branches. --- src/usr/sbin/mysqld-wrapper | 68 +++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index 120633e..8cd543c 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -23,6 +23,20 @@ function __delete_lock () fi } +function __get_bin () +{ + local bin="/usr/sbin/mysqld" + + if [[ -f /usr/bin/mysqld_safe ]] + then + bin="/usr/bin/mysqld_safe" + fi + + printf -- \ + '%s' \ + "${bin}" +} + function __get_mysql_init_limit () { local -r default_value="${1:-10}" @@ -40,13 +54,38 @@ function __get_mysql_init_limit () function __get_options () { local -r options="${1}" - local -r pid_file="/var/run/mysqld/mysqld.pid" - printf -- \ - '--pid-file=%s%s%s' \ - "${pid_file}" \ - "${options:+" "}" \ - "${options}" + if [[ ${bin} == "/usr/bin/mysqld_safe" ]] + then + printf -- \ + '%s%s' \ + "${options:+" "}" \ + "${options}" + else + printf -- \ + '--pid-file=%s%s%s' \ + "${pid_file}" \ + "${options:+" "}" \ + "${options}" + fi +} + +function __get_proxy () +{ + if [[ ${bin} == "/usr/bin/mysqld_safe" ]] + then + printf -- \ + '%s -n %s %s %s' \ + "${nice}" \ + "${niceness}" \ + "${pid_proxy}" \ + "${pid_file}" + else + printf -- \ + '%s -n %s' \ + "${nice}" \ + "${niceness}" + fi } function __is_valid_mysql_init_limit () @@ -64,7 +103,6 @@ function __is_valid_mysql_init_limit () function main () { - local -r bin="/usr/sbin/mysqld" local -r bootstrap_state_file="/var/lib/misc/mysqld-bootstrap" local -r bootstrap_timeout="$(( $(__get_mysql_init_limit) + 10 @@ -72,8 +110,12 @@ function main () local -r lock_file="/var/lock/subsys/mysqld-wrapper" local -r nice="/bin/nice" local -r niceness="10" + local -r pid_proxy="/usr/bin/pidproxy" + local -r pid_file="/var/run/mysqld/mysqld.pid" + local bin local options + local proxy local verbose="false" while [[ "${#}" -gt 0 ]] @@ -98,9 +140,15 @@ function main () EXIT INT TERM __create_lock + bin="$( + __get_bin + )" options="$( __get_options )" + proxy="$( + __get_proxy + )" if [[ ${verbose} == true ]] then @@ -144,10 +192,10 @@ function main () trap - \ EXIT INT TERM - exec ${nice} \ - -n ${niceness} \ + exec \ + ${proxy} \ ${bin} \ ${options} } -main "${@}" \ No newline at end of file +main "${@}" From 8b2c794b84e4fc9731e83aabb7145bec596a8844 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Tue, 9 Jul 2019 16:46:39 +0100 Subject: [PATCH 93/98] Release changes for 1.11.0 and 2.3.0. --- CHANGELOG.md | 2 +- Dockerfile | 2 +- README.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 522c3a8..36d34a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Summary of release changes. -### 2.3.0 - Unreleased +### 2.3.0 - 2019-07-09 - Updates source image to [2.6.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.6.0). - Updates `mysql-community-server` package to 5.7.26-1. diff --git a/Dockerfile b/Dockerfile index 13c65bc..f4287ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM jdeathe/centos-ssh:2.6.0 -ARG RELEASE_VERSION="2.2.0" +ARG RELEASE_VERSION="2.3.0" # ------------------------------------------------------------------------------ # Base install of required packages diff --git a/README.md b/README.md index f2c9d93..e9034e1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Tags and respective `Dockerfile` links -- `centos-7-mysql57-community`, [`2.2.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.2.0) [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, [`1.10.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.10.0) [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- `centos-7-mysql57-community`, [`2.3.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.3.0) [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- `centos-6`, [`1.11.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.11.0) [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) ## Overview @@ -25,7 +25,7 @@ $ docker run -d \ --name mysql.1 \ -p 3306:3306 \ -v /var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.2.0 + jdeathe/centos-ssh-mysql:2.3.0 ``` Verify the named container's process status and health. @@ -114,7 +114,7 @@ $ docker stop mysql.1 && \ --env "MYSQL_USER_PASSWORD=" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume mysql.1.data-mysql:/var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.2.0 + jdeathe/centos-ssh-mysql:2.3.0 ``` The environmental variable `MYSQL_SUBNET` is optional but can be used to generate users with access to databases outside the `localhost`, (the default for the root user). In the example, the subnet definition `0.0.0.0/0.0.0.0` allows connections from any network which is equivalent to the wildcard symbol, `%`, in MySQL GRANT definitions. From a515b54d3adfd6aaf3291ee8d2b12749757a53c0 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 23 Aug 2019 14:30:21 +0100 Subject: [PATCH 94/98] #261: Fixes issue with initialsation from existing datadir volume. --- CHANGELOG.md | 6 + README.md | 12 +- .../supervisord.d/20-mysqld-bootstrap.conf | 2 +- src/usr/bin/healthcheck | 35 +- src/usr/sbin/mysqld-bootstrap | 313 ++++++++++-------- test/shpec/operation_shpec.sh | 18 +- 6 files changed, 211 insertions(+), 175 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d34a4..c8c5781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Summary of release changes. +### 2.3.1 - Unreleased + +- Updates healthcheck script for consistency. +- Fixes container mount point for emulating and testing secrets. +- Fixes issue with automatically generated root password initialisation when running with existing datadir volume. + ### 2.3.0 - 2019-07-09 - Updates source image to [2.6.0](https://github.com/jdeathe/centos-ssh/releases/tag/2.6.0). diff --git a/README.md b/README.md index e9034e1..9580c91 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,11 @@ $ docker exec -it \ mysql.1 \ mysql ``` -### Sakila Example +### Sakila example Import the Sakila example database from the [MySQL Documentation](https://dev.mysql.com/doc/index-other.html) and view the first 2 records from the film table. -#### Import Schema +#### Import schema ``` $ docker exec -i mysql.1 mysql \ @@ -74,7 +74,7 @@ $ docker exec -i mysql.1 mysql \ ) ``` -#### Import Data +#### Import data ``` $ docker exec -i mysql.1 mysql \ @@ -83,7 +83,7 @@ $ docker exec -i mysql.1 mysql \ ) ``` -#### Select Records +#### Select records ``` $ docker exec mysql.1 mysql \ @@ -104,7 +104,7 @@ The following example sets up a custom MySQL database, user and user password on ``` $ docker stop mysql.1 && \ - docker rm mysql.1 && \ + docker rm mysql.1; \ docker run \ --detach \ --name mysql.1 \ @@ -125,7 +125,7 @@ Verify it's initialised and running successfully by inspecting the container's l $ docker logs mysql.1 ``` -#### Environment Variables +#### Environment variables There are several environmental variables defined at runtime these allow the operator to customise the running container. diff --git a/src/etc/supervisord.d/20-mysqld-bootstrap.conf b/src/etc/supervisord.d/20-mysqld-bootstrap.conf index 4e898d1..2575393 100644 --- a/src/etc/supervisord.d/20-mysqld-bootstrap.conf +++ b/src/etc/supervisord.d/20-mysqld-bootstrap.conf @@ -3,8 +3,8 @@ autorestart = false autostart = %(ENV_ENABLE_MYSQLD_BOOTSTRAP)s command = /usr/sbin/mysqld-bootstrap --verbose priority = 20 -startsecs = 0 startretries = 0 +startsecs = 0 stderr_logfile = /dev/stderr stderr_logfile_maxbytes = 0 stdout_logfile = /dev/stdout diff --git a/src/usr/bin/healthcheck b/src/usr/bin/healthcheck index fd4c388..55ed782 100755 --- a/src/usr/bin/healthcheck +++ b/src/usr/bin/healthcheck @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -e + function __get_system_timezone () { local -r default_value="${1:-UTC}" @@ -97,14 +99,14 @@ function __last_check_passed () { local -i status=0 - if [[ ! -f ${STATUS_PATH} ]] + if [[ ! -f ${status_path} ]] then return ${status} else read \ -n 1 \ status \ - < "${STATUS_PATH}" + < "${status_path}" if [[ ${status} =~ ^[01]$ ]] then @@ -130,8 +132,9 @@ function __mysql_get_option () function __print_message () { + local -r quiet="${quiet:-false}" local -r type="${1:-}" - local -r quiet="${QUIET:-false}" + local message="${2:-}" local prefix @@ -168,10 +171,9 @@ function __print_message () function __record_exit_status () { + local -r status_directory="${status_path%/*}" + local -i status=${1:-${?}} - local status_directory="$( - dirname "${STATUS_PATH}" - )" if [[ ! -d ${status_directory} ]] then @@ -183,20 +185,20 @@ function __record_exit_status () "${status_directory}" fi - if [[ ! -f ${STATUS_PATH} ]] + if [[ ! -f ${status_path} ]] then install \ -m 0660 \ -o root \ -g root \ /dev/null \ - "${STATUS_PATH}" + "${status_path}" fi printf -- \ '%d' \ "${status}" \ - > "${STATUS_PATH}" + > "${status_path}" trap - \ EXIT @@ -208,8 +210,8 @@ function __usage () { cat <<-USAGE - Usage: $(basename ${0}) [OPTIONS] - $(basename ${0}) [-h|--help] + Usage: ${0##*/} [OPTIONS] + ${0##*/} [-h|--help] Options: -i, --interval= After the first successful check the @@ -228,10 +230,10 @@ function __usage () function main () { - local -r STATUS_PATH="/var/lib/healthcheck/status" local -r mysqld="/usr/sbin/mysqld" local -r pattern_seconds_in_minute='^([1-9]|[1-5][0-9]|60)$' local -r redacted_value="********" + local -r status_path="/var/lib/healthcheck/status" local -r system_timezone="$( __get_system_timezone )" @@ -239,7 +241,7 @@ function main () system-timezone -qq )" - local QUIET="false" + local quiet="false" local -i interval=10 # Trap and record the exit status @@ -262,7 +264,7 @@ function main () shift 1 ;; -q|--quiet) - QUIET="true" + quiet="true" shift 1 ;; *) @@ -296,10 +298,9 @@ function main () exit 1 fi - # mysqld-bootstrap if [[ ${ENABLE_MYSQLD_BOOTSTRAP} == true ]] then - if [[ -e /var/lock/subsys/mysqld-bootstrap ]] \ + if [[ -f /var/lock/subsys/mysqld-bootstrap ]] \ || ! __is_mysql_data_directory_populated then __print_message \ @@ -309,7 +310,6 @@ function main () fi fi - # mysqld-wrapper if [[ ${ENABLE_MYSQLD_WRAPPER} == true ]] then if ! ps axo command \ @@ -338,6 +338,7 @@ function main () if ! __have_mysql_access then __print_message \ + "error" \ "root@localhost access failed." exit 1 fi diff --git a/src/usr/sbin/mysqld-bootstrap b/src/usr/sbin/mysqld-bootstrap index f49dd7c..393da1a 100755 --- a/src/usr/sbin/mysqld-bootstrap +++ b/src/usr/sbin/mysqld-bootstrap @@ -38,15 +38,6 @@ function __configure_mysql_client_root_password () mktemp -d )" - if [[ ${?} -ne 0 ]] - then - >&2 printf -- \ - 'ERROR: %s failed to create directory %s - aborting\n' \ - "${0##*/}" \ - "${fifo_path}" - exit 1 - fi - trap "__cleanup; \ rm -rf \"${fifo_path}\";" \ EXIT INT TERM @@ -93,6 +84,7 @@ function __configure_mysql_client_root_password () [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]};" \ EXIT INT TERM + set +e if [[ ${pids[1]} -gt 0 ]] then wait ${pids[1]} @@ -105,6 +97,7 @@ function __configure_mysql_client_root_password () "${0##*/}" exit 1 fi + set -e trap - \ EXIT INT TERM @@ -244,15 +237,6 @@ function __get_mysql_init_template () mktemp -d )" - if [[ ${?} -ne 0 ]] - then - >&2 printf -- \ - 'ERROR: %s failed to create directory %s - aborting\n' \ - "${0##*/}" - "${fifo_path}" - exit 1 - fi - trap "__cleanup; \ rm -rf \"${fifo_path}\";" \ EXIT INT TERM @@ -366,6 +350,7 @@ function __get_mysql_init_template () [[ ${pids[2]} -gt 0 ]] && kill -9 ${pids[2]};" \ EXIT INT TERM + set +e if [[ ${pids[1]} -gt 0 ]] then wait ${pids[1]} @@ -378,6 +363,7 @@ function __get_mysql_init_template () "${0##*/}" exit 1 fi + set -e rm -rf \ "${fifo_path}" @@ -664,6 +650,87 @@ function __init_mysql () fi } +function __init_mysql_completion () +{ + local counter + + # Wait for initialisation to complete (poll for access) or timeout + counter="$(( + 2 * mysql_init_limit + ))" + + set +e + until (( counter == 0 )) + do + sleep 0.5 + + if __have_mysql_access + then + # Set the password if it was supplied pre-hashed. + if [[ ${mysql_root_password_hashed} == true ]] + then + if command -v mysqld_safe &> /dev/null + then + # MySQL version < 5.7 + mysql \ + -e "SET PASSWORD = '${mysql_root_password}'" + else + # MySQL version >= 5.7 + mysql \ + -e "UPDATE mysql.user \ + SET authentication_string = '${mysql_root_password}' \ + WHERE User = 'root' \ + AND Host = 'localhost';" + fi + + rm -f \ + /root/.{my,mylogin}.cnf + fi + + break + fi + + (( counter -= 1 )) + done + set -e + + if [[ ${counter} -eq 0 ]] + then + >&2 printf -- \ + 'ERROR: %s initilisation timed out - aborting\n' \ + "${0##*/}" + + killall \ + -15 \ + mysqld + + exit 1 + else + if [[ ${verbose} == true ]] + then + printf -- \ + 'INFO: %s stopping mysqld\n' \ + "${0##*/}" + fi + + # Prefer mysqladmin shutdown method if password is known + if [[ ${mysql_root_password_hashed} == true ]] + then + killall \ + -w \ + -15 \ + mysqld + else + mysqladmin \ + --user=root \ + shutdown + fi + + rm -rf \ + "${init_path}" + fi +} + function __is_mysql_datadir_populated () { local -r directory="${1:-"$( @@ -883,7 +950,6 @@ function main () date -u +%s.%N )" - local counter local datadir local init_file local init_path @@ -976,7 +1042,7 @@ function main () # Certificate generation if [[ -x /usr/bin/mysql_ssl_rsa_setup ]] \ - && [[ ! -e ${datadir}/server-key.pem ]] + && [[ ! -f ${datadir}/server-key.pem ]] then if [[ ${verbose} == true ]] then @@ -988,15 +1054,6 @@ function main () mktemp -d )" - if [[ ${?} -ne 0 ]] - then - >&2 printf -- \ - 'ERROR: %s failed to create directory %s - aborting\n' \ - "${0##*/}" \ - "${server_key_path}" - exit 1 - fi - trap "__cleanup; \ rm -rf \"${server_key_path}\";" \ EXIT INT TERM @@ -1009,6 +1066,31 @@ function main () pids[2]="${!}" fi + # Prepair initialisation path and file + init_path="$( + mktemp -d + )" + + trap "__cleanup; \ + rm -rf \"${server_key_path}\"; \ + rm -rf \"${init_path}\";" \ + EXIT INT TERM + + chmod 750 \ + "${init_path}" + chown root:mysql \ + "${init_path}" + + init_file="$( + mktemp \ + --tmpdir="${init_path}" + )" + + chmod 640 \ + "${init_file}" + chown root:mysql \ + "${init_file}" + # Initialisation is a one-shot process. if __is_mysql_datadir_populated "${datadir}" then @@ -1019,6 +1101,41 @@ function main () "${0##*/}" fi + if command -v mysqld_safe &> /dev/null + then + # MySQL version < 5.7.6 + printf -v \ + init_template \ + -- "SET PASSWORD FOR '%s'@'%s' = PASSWORD('%s');" \ + "root" \ + "localhost" \ + "{{MYSQL_ROOT_PASSWORD}}" + else + # MySQL version >= 5.7.6 + printf -v \ + init_template \ + -- "ALTER USER '%s'@'%s' IDENTIFIED BY '%s';" \ + "root" \ + "localhost" \ + "{{MYSQL_ROOT_PASSWORD}}" + fi + + if [[ ${verbose} == true ]] + then + printf -- \ + 'INFO: %s initialising\n' \ + "${0##*/}" + fi + + printf -- \ + '%s\n' \ + "${init_template//'{{MYSQL_ROOT_PASSWORD}}'/${mysql_root_password}}" \ + > "${init_file}" + + __init_mysql \ + "${init_file}" \ + & + if [[ ${mysql_root_password_hashed} != true ]] then __configure_mysql_client_root_password \ @@ -1029,10 +1146,27 @@ function main () if [[ -d ${server_key_path} ]] then trap "__cleanup; \ - rm -rf \"${server_key_path}\";" \ + rm -rf \"${server_key_path}\"; \ + rm -rf \"${init_path}\";" \ + EXIT INT TERM + else + trap "__cleanup; \ + rm -rf \"${init_path}\";" \ + EXIT INT TERM + fi + + __init_mysql_completion + + # Restore appropriate trap + if [[ -d ${server_key_path} ]] + then + trap "__cleanup; \ + rm -rf \"${server_key_path}\"; \ + rm -rf \"${init_path}\";" \ EXIT INT TERM else - trap "__cleanup;" \ + trap "__cleanup; \ + rm -rf \"${init_path}\";" \ EXIT INT TERM fi @@ -1072,7 +1206,8 @@ function main () )" trap "__cleanup; \ - rm -rf \"${server_key_path}\";" \ + rm -rf \"${server_key_path}\"; \ + rm -rf \"${init_path}\";" \ EXIT INT TERM init_template="${init_template//'{{MYSQL_USER}}'/${mysql_user}}" @@ -1081,39 +1216,6 @@ function main () init_template="${init_template//'{{MYSQL_USER_PASSWORD}}'/${mysql_user_password}}" init_template="${init_template//'{{MYSQL_ROOT_PASSWORD}}'/${mysql_root_password}}" - init_path="$( - mktemp -d - )" - - if [[ ${?} -ne 0 ]] - then - >&2 printf -- \ - 'ERROR: %s failed to create directory %s - aborting\n' \ - "${0##*/}" \ - "${init_path}" - exit 1 - fi - - trap "__cleanup; \ - rm -rf \"${server_key_path}\"; \ - rm -rf \"${init_path}\";" \ - EXIT INT TERM - - chmod 750 \ - "${init_path}" - chown root:mysql \ - "${init_path}" - - init_file="$( - mktemp \ - --tmpdir="${init_path}" - )" - - chmod 640 \ - "${init_file}" - chown root:mysql \ - "${init_file}" - if [[ ${verbose} == true ]] then printf -- \ @@ -1144,84 +1246,11 @@ function main () rm -rf \"${init_path}\";" \ EXIT INT TERM - # Wait for initialisation to complete (poll for access) or timeout - counter="$(( - 2 * mysql_init_limit - ))" - set +e - until (( counter == 0 )) - do - sleep 0.5 - - if __have_mysql_access - then - # Set the password if it was supplied pre-hashed. - if [[ ${mysql_root_password_hashed} == true ]] - then - if command -v mysqld_safe &> /dev/null - then - # MySQL version < 5.7 - mysql \ - -e "SET PASSWORD = '${mysql_root_password}'" - else - # MySQL version >= 5.7 - mysql \ - -e "UPDATE mysql.user \ - SET authentication_string = '${mysql_root_password}' \ - WHERE User = 'root' \ - AND Host = 'localhost';" - fi - - rm -f \ - /root/.{my,mylogin}.cnf - fi - - break - fi - - (( counter -= 1 )) - done - set -e - - if [[ ${counter} -eq 0 ]] - then - >&2 printf -- \ - 'ERROR: %s initilisation timed out - aborting\n' \ - "${0##*/}" - - killall \ - -15 \ - mysqld + __init_mysql_completion - exit 1 - else - if [[ ${verbose} == true ]] - then - printf -- \ - 'INFO: %s stopping mysqld\n' \ - "${0##*/}" - fi - - # Prefer mysqladmin shutdown method if password is known - if [[ ${mysql_root_password_hashed} == true ]] - then - killall \ - -w \ - -15 \ - mysqld - else - mysqladmin \ - --user=root \ - shutdown - fi - - rm -rf \ - "${init_path}" - - trap "__cleanup; \ - rm -rf \"${server_key_path}\";" \ - EXIT INT TERM - fi + trap "__cleanup; \ + rm -rf \"${server_key_path}\";" \ + EXIT INT TERM # Local root user details if [[ ${verbose} == true ]] diff --git a/test/shpec/operation_shpec.sh b/test/shpec/operation_shpec.sh index c04bd6a..e242b43 100644 --- a/test/shpec/operation_shpec.sh +++ b/test/shpec/operation_shpec.sh @@ -727,13 +727,13 @@ function test_custom_configuration () --name mysql.2 \ --network-alias mysql.2 \ --network ${private_network_1} \ - --env "MYSQL_ROOT_PASSWORD=/run/secrets/mysql_root_password" \ + --env "MYSQL_ROOT_PASSWORD=/var/run/secrets/mysql_root_password" \ --env "MYSQL_SUBNET=172.172.40.0/255.255.255.0" \ --env "MYSQL_USER=app-user" \ - --env "MYSQL_USER_PASSWORD=/run/secrets/mysql_user_password" \ + --env "MYSQL_USER_PASSWORD=/var/run/secrets/mysql_user_password" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume ${data_volume_1}:/var/lib/mysql \ - --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/run/secrets:ro \ + --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/var/run/secrets:ro \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -822,15 +822,15 @@ function test_custom_configuration () --name mysql.2 \ --network-alias mysql.2 \ --network ${private_network_1} \ - --env "MYSQL_ROOT_PASSWORD=/run/secrets/mysql_root_password_hashed" \ + --env "MYSQL_ROOT_PASSWORD=/var/run/secrets/mysql_root_password_hashed" \ --env "MYSQL_ROOT_PASSWORD_HASHED=true" \ --env "MYSQL_SUBNET=172.172.40.0/255.255.255.0" \ --env "MYSQL_USER=app-user" \ - --env "MYSQL_USER_PASSWORD=/run/secrets/mysql_user_password_hashed" \ + --env "MYSQL_USER_PASSWORD=/var/run/secrets/mysql_user_password_hashed" \ --env "MYSQL_USER_PASSWORD_HASHED=true" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume ${data_volume_1}:/var/lib/mysql \ - --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/run/secrets:ro \ + --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/var/run/secrets:ro \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null @@ -1083,13 +1083,13 @@ function test_custom_configuration () --network ${private_network_1} \ --env "MYSQL_INIT_LIMIT=30" \ --env "MYSQL_INIT_SQL=CREATE DATABASE IF NOT EXISTS \`{{MYSQL_USER_DATABASE}}-1\`; GRANT ALL PRIVILEGES ON \`{{MYSQL_USER_DATABASE}}-%\`.* TO '{{MYSQL_USER}}'@'{{MYSQL_USER_HOST}}' IDENTIFIED BY '{{MYSQL_USER_PASSWORD}}'; CREATE TABLE \`{{MYSQL_USER_DATABASE}}-1\`.\`user\` (\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, \`email\` varchar(255), PRIMARY KEY (\`id\`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;" \ - --env "MYSQL_ROOT_PASSWORD=/run/secrets/mysql_root_password" \ + --env "MYSQL_ROOT_PASSWORD=/var/run/secrets/mysql_root_password" \ --env "MYSQL_SUBNET=172.172.40.0/255.255.255.0" \ --env "MYSQL_USER=app" \ - --env "MYSQL_USER_PASSWORD=/run/secrets/mysql_user_password" \ + --env "MYSQL_USER_PASSWORD=/var/run/secrets/mysql_user_password" \ --env "MYSQL_USER_DATABASE=appdb" \ --volume ${data_volume_3}:/var/lib/mysql \ - --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/run/secrets:ro \ + --volume ${PWD}/${TEST_DIRECTORY}/fixture/secrets:/var/run/secrets:ro \ jdeathe/centos-ssh-mysql:latest \ &> /dev/null From a39b6589c86c0b28b5f879446cbfea5d3985ad1e Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 23 Aug 2019 18:46:17 +0100 Subject: [PATCH 95/98] Release changes for 1.11.1 and 2.3.1. --- CHANGELOG.md | 2 +- Dockerfile | 2 +- README.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c5781..c78a843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Summary of release changes. -### 2.3.1 - Unreleased +### 2.3.1 - 2019-08-23 - Updates healthcheck script for consistency. - Fixes container mount point for emulating and testing secrets. diff --git a/Dockerfile b/Dockerfile index f4287ca..194fd00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM jdeathe/centos-ssh:2.6.0 -ARG RELEASE_VERSION="2.3.0" +ARG RELEASE_VERSION="2.3.1" # ------------------------------------------------------------------------------ # Base install of required packages diff --git a/README.md b/README.md index 9580c91..33316a0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Tags and respective `Dockerfile` links -- `centos-7-mysql57-community`, [`2.3.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.3.0) [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, [`1.11.0`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.11.0) [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- `centos-7-mysql57-community`, [`2.3.1`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.3.1) [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- `centos-6`, [`1.11.1`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.11.1) [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) ## Overview @@ -25,7 +25,7 @@ $ docker run -d \ --name mysql.1 \ -p 3306:3306 \ -v /var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.3.0 + jdeathe/centos-ssh-mysql:2.3.1 ``` Verify the named container's process status and health. @@ -114,7 +114,7 @@ $ docker stop mysql.1 && \ --env "MYSQL_USER_PASSWORD=" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume mysql.1.data-mysql:/var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.3.0 + jdeathe/centos-ssh-mysql:2.3.1 ``` The environmental variable `MYSQL_SUBNET` is optional but can be used to generate users with access to databases outside the `localhost`, (the default for the root user). In the example, the subnet definition `0.0.0.0/0.0.0.0` allows connections from any network which is equivalent to the wildcard symbol, `%`, in MySQL GRANT definitions. From 174def030da6285fc7c033d638272fb625f47f48 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 27 Sep 2019 13:59:36 +0100 Subject: [PATCH 96/98] #269: Updates upstream source to 2.6.1 --- .dockerignore | 25 ++++++------ .gitignore | 5 +-- CHANGELOG.md | 15 +++++++ Dockerfile | 2 +- Makefile | 45 +++++++++++++++++---- README.md | 4 +- docs/testing.md | 13 +++++++ src/usr/bin/healthcheck | 78 ++++++++----------------------------- src/usr/sbin/mysqld-wrapper | 3 +- test/health_status | 77 ++++++++++++++++++------------------ testing.md | 21 ---------- 11 files changed, 142 insertions(+), 146 deletions(-) create mode 100644 docs/testing.md delete mode 100644 testing.md diff --git a/.dockerignore b/.dockerignore index 9920c41..5f20999 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,15 @@ -.env -.env.example -.git -.gitignore -dist -images -test -docker-compose.yml -LICENSE -README-short.txt -*.md +/.env +/.env.example +/.git +/.gitignore +/dist +/docs +/images +/test +/docker-compose.yml +/LICENSE +/README-short.txt +/*.md !README.md **/*.mk -**/Makefile \ No newline at end of file +**/Makefile diff --git a/.gitignore b/.gitignore index 01faf4b..3bd889e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -.env -packages -dist \ No newline at end of file +/.env +/dist diff --git a/CHANGELOG.md b/CHANGELOG.md index c78a843..82dd32a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ Summary of release changes. +### 2.3.2 - Unreleased + +- Deprecate Makefile target `logs-delayed`; replaced with `logsdef`. +- Updates source image to [2.6.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.6.1). +- Updates `test/health_status` helper script with for consistency. +- Updates Makefile target `logs` to accept `[OPTIONS]` (e.g `make -- logs -ft`). +- Updates info/error output for consistency. +- Updates healthcheck failure messages to remove EOL character that is rendered in status response. +- Updates wrapper script; only emit "waiting on" info message if bootstrap hasn't completed. +- Updates ordering of Tags and respective Dockerfile links in README.md for readability. +- Adds improved test workflow; added `test-setup` target to Makefile. +- Adds Makefile target `logsdef` to handle deferred logs output within a target chain. +- Adds `/docs` directory for supplementary documentation. +- Fixes validation failure of 0 second --timeout value in `test/health_status`. + ### 2.3.1 - 2019-08-23 - Updates healthcheck script for consistency. diff --git a/Dockerfile b/Dockerfile index 194fd00..0f83730 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM jdeathe/centos-ssh:2.6.0 +FROM jdeathe/centos-ssh:2.6.1 ARG RELEASE_VERSION="2.3.1" diff --git a/Makefile b/Makefile index 6416f1b..35ff77a 100644 --- a/Makefile +++ b/Makefile @@ -25,10 +25,11 @@ Targets: images Show container's image details. load Loads from the distribution package. Requires DOCKER_IMAGE_TAG variable. - logs Display log output from the running container. - logs-delayed Display log output from the running container after - backing off for STARTUP_TIME seconds. This can be - necessary when chaining make targets together. + logs [OPTIONS] Display log output from the container. + logsdef Display log output from the container deferred for + STARTUP_TIME seconds. This will work in a chain + unlike the logs target. + logs-delayed [DEPRECATED] Replaced with logsdef. pause Pause the running container. pull Pull the release image from the registry. Requires the DOCKER_IMAGE_TAG variable. @@ -45,6 +46,7 @@ Targets: stop Stop the container when in a running state. terminate Unpause, stop and remove the container. test Run all test cases. + test-setup Install test dependencies. top [ps OPTIONS] Display the running processes of the container. unpause Unpause the container when in a paused state. @@ -161,6 +163,7 @@ endef _require-docker-image-tag \ _require-docker-release-tag \ _require-package-path \ + _require-root \ _test-prerequisites \ _usage \ all \ @@ -176,6 +179,7 @@ endef images \ load \ logs \ + logsdef \ logs-delayed \ pause \ pull \ @@ -191,6 +195,7 @@ endef stop \ terminate \ test \ + test-setup \ top \ unpause @@ -365,9 +370,17 @@ _require-package-path: exit 1; \ fi +_require-root: + @ if [[ $${EUID} -ne 0 ]]; \ + then \ + >&2 printf -- '%sMust be run as root\n' \ + "$(PREFIX_STEP_NEGATIVE)"; \ + exit 1; \ + fi + _test-prerequisites: ifeq ($(shpec),) - $(error "Please install shpec.") + $(error "Please install shpec. Try: DOCKER_NAME=$(DOCKER_NAME) make test-setup") endif _usage: @@ -581,14 +594,20 @@ install: | \ logs: \ _prerequisites \ _require-docker-container - @ $(docker) logs $(DOCKER_NAME) + @ $(docker) logs \ + $(filter-out $@, $(MAKECMDGOALS)) \ + $(DOCKER_NAME) +%:; @: -logs-delayed: \ +logsdef: \ _prerequisites \ _require-docker-container @ sleep $(STARTUP_TIME) @ $(MAKE) logs +logs-delayed: \ + logsdef + load: \ _prerequisites \ _require-docker-release-tag \ @@ -980,6 +999,18 @@ test: \ "Functional test" @ SHPEC_ROOT=$(SHPEC_ROOT) $(shpec) +test-setup: \ + _require-root + @ printf -- '%s%s\n' \ + "$(PREFIX_STEP)" \ + "Installing shpec" + @ bash -c "$$(curl -LSs \ + https://raw.githubusercontent.com/rylnd/shpec/master/install.sh \ + )" + @ ln -sf \ + /usr/local/bin/shpec \ + /usr/bin/shpec + unpause: \ _prerequisites \ _require-docker-container \ diff --git a/README.md b/README.md index 33316a0..9c276a5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Tags and respective `Dockerfile` links -- `centos-7-mysql57-community`, [`2.3.1`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.3.1) [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- `centos-6`, [`1.11.1`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.11.1) [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- [`2.3.1`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.3.1), `centos-7-mysql57-community` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- [`1.11.1`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.11.1), `centos-6` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) ## Overview diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..540d82d --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,13 @@ +# Testing + +## Functional + +The functional test cases are written in [shpec](https://github.com/rylnd/shpec). + +To run the tests use the `test` Makefile target after building. + +> *Note:* You might need to run via sudo if your environment requires root privileges to run docker. + +``` +$ make build test +``` diff --git a/src/usr/bin/healthcheck b/src/usr/bin/healthcheck index 55ed782..ce5eaba 100755 --- a/src/usr/bin/healthcheck +++ b/src/usr/bin/healthcheck @@ -130,45 +130,6 @@ function __mysql_get_option () "${value:-$3}" } -function __print_message () -{ - local -r quiet="${quiet:-false}" - local -r type="${1:-}" - - local message="${2:-}" - local prefix - - case "${type}" in - error) - prefix="ERROR: " - ;; - info) - prefix="INFO: " - ;; - *) - message="${type}" - ;; - esac - - if [[ ${quiet} == true ]] \ - && [[ ${type} != error ]] - then - return 0 - elif [[ ${quiet} == true ]] \ - && [[ ${type} == error ]] - then - >&2 printf -- \ - '%s%s\n' \ - "${prefix}" \ - "${message}" - else - printf -- \ - '%s%s\n' \ - "${prefix}" \ - "${message}" - fi -} - function __record_exit_status () { local -r status_directory="${status_path%/*}" @@ -255,14 +216,14 @@ function main () __usage break ;; - -i) - interval="${2}" - shift 2 || break - ;; --interval=*) interval="${1#*=}" shift 1 ;; + -i|--interval) + interval="${2}" + shift 2 || break + ;; -q|--quiet) quiet="true" shift 1 @@ -275,26 +236,24 @@ function main () if [[ ! ${interval} =~ ${pattern_seconds_in_minute} ]] then - __print_message \ - "error" \ - "Invalid interval." + >&2 printf -- \ + 'ERROR: %s invalid interval' \ + "${0##*/}" exit 1 fi if ! ps axo command \ | grep -qE '^/usr/bin/python /usr/bin/supervisord' then - __print_message \ - "error" \ - "supervisord not running." + >&2 printf -- \ + "supervisord not running" exit 1 fi if [[ ${system_timezone} != "${zone}" ]] then - __print_message \ - "error" \ - "system-timezone zone mismatch." + >&2 printf -- \ + "system-timezone zone mismatch" exit 1 fi @@ -303,9 +262,8 @@ function main () if [[ -f /var/lock/subsys/mysqld-bootstrap ]] \ || ! __is_mysql_data_directory_populated then - __print_message \ - "error" \ - "Bootstrap failed to complete." + >&2 printf -- \ + "bootstrap failed to complete" exit 1 fi fi @@ -315,9 +273,8 @@ function main () if ! ps axo command \ | grep -qE "^${mysqld} " then - __print_message \ - "error" \ - "Service process not running." + >&2 printf -- \ + "service process not running" exit 1 fi @@ -337,9 +294,8 @@ function main () if ! __have_mysql_access then - __print_message \ - "error" \ - "root@localhost access failed." + >&2 printf -- \ + "root@localhost access failed" exit 1 fi fi diff --git a/src/usr/sbin/mysqld-wrapper b/src/usr/sbin/mysqld-wrapper index 8cd543c..caf60fe 100755 --- a/src/usr/sbin/mysqld-wrapper +++ b/src/usr/sbin/mysqld-wrapper @@ -150,7 +150,8 @@ function main () __get_proxy )" - if [[ ${verbose} == true ]] + if [[ ${verbose} == true ]] \ + && [[ ! -f ${bootstrap_state_file} ]] then printf -- \ 'INFO: %s waiting on %s\n' \ diff --git a/test/health_status b/test/health_status index c26a7b9..6aae041 100755 --- a/test/health_status +++ b/test/health_status @@ -1,8 +1,10 @@ #!/usr/bin/env bash +set -e + function __cleanup () { - local -r fifo_path="${1:-}" + local -r fifo_path="${1}" local -r pid="${2:-0}" if [[ -p ${fifo_path} ]] @@ -29,7 +31,7 @@ function __print_status () local colour_negative='\033[1;31m' local colour_positive='\033[1;32m' local colour_reset='\033[0m' - local type="${1:-}" + local type="${1}" local message="${2:-${type}}" if [[ ${option_quiet} == true ]] @@ -71,9 +73,10 @@ function __print_status () function __usage() { cat <<-EOF - Usage: $(basename ${0}) -c [OPTIONS] - $(basename ${0}) --container= [OPTIONS] - $(basename ${0}) [-h|--help] + + Usage: ${0##*/} -c [OPTIONS] + ${0##*/} --container= [OPTIONS] + ${0##*/} [-h|--help] Gets health_status events and returns the status. @@ -116,7 +119,6 @@ function health_status () local until_timestamp local until - # Parse install options while [[ "${#}" -gt 0 ]] do case "${1}" in @@ -128,18 +130,18 @@ function health_status () option_monochrome=true shift 1 ;; - -c) - if [[ -z ${2:-} ]] + --container=*) + container="${1#*=}" + shift 1 + ;; + -c|--container) + if [[ -z ${2} ]] then __usage fi - container="${2:-}" + container="${2}" shift 2 ;; - --container=*) - container="${1#*=}" - shift 1 - ;; -q|--quiet) option_quiet=true shift 1 @@ -148,18 +150,18 @@ function health_status () since_timestamp="${1#*=}" shift 1 ;; - -t) - if [[ -z ${2:-} ]] + --timeout=*) + timeout="${1#*=}" + shift 1 + ;; + -t|--timeout) + if [[ -z ${2} ]] then __usage fi - timeout="${2:-}" + timeout="${2}" shift 2 ;; - --timeout=*) - timeout="${1#*=}" - shift 1 - ;; *) __usage ;; @@ -173,9 +175,9 @@ function health_status () if ! [[ ${timeout} =~ ${pattern_timeout} ]] then - printf -- \ - '[ERROR] Invalid --time value.\n' \ - >&2 + >&2 printf -- \ + 'ERROR: %s invalid --timeout value\n' \ + "${0##*/}" __usage fi @@ -198,12 +200,12 @@ function health_status () fi # Fail if operator attempts start time limit before end limit. - if ! [[ ${since_timestamp} =~ ${pattern_timestamp} ]] \ - || (( since_timestamp > until_timestamp )) + if (( timeout > 0 )) && (( since_timestamp > until_timestamp )) \ + || ! [[ ${since_timestamp} =~ ${pattern_timestamp} ]] then - printf -- \ - '[ERROR] Invalid --since value.\n' \ - >&2 + >&2 printf -- \ + 'ERROR: %s invalid --since value\n' \ + "${0##*/}" __usage fi @@ -216,34 +218,33 @@ function health_status () trap \ "__cleanup \"${fifo_path}\"" \ - INT TERM EXIT + EXIT INT TERM mkfifo \ -m 0600 \ "${fifo_path}" + cd / docker events \ --format '{{.Status}}' \ --filter "event=health_status" \ --filter "container=${container}" \ ${since} \ ${until} \ - > "${fifo_path}" \ - & + 0> /dev/null \ + 1> "${fifo_path}" \ + 2> /dev/null \ + & disown pid="${!}" - disown + cd - \ + > /dev/null trap \ "__cleanup \"${fifo_path}\" \"${pid}\"" \ - INT TERM EXIT + EXIT INT TERM while IFS= read -r health_status || [[ -n ${health_status} ]] do - if [[ ${health_status} =~ ${pattern_starting} ]] - then - __print_status "starting" - fi - if [[ ${health_status} =~ ${pattern_healthy} ]] then __print_status "healthy" diff --git a/testing.md b/testing.md deleted file mode 100644 index 4986749..0000000 --- a/testing.md +++ /dev/null @@ -1,21 +0,0 @@ -# Testing - -## Functional - -### Installation - -The functional test cases are written in [shpec](https://github.com/rylnd/shpec). - -To run the tests install shpec with the installer. - -``` -$ bash -c "$(curl -L https://raw.github.com/rylnd/shpec/master/install.sh)" -``` - -### Usage - -To manually run the test cases, from the project root: - -``` -$ SHPEC_ROOT=test/shpec shpec -``` From 4bf6f5f6ddcb20ae15bc4cd01b2af11967ce6db1 Mon Sep 17 00:00:00 2001 From: James Deathe Date: Fri, 27 Sep 2019 22:29:29 +0100 Subject: [PATCH 97/98] #269: Updates packages --- CHANGELOG.md | 2 ++ Dockerfile | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82dd32a..c0dc862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Summary of release changes. - Deprecate Makefile target `logs-delayed`; replaced with `logsdef`. - Updates source image to [2.6.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.6.1). +- Updates `mysql-community-server` package to 5.7.27-1. +- Updates `psmisc` package to 22.20-16. - Updates `test/health_status` helper script with for consistency. - Updates Makefile target `logs` to accept `[OPTIONS]` (e.g `make -- logs -ft`). - Updates info/error output for consistency. diff --git a/Dockerfile b/Dockerfile index 0f83730..698ddb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,11 +19,11 @@ RUN { printf -- \ && yum -y install \ --setopt=tsflags=nodocs \ --disableplugin=fastestmirror \ - mysql-community-client-5.7.26-1.el7 \ - mysql-community-common-5.7.26-1.el7 \ - mysql-community-libs-5.7.26-1.el7 \ - mysql-community-server-5.7.26-1.el7 \ - psmisc-22.20-15.el7 \ + mysql-community-client-5.7.27-1.el7 \ + mysql-community-common-5.7.27-1.el7 \ + mysql-community-libs-5.7.27-1.el7 \ + mysql-community-server-5.7.27-1.el7 \ + psmisc-22.20-16.el7 \ sshpass-1.06-2.el7 \ && yum versionlock add \ mysql-community-* \ From 75b30627c9379ce63152f0f8d493d286b6cbabfd Mon Sep 17 00:00:00 2001 From: James Deathe Date: Sat, 28 Sep 2019 12:11:41 +0100 Subject: [PATCH 98/98] Release changes for: 2.3.2, 1.11.2 --- CHANGELOG.md | 2 +- Dockerfile | 2 +- README.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0dc862..9b50801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Summary of release changes. -### 2.3.2 - Unreleased +### 2.3.2 - 2019-09-28 - Deprecate Makefile target `logs-delayed`; replaced with `logsdef`. - Updates source image to [2.6.1](https://github.com/jdeathe/centos-ssh/releases/tag/2.6.1). diff --git a/Dockerfile b/Dockerfile index 698ddb7..69e1916 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM jdeathe/centos-ssh:2.6.1 -ARG RELEASE_VERSION="2.3.1" +ARG RELEASE_VERSION="2.3.2" # ------------------------------------------------------------------------------ # Base install of required packages diff --git a/README.md b/README.md index 9c276a5..dbf3a3c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Tags and respective `Dockerfile` links -- [`2.3.1`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.3.1), `centos-7-mysql57-community` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) -- [`1.11.1`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.11.1), `centos-6` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) +- [`2.3.2`](https://github.com/jdeathe/centos-ssh-mysql/tree/2.3.2), `centos-7-mysql57-community` [(centos-7-mysql57-community/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-7-mysql57-community/Dockerfile) +- [`1.11.2`](https://github.com/jdeathe/centos-ssh-mysql/tree/1.11.2), `centos-6` [(centos-6/Dockerfile)](https://github.com/jdeathe/centos-ssh-mysql/blob/centos-6/Dockerfile) ## Overview @@ -25,7 +25,7 @@ $ docker run -d \ --name mysql.1 \ -p 3306:3306 \ -v /var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.3.1 + jdeathe/centos-ssh-mysql:2.3.2 ``` Verify the named container's process status and health. @@ -114,7 +114,7 @@ $ docker stop mysql.1 && \ --env "MYSQL_USER_PASSWORD=" \ --env "MYSQL_USER_DATABASE=app-db" \ --volume mysql.1.data-mysql:/var/lib/mysql \ - jdeathe/centos-ssh-mysql:2.3.1 + jdeathe/centos-ssh-mysql:2.3.2 ``` The environmental variable `MYSQL_SUBNET` is optional but can be used to generate users with access to databases outside the `localhost`, (the default for the root user). In the example, the subnet definition `0.0.0.0/0.0.0.0` allows connections from any network which is equivalent to the wildcard symbol, `%`, in MySQL GRANT definitions.