feat: 更新脚本;增加CI/CD模板

This commit is contained in:
2023-08-21 16:47:15 +08:00
parent 4f8b2215ea
commit 7a2fbef429
28 changed files with 1861 additions and 371 deletions
+113 -112
View File
@@ -1,156 +1,157 @@
# Ver: 1.8 by Endial Fang (endial@126.com)
# Ver: 1.10 by Endial Fang (endial@126.com)
#
# 默认变量 ========================================================================
# 该部分变量为系统根据编译命令默认设置
# `TARGETPLATFORM`:构建后的目标平台信息。如 `linux/amd64``linux/arm/v7``windows/amd64`
# `TARGETOS`:目标平台信息(TARGETPLATFORM)中的操作系统部分,如:`linux`、`windows`
# `TARGETARCH`:目标平台信息(TARGETPLATFORM)中的平台架构部分,如:`amd64`、`arm`
# `TARGETVARIANT`:目标平台信息(TARGETPLATFORM)中的版本变体部分,如:`v7`
# `BUILDPLATFORM`:用于构建的节点平台信息
# `BUILDOS`:用于构建的节点平台信息(BUILDPLATFORM)中的操作系统部分
# `BUILDARCH`用于构建的节点平台信息(BUILDPLATFORM)中的平台架构部分
# `BUILDVARIANT`用于构建的节点平台信息(BUILDPLATFORM)中的版本变体部分
# 可变参数 ========================================================================
# 该部分变量,在编译命令中通过 `--build-arg` 传入;如果未设置,则使用下面对应的默认值
# 设置当前应用名称及版本
ARG app_name=test
ARG app_version=1.0.0
# 设置默认仓库地址,默认为 阿里云 仓库
ARG registry_url="registry.cn-shenzhen.aliyuncs.com"
# 设置 apt-get 源:default / tencent / ustc / aliyun / huawei
ARG apt_source=aliyun
# 编译镜像时指定用于加速的本地服务器地址
ARG local_url=""
ARG APP_NAME=test # 设置当前应用名称
ARG APP_VER=1.0.0 # 设置当前应用版本
ARG REGISTRY_URL="docker.colovu.com/" # 设置默认仓库地址,默认为本地仓库;定义时需要包含末尾的`/`
ARG APT_SOURCE=aliyun # 设置 apt-get 源:default / ustc / aliyun
ARG LOCAL_URL="http://local.colovu.com/dist" # 编译镜像时指定用于加速的本地软件包存储服务器地址
# 0. 预处理 ======================================================================
FROM ${registry_url}/colovu/dbuilder as builder
FROM --platform=${TARGETPLATFORM:-linux/amd64} ${REGISTRY_URL}colovu/dbuilder:12 as builder
#FROM --platform=${TARGETPLATFORM:-linux/amd64} ${REGISTRY_URL}colovu/openjdk:8 as builder
#FROM --platform=${TARGETPLATFORM:-linux/amd64} ${REGISTRY_URL}colovu/golang:1.21 as builder
# 声明需要使用的全局可变参数
ARG app_name
ARG app_version
ARG registry_url
ARG apt_source
ARG local_url
ARG APP_NAME
ARG APP_VER
ARG APT_SOURCE
ARG LOCAL_URL
# 选择软件包源(Optional),以加速后续软件包安装
RUN select_source ${apt_source};
RUN select_source ${APT_SOURCE};
# 安装依赖的软件包及库(Optional)
#RUN install_pkg xz-utils
# 设置工作目录
WORKDIR /tmp
# dbuilder已安装: libtool libltdl7 libltdl-dev libssl3 libssl-dev
# 下载并解压软件包
#RUN set -eux; \
# appName=${app_name}-${app_version}.tgz; \
# appKeys="0xB42F6819007F00F88E364FD4036A9C25BF357DD4"; \
# sha256="04fa1fddc39bd1aecb6739dd5dd73858a3515b427acd1e2947a66dadce868d68"; \
# [ ! -z ${local_url} ] && localURL=${local_url}/${app_name}; \
# appUrls="${localURL:-} \
# https://github.com/tianon/gosu/releases/download/${appVersion} \
# "; \
# download_pkg install ${appName} "${appUrls}" -g "${appKeys}" -s "${sha256}"; \
# download_pkg unpack ${appName} "${appUrls}"; \
# chmod +x /usr/local/bin/${appName};
RUN set -eux; \
if [ "$TARGETARCH" = "arm64" ]]; \
then appArch=aarch64; \
else appArch=x64; \
fi; \
appName="${APP_NAME}-${appArch}-${APP_VER}.tar.gz"; \
appKeys="0xB42F6819007F00F88E364FD4036A9C25BF357DD4"; \
sha256="04fa1fddc39bd1aecb6739dd5dd73858a3515b427acd1e2947a66dadce868d68"; \
[ -n ${LOCAL_URL} ] && localURL=${LOCAL_URL}/${APP_NAME}; \
appUrls="${localURL:-} \
https://github.com/tianon/gosu/releases/download/${APP_VER} \
"; \
download_pkg install ${appName} "${appUrls}" -g "${appKeys}" -s "${sha256}"; \
download_pkg unpack ${appName} "${appUrls}"; \
chmod +x /usr/local/bin/${appName};
# 源码编译
#RUN set -eux; \
# APP_SRC="/tmp/${app_name}-${app_version}"; \
# cd ${APP_SRC}; \
# ./configure \
# --prefix=/usr/local/${app_name} \
# CPPFLAGS="-I/usr/local/include -D_GNU_SOURCE" \
# LDFLAGS="-L/usr/local/lib" \
# ; \
# make -j "$(nproc)"; \
# make install;
RUN set -eux; \
APP_ARCH=`arch` \
APP_SRC="/tmp/${APP_NAME}-${APP_VER}"; \
cd ${APP_SRC}; \
LDFLAGS="-L/usr/local/lib -L/usr/lib/${APP_ARCH}-linux-gnu" \
CPPFLAGS="-I/usr/local/include -D_GNU_SOURCE" \
./configure \
--prefix=/usr/local/${APP_NAME} \
; \
make -j "$(nproc)" && make install; \
strip /usr/local/${APP_NAME}/sbin/${APP_NAME};
# 删除编译生成的多余文件
RUN set -eux; \
find /usr/local -name '*.a' -delete; \
rm -rf /usr/local/${app_name}/share; \
rm -rf /usr/local/${app_name}/include;
rm -rf /usr/local/${APP_NAME}/share; \
rm -rf /usr/local/${APP_NAME}/include;
# 检测并生成依赖文件记录
RUN set -eux; \
find /usr/local/${app_name} -type f -executable -exec ldd '{}' ';' | \
find /usr/local/${APP_NAME} -type f -executable -exec ldd '{}' ';' | \
awk '/=>/ { print $(NF-1) }' | \
sort -u | \
xargs -r readlink -f | \
xargs -r dpkg-query --search 2>/dev/null | \
cut -d: -f1 | \
sort -u >/usr/local/${app_name}/runDeps;
sort -u >>/usr/local/${APP_NAME}/runDeps;
# 1. 生成镜像 =====================================================================
FROM ${registry_url}/colovu/debian:buster
FROM ${registry_url}/colovu/openjre:8
FROM --platform=${TARGETPLATFORM:-linux/amd64} ${REGISTRY_URL}colovu/debian:12
#FROM --platform=${TARGETPLATFORM:-linux/amd64} ${REGISTRY_URL}colovu/openjre:8
# 声明需要使用的全局可变参数
ARG app_name
ARG app_version
ARG registry_url
ARG apt_source
ARG local_url
# 声明需要使用的全局可变参数ARG声明的变量仅编译打包阶段有效)
ARG APP_NAME
ARG APP_VER
ARG APT_SOURCE
# 镜像所包含应用的基础信息,定义环境变量,供后续脚本使用
ENV APP_NAME=${app_name} \
APP_EXEC=${app_name} \
APP_VERSION=${app_version}
# 定义应用的基础信息变量(ENV声明的变量实例化后容器内有效)
ENV APP_NAME=${APP_NAME} \
APP_VER=${APP_VER} \
APP_EXEC=${APP_NAME} \
APP_USER=${APP_NAME} \
APP_HOME=/srv/${APP_NAME} \
APP_BASE=/usr/local/${APP_NAME}
ENV APP_HOME_DIR=/usr/local/${APP_NAME} \
APP_DEF_DIR=/etc/${APP_NAME}
ENV PATH="${APP_HOME_DIR}/sbin:${APP_HOME_DIR}/bin:${PATH}" \
LD_LIBRARY_PATH="${APP_HOME_DIR}/lib"
# 增加应用可执行文件及库文件搜索路径
ENV PATH="${PATH}:/usr/local/${APP_NAME}/bin" \
CLASSPATH=".:/usr/local/${APP_NAME}/lib" \
LD_LIBRARY_PATH="/usr/local/${APP_NAME}/lib"
LABEL \
"Version"="v${app_version}" \
"Description"="Docker image for ${app_name}(v${app_version})." \
"Dockerfile"="https://github.com/colovu/docker-${app_name}" \
"Version"="v${APP_VER}" \
"Description"="Docker image for ${APP_NAME}." \
"Github"="https://github.com/colovu/docker-${APP_NAME}" \
"Vendor"="Endial Fang (endial@126.com)"
# 从预处理过程中拷贝软件包(Optional),可以使用阶段编号或阶段命名定义来源
COPY --from=0 /usr/local/${APP_NAME} /usr/local/${APP_NAME}
VOLUME ["/srv/${APP_NAME}"] # 默认提供的数据卷
WORKDIR /srv/${APP_NAME} # 设置工作目录
EXPOSE 8080 8443 # 默认使用gosu切换为新建用户启动,必须保证端口在1024之上
# 拷贝应用使用的客制化脚本,并创建对应的用户及数据存储目录
# 从预处理过程中拷贝软件包,可以使用阶段编号或阶段命名定义来源
COPY --from=builder /usr/local/${APP_NAME} /usr/local/${APP_NAME}
# 拷贝应用使用的客制化脚本
COPY customer /
RUN set -eux; \
prepare_env; \
/bin/bash -c "ln -sf /usr/local/${APP_NAME}/etc/${APP_NAME} /etc/";
# 选择软件包源(Optional),以加速后续软件包安装
RUN select_source ${apt_source}
# 安装依赖的软件包及库(Optional)
RUN install_pkg `cat /usr/local/${APP_NAME}/runDeps`;
#RUN install_pkg bash sudo libssl1.1
# 执行预处理脚本,并验证安装的软件包
RUN set -eux; \
override_file="/usr/local/overrides/overrides-${APP_VERSION}.sh"; \
[ -e "${override_file}" ] && /bin/bash "${override_file}"; \
\
# 创建对应的用户及数据存储目录
useradd -U -u 996 -d ${APP_HOME} -s /usr/sbin/nologin -r ${APP_USER}; \
mkdir -p /var/log/${APP_NAME} /var/run/${APP_NAME} /var/cache/${APP_NAME} ${APP_HOME}/{conf,data,cert}; \
chown -R ${APP_USER}:${APP_USER} /var/log/${APP_NAME} /var/run/${APP_NAME} /var/cache/${APP_NAME} ${APP_HOME}; \
\
/bin/bash -c "ln -sf ${APP_BASE}/etc/${APP_NAME} /etc/"; \
\
# 选择软件包源,以加速后续软件包安装
select_source ${APT_SOURCE}; \
\
# 安装应用依赖的软件包及库
install_pkg curl; \
install_pkg `cat ${APP_BASE}/runDeps`; \
\
# 执行后处理脚本
overrideShell="/usr/local/overrides/overrides-${APP_VER}.sh"; \
[ -e "${overrideShell}" ] && /bin/bash "${overrideShell}"; \
\
# 验证安装的应用
${APP_EXEC} --version ;
# 默认提供的数据卷
VOLUME ["/srv/conf", "/srv/data", "/srv/datalog", "/srv/cert", "/var/log"]
# 默认non-root用户启动,必须保证端口在1024之上
EXPOSE 8080 8443
# 关闭基础镜像的健康检查
#HEALTHCHECK NONE
# 应用健康状态检查
#HEALTHCHECK --interval=30s --timeout=30s --retries=3 \
# CMD curl -fs http://localhost:8080/ || exit 1
#HEALTHCHECK --interval=10s --timeout=10s --retries=3 \
# CMD netstat -ltun | grep 8080
# 使用 non-root 用户运行后续的命令
USER 1001
# 设置工作目录
WORKDIR /srv/conf
# 容器初始化命令
ENTRYPOINT ["/usr/local/bin/entry.sh"]
# 应用程序的启动命令,必须使用非守护进程方式运行
CMD ["/usr/local/bin/run.sh"]
#HEALTHCHECK NONE
#HEALTHCHECK --interval=30s --timeout=30s --retries=3 CMD curl -fs http://localhost:8080/ || exit 1
#HEALTHCHECK --interval=10s --timeout=10s --retries=3 CMD netstat -ltun | grep 8080
# 容器入口脚本及应用启动命令
ENTRYPOINT ["dumb-init", "entry.sh"]
CMD ["run.sh"]
+11 -14
View File
@@ -6,33 +6,30 @@
image_name :=colovu/template
# 定义默认镜像仓库地址
registry_url :=docker.io
REGISTRY_URL :=docker.colovu.com
# 定义系统默认使用的源服务器,包含:default / tencent / ustc / aliyun / huawei
apt_source :=tencent
# 定义系统默认使用的源服务器,包含:default / ustc / aliyun
APT_SOURCE :=aliyun
# 定义镜像TAG,类似:
# <镜像名>:<分支名>-<7位Git ID> # Git 仓库且无文件修改直接编译
# <镜像名>:<分支名>-<年月日>-<时分秒> # Git 仓库有文件修改后的编译
# <镜像名>:latest-<年月日>-<时分秒> # 非 Git 仓库编译
# <镜像名>:<分支名>-<7位Git ID> # Git 仓库且无文件修改直接编译
# <镜像名>:<分支名>-<年月日>-<时分秒> # Git 仓库有文件修改后的编译
# <镜像名>:latest-<年月日>-<时分秒> # 非 Git 仓库编译
current_subversion:=$(shell if [ ! `git status >/dev/null 2>&1` ]; then git rev-parse --short HEAD; else date +%y%m%d-%H%M%S; fi)
image_tag:=$(shell if [ ! `git status >/dev/null 2>&1` ]; then git rev-parse --abbrev-ref HEAD | sed -e 's/master/latest/'; else echo "latest"; fi)-$(current_subversion)
image_tag:=$(shell if [ ! `git status >/dev/null 2>&1` ]; then git rev-parse --abbrev-ref HEAD | sed -e 's/master/latest/' | sed -e 's/main/latest/'; else echo "latest"; fi)-$(current_subversion)
build-arg:=--build-arg registry_url=$(registry_url)
build-arg+=--build-arg apt_source=$(apt_source)
build-arg:=--build-arg REGISTRY_URL=$(REGISTRY_URL)
build-arg+=--build-arg APT_SOURCE=$(APT_SOURCE)
# 设置本地下载服务器路径,加速调试时的本地编译速度
local_ip:=`echo "en0 eth0" | xargs -n1 ip addr show 2>/dev/null | grep inet | grep -v 127.0.0.1 | grep -v inet6 | tr "/" " " | awk '{print $$2}'`
build-arg+=--build-arg local_url=http://$(local_ip)/dist-files
build-arg+=--build-arg LOCAL_URL=http://local.colovu.com/dist
.PHONY: build clean clearclean upgrade
# 屏蔽 "Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them"
export DOCKER_SCAN_SUGGEST=false
build:
@echo "Build $(image_name):$(image_tag)"
@docker build --progress plain --force-rm $(build-arg) -t $(image_name):$(image_tag) .
@docker buildx build --progress plain --force-rm $(build-arg) -t $(image_name):$(image_tag) .
@echo "Add tag: $(image_name):latest"
@docker tag $(image_name):$(image_tag) $(image_name):latest
@echo "Build complete"
+45 -70
View File
@@ -13,29 +13,23 @@
**镜像信息:**
* 镜像地址:
- 阿里云: registry.cn-shenzhen.aliyuncs.com/colovu/imgname:1.0
- DockerHubcolovu/imgname:1.0
* 依赖镜像:debian:buster
> 后续相关命令行默认使用`[Docker Hub](https://hub.docker.com)`镜像服务器做说明
- 阿里云: registry.cn-shenzhen.aliyuncs.com/colovu/imgname:latest
- DockerHubcolovu/imgname:latest
- Colovu Registry: docker.colovu.com/colovu/imgname:latest
- 依赖镜像:colovu/debian:12
> 后续相关命令行默认使用`[Colovu Registry](https://docker.colovu.com)`镜像服务器做说明
## TL;DR
Docker 快速启动命令:
```shell
# 从 Docker Hub 服务器下载镜像并启动
$ docker run -d -e ALLOW_ANONYMOUS_LOGIN=yes --name imgname colovu/imgname
# 从 Aliyun 服务器下载镜像并启动
$ docker run -d -e ALLOW_ANONYMOUS_LOGIN=yes --name imgname registry.cn-shenzhen.aliyuncs.com/colovu/imgname
# 从 Registry 服务器下载镜像并启动
$ docker run -d -e ALLOW_ANONYMOUS_LOGIN=yes --name imgname docker.colovu.com/colovu/imgname:latest
```
- `colovu/imgname:<TAG>`:镜像名称及版本标签;标签不指定时默认使用`latest`
- `docker.colovu.com/colovu/imgname:<TAG>`:镜像名称及版本标签 TAG;标签不指定时默认使用最新版本
Docker-Compose 快速启动命令:
@@ -51,95 +45,87 @@ $ curl -sSL -o https://raw.githubusercontent.com/colovu/docker-imgname/master/do
$ docker-compose up -d
```
---
## 默认对外声明
### 端口
- xx:端口用途
- 8080:端口用途
### 数据卷
镜像默认提供以下数据卷定义,默认数据分别存储在自动生成的应用名对应`imgname`子目录中
镜像默认提供以下数据卷定义:
```shell
/var/datalog # 数据操作日志文件
/srv/conf # 配置文件
/srv/data # 数据文件,主要存放应用数据
/srv/cert # 证书文件存放目录
/srv/imgname/conf # 配置文件
/srv/imgname/data # 数据文件,主要存放应用数据
/srv/imgname/cert # 证书文件存放目录
/var/imgname/binlog # 数据操作日志文件
/var/log # 日志输出
/var/run # 系统运行时文件,如 PID 文件
/var/log/imgname # 日志输出
/var/run/imgname # 系统运行时文件,如 PID 文件
```
如果需要持久化存储相应数据,需要**在宿主机建立本地目录**,并在使用镜像初始化容器时进行映射。宿主机相关的目录中如果不存在对应应用`imgname`的子目录或相应数据文件,则容器会在初始化时创建相应目录及文件。
## 容器配置
在初始化 `AppName` 容器时,如果没有预置配置文件,可以在命令行中设置相应环境变量对默认参数进行修改。类似命令如下(配置环境变量`APP_ENV_KEY_NAME`的值为`key_value`):
```shell
$ docker run -d -e "APP_ENV_KEY_NAME=key_value" colovu/imgname
$ docker run -d -e "APP_ENV_KEY_NAME=key_value" docker.colovu.com/colovu/imgname:latest
```
### 自动变量替换
针对配置文件中的配置项,支持环境变量名自动替换,该类环境变量定义规则为:`APP_CFG_*=<val>`
针对应用配置文件中的配置项,支持环境变量名自动替换生成,该类环境变量需要使用统一前缀,定义规则为:`APP_CFG_*=<val>`
- `APP_CFG_`:环境变量自动替换标识,具备该前缀的环境变量会被自动处理并更新至配置文件
- `*`:配置文件中对应的配置项名,大小写需要符合实际参数名要求;特殊字符需要符合`特殊字符替换规则`
- `<val>`:配置项对应值
**特殊字符替换规则**
因为 Shell 变量只能以字母、数字和下划线组成,针对'xml'、'ini'等配置文件中使用的'.'、'-'等特殊字符,需要进行重定义及转换。预定义如下:
+ `_` ==> `_` : 应用配置属性中的`_`(下划线),与环境变量相同
+ `__` ==> `.` : 应用配置属性中的`.`(半角点),在环境变量中由`__`(双下划线)表示
+ `___` ==> `-` : 应用配置属性中的`-`(中划线),在环境变量中由`___`(三下划线)表示
例如:
```shell
# 设置配置文件中配置项 max_wal_size,传入容器的变量为(两者都可以):
APP_CFG_max_wal_size=400MB
# 常用于`key-value`类型的配置
APP_CFG_min_wal_size=100MB
APP_CFG_max_wal_size="400MB"
# 容器启动后,应用配置文件中对应配置项生效,且设置为相应值:
min_wal_size = '100MB'
max_wal_size = '400MB'
# 常用于`xml`类型的配置
APP_CFG_fs__defaultFS=hdfs://namenode:8020
APP_CFG_yarn__log___aggregation___enable=true
# 容器启动后,应用配置文件中对应配置项生效,且设置为相应值:
<property><name>fs.defaultFS</name><value>hdfs://namenode:8020</value></property>
<property><name>yarn.log-aggregation-enable</name><value>true</value></property>
```
**特殊字符替换规则**
- 针对使用`xml`格式的配置文件
+ `_` ==> `.` : 环境变量中的`下划线`会被转义为设置属性中的`半角点`
+ `__` ==> `_` : 环境变量中的`双下划线`会被转义为设置属性中的`单下划线`
+ `___` ==> `-` : 环境变量中的`三下划线`会被转义为设置属性中的`中划线`
- 针对使用`key-val`格式的配置文件
+ `_` ==> `_` : 环境变量中的`下划线`不会被替换
+ `__` ==> `.` : 环境变量中的`双下划线`会被转义为设置属性中的`半角点`
+ `___` ==> `-` : 环境变量中的`三下划线`会被转义为设置属性中的`中划线`
### 常规配置参数
常规配置参数用来配置容器基本属性,一般情况下需要设置,主要包括:
-
### 常规可选参数
如果没有必要,可选配置参数可以不用定义,直接使用对应的默认值,主要包括:
- `ENV_DEBUG`:默认值:**false**。设置是否输出容器调试信息。可选值:no、true、yes
- `ENV_DEBUG`:默认值:**false**。设置是否输出容器调试信息。可选值:false、no、true、yes
- `ALLOW_ANONYMOUS`:默认值:**no**。设置是否允许匿名链接。可选值:false、no、true、yes
### 集群配置参数
@@ -147,16 +133,12 @@ max_wal_size = '400MB'
-
### TLS配置参数
配置服务使用 TLS 加密时,通过以下参数进行配置:
-
## 安全
### 用户及密码
@@ -164,13 +146,13 @@ max_wal_size = '400MB'
`AppName`镜像默认禁用了无密码访问功能,在实际生产环境中建议使用用户名及密码控制访问;如果为了测试需要,可以使用以下环境变量启用无密码访问功能:
```shell
ALLOW_ANONYMOUS_LOGIN=yes
ALLOW_ANONYMOUS=yes
```
通过配置环境变量`APPNAME_PASSWORD`,可以启用基于密码的用户认证功能。命令行使用参考:
通过配置类似环境变量`APPNAME_USER``APPNAME_PASSWORD`,可以启用基于密码的用户认证功能。命令行使用参考:
```shell
$ docker run -d -e APPNAME_PASSWORD=colovu colovu/imgname
$ docker run -d -e APPNAME_USER=colovu -e APPNAME_PASSWORD=colovu123 docker.colovu.com/colovu/imgname:latest
```
使用 Docker-Compose 时,`docker-compose.yml`应包含类似如下配置:
@@ -180,12 +162,11 @@ services:
imgname:
...
environment:
- APPNAME_PASSWORD=colovu
- APPNAME_USER=colovu
- APPNAME_PASSWORD=colovu123
...
```
### 容器安全
本容器默认使用`non-root`运行应用,以加强容器的安全性。在使用`non-root`用户运行容器时,相关的资源访问会受限;应用仅能操作镜像创建时指定的路径及数据。使用`non-root`方式的容器,更适合在生产环境中使用。
@@ -197,20 +178,14 @@ services:
如果需要切换为`root`方式运行应用,可以在启动命令中增加`-u root`以指定运行的用户。
## 注意事项
- 容器中应用的启动参数不能配置为后台运行,如果应用使用后台方式运行,则容器的启动命令会在运行后自动退出,从而导致容器退出
## 更新记录
- 2021/1/1 (1.0): 初始版本
----
本文原始来源 [Endial Fang](https://github.com/colovu) @ [Github.com](https://github.com)
+45 -45
View File
@@ -1,17 +1,17 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
# Ver: 1.3 by Endial Fang (endial@126.com)
#
# 应用通用业务处理函数
# 加载依赖脚本
. /usr/local/scripts/libcommon.sh # 通用函数库
. /colovu/lib/libcommon.sh # 通用函数库
. /usr/local/scripts/libfile.sh
. /usr/local/scripts/libfs.sh
. /usr/local/scripts/liblog.sh
. /usr/local/scripts/libos.sh
. /usr/local/scripts/libservice.sh
. /usr/local/scripts/libvalidations.sh
. /colovu/lib/libfile.sh
. /colovu/lib/libfs.sh
. /colovu/lib/liblog.sh
. /colovu/lib/libos.sh
. /colovu/lib/libservice.sh
. /colovu/lib/libvalidations.sh
# 函数列表
@@ -30,12 +30,12 @@
#
# 举例:
# CORE_CONF_fs_defaultFS 对应配置文件中的配置项:fs.defaultFS
appname_configure_from_environment() {
app_configure_from_environment() {
# Map environment variables to config properties
for var in "${!APP_CFG_@}"; do
key="$(echo "$var" | sed -e 's/^APP_CFG_//g' -e 's/_/\./g' | tr '[:upper:]' '[:lower:]')"
value="${!var}"
appname_conf_set "$key" "$value"
app_conf_set "$key" "$value"
done
local path="${1:?missing file}"
@@ -69,7 +69,7 @@ appname_configure_from_environment() {
# $1 - 文件
# $2 - 变量
# $3 - 值(列表)
appname_common_conf_set() {
app_common_conf_set() {
local file="${1:?missing file}"
local key="${2:?missing key}"
shift
@@ -81,7 +81,7 @@ appname_common_conf_set() {
return 1
elif [[ "${#values[@]}" -ne 1 ]]; then
for i in "${!values[@]}"; do
appname_common_conf_set "$file" "${key[$i]}" "${values[$i]}"
app_common_conf_set "$file" "${key[$i]}" "${values[$i]}"
done
else
value="${values[0]}"
@@ -100,42 +100,42 @@ appname_common_conf_set() {
# 变量:
# $1 - 变量
# $2 - 值(列表)
appname_conf_set() {
appname_common_conf_set "${APP_CONF_DIR}/zoo.cfg" "$@"
app_conf_set() {
app_common_conf_set "${APP_CONF_DIR}/zoo.cfg" "$@"
}
# 更新 log4j.properties 配置文件中指定变量值
# 变量:
# $1 - 变量
# $2 - 值(列表)
appname_log4j_set() {
appname_common_conf_set "${APP_CONF_DIR}/log4j.properties" "$@"
app_log4j_set() {
app_common_conf_set "${APP_CONF_DIR}/log4j.properties" "$@"
}
# 使用环境变量中配置,更新配置文件
appname_update_conf() {
app_update_conf() {
LOG_I "Update configure files..."
}
# 生成默认配置文件
appname_generate_conf() {
app_generate_conf() {
# 准备原始默认配置文件或生成空文件
cp "${APP_CONF_DIR}/app_sample.cfg" "${APP_CONF_FILE}"
echo "">> "${APP_CONF_FILE}"
# 根据容器参数,设置配置文件
appname_log4j_set "zookeeper.console.threshold" "${ZOO_LOG_LEVEL}"
appname_log4j_set "zookeeper.log.dir" "${APP_LOG_DIR}"
app_log4j_set "zookeeper.console.threshold" "${ZOO_LOG_LEVEL}"
app_log4j_set "zookeeper.log.dir" "${APP_LOG_DIR}"
appname_update_conf
app_update_conf
}
# 设置环境变量 JVMFLAGS
# 参数:
# $1 - value
appname_export_jvmflags() {
app_export_jvmflags() {
local -r value="${1:?value is required}"
export JVMFLAGS="${JVMFLAGS} ${value}"
@@ -145,19 +145,19 @@ appname_export_jvmflags() {
# 配置 HEAP 大小
# 参数:
# $1 - HEAP 大小
appname_configure_heap_size() {
app_configure_heap_size() {
local -r heap_size="${1:?heap_size is required}"
if [[ "${JVMFLAGS}" =~ -Xm[xs].*-Xm[xs] ]]; then
LOG_D "Using specified values (JVMFLAGS=${JVMFLAGS})"
else
LOG_D "Setting '-Xmx${heap_size}m -Xms${heap_size}m' heap options..."
appname_export_jvmflags "-Xmx${heap_size}m -Xms${heap_size}m"
app_export_jvmflags "-Xmx${heap_size}m -Xms${heap_size}m"
fi
}
# 检测用户参数信息是否满足条件; 针对部分权限过于开放情况,打印提示信息
appname_verify_minimum_env() {
app_verify_minimum_env() {
local error_code=0
LOG_D "Validating settings in APP_* env vars..."
@@ -180,7 +180,7 @@ appname_verify_minimum_env() {
}
# 更改默认监听地址为 "*" 或 "0.0.0.0",以对容器外提供服务;默认配置文件应当为仅监听 localhost(127.0.0.1)
appname_enable_remote_connections() {
app_enable_remote_connections() {
LOG_D "Modify default config to enable all IP access"
}
@@ -188,7 +188,7 @@ appname_enable_remote_connections() {
# 检测依赖的服务端口是否就绪;该脚本依赖系统工具 'netcat'
# 参数:
# $1 - host:port
appname_wait_service() {
app_wait_service() {
local serviceport=${1:?Missing server info}
local service=${serviceport%%:*}
local port=${serviceport#*:}
@@ -227,8 +227,8 @@ appname_wait_service() {
}
# 以后台方式启动应用服务,并等待启动就绪
appname_start_server_bg() {
appname_is_server_running && return
app_start_server_bg() {
app_is_server_running && return
LOG_I "Starting ${APP_NAME} in background..."
@@ -255,8 +255,8 @@ appname_start_server_bg() {
}
# 停止应用服务
appname_stop_server() {
if appname_is_server_running ; then
app_stop_server() {
if app_is_server_running ; then
LOG_I "Stopping ${APP_NAME}..."
# 使用 PID 文件 kill 进程
@@ -274,7 +274,7 @@ appname_stop_server() {
# 检测停止是否完成
local counter=10
while [[ "$counter" -ne 0 ]] && appname_is_server_running; do
while [[ "$counter" -ne 0 ]] && app_is_server_running; do
LOG_D "Waiting for ${APP_NAME} to stop..."
sleep 1
counter=$((counter - 1))
@@ -283,7 +283,7 @@ appname_stop_server() {
}
# 检测应用服务是否在后台运行中
appname_is_server_running() {
app_is_server_running() {
LOG_D "Check if ${APP_NAME} is running..."
local pid
pid="$(get_pid_from_file "${APP_PID_FILE}")"
@@ -296,18 +296,18 @@ appname_is_server_running() {
fi
}
appname_is_server_not_running() {
! appname_is_server_running
app_is_server_not_running() {
! app_is_server_running
}
# 清理初始化应用时生成的临时文件
appname_clean_tmp_file() {
app_clean_tmp_file() {
LOG_D "Clean ${APP_NAME} tmp files for init..."
}
# 在重新启动容器时,删除标志文件及必须删除的临时文件 (容器重新启动)
appname_clean_from_restart() {
app_clean_from_restart() {
LOG_D "Clean ${APP_NAME} tmp files for restart..."
local -r -a files=(
"/var/run/${APP_NAME}/${APP_NAME}.pid"
@@ -323,8 +323,8 @@ appname_clean_from_restart() {
# 应用默认初始化操作
# 执行完毕后,生成文件 ${APP_CONF_DIR}/.app_init_flag 及 ${APP_DATA_DIR}/.data_init_flag 文件
appname_default_init() {
appname_clean_from_restart
app_default_init() {
app_clean_from_restart
LOG_D "Check init status of ${APP_NAME}..."
# 检测配置文件是否存在
@@ -339,14 +339,14 @@ appname_default_init() {
LOG_I "User injected custom configuration detected!"
LOG_D "Update configure files from environment..."
appname_update_conf
app_update_conf
fi
if [[ ! -f "${APP_DATA_DIR}/.data_init_flag" ]]; then
LOG_I "Deploying ${APP_NAME} from scratch..."
# 启动后台服务
appname_start_server_bg
app_start_server_bg
# TODO: 根据需要生成相应初始化数据
@@ -359,7 +359,7 @@ appname_default_init() {
# 用户自定义的前置初始化操作,依次执行目录 preinitdb.d 中的初始化脚本
# 执行完毕后,生成文件 ${APP_DATA_DIR}/.custom_preinit_flag
appname_custom_preinit() {
app_custom_preinit() {
LOG_I "Check custom pre-init status of ${APP_NAME}..."
# 检测用户配置文件目录是否存在 preinitdb.d 文件夹,如果存在,尝试执行目录中的初始化脚本
@@ -382,13 +382,13 @@ appname_custom_preinit() {
# 检测依赖的服务是否就绪
#for i in ${SERVICE_PRECONDITION[@]}; do
# appname_wait_service "${i}"
# app_wait_service "${i}"
#done
}
# 用户自定义的应用初始化操作,依次执行目录initdb.d中的初始化脚本
# 执行完毕后,生成文件 ${APP_DATA_DIR}/.custom_init_flag
appname_custom_init() {
app_custom_init() {
LOG_I "Check custom initdb status of ${APP_NAME}..."
# 检测用户配置文件目录是否存在 initdb.d 文件夹,如果存在,尝试执行目录中的初始化脚本
@@ -399,7 +399,7 @@ appname_custom_init() {
LOG_I "Process custom init scripts from /srv/conf/${APP_NAME}/initdb.d..."
# 启动后台服务
appname_start_server_bg
app_start_server_bg
# 检索所有可执行脚本,排序后执行
find "/srv/conf/${APP_NAME}/initdb.d/" -type f -regex ".*\.\(sh\|sql\|sql.gz\)" | sort | while read -r f; do
+13 -15
View File
@@ -1,29 +1,27 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#!/usr/bin/dumb-init /bin/bash
# Ver: 1.5 by Endial Fang (endial@126.com)
#
# 容器入口脚本
# 容器入口脚本;当前脚本执行完毕时,使用默认用户执行镜像 CMD 定义的命令(默认为'/usr/local/bin/run.sh'
# 设置 shell 执行参数,可使用'-'(打开)'+'(关闭)控制。常用:
# -e: 命令执行错误则报错(errexit); -u: 变量未定义则报错(nounset); -x: 打印实际待执行的命令行; -o pipefail: 设置管道中命令遇到失败则报错
set -eu
set -o pipefail
set -euo pipefail
. /usr/local/scripts/libcommon.sh # 加载通用函数库
. /colovu/lib/libcommon.sh # 加载通用函数库
LOG_I "** Processing entry.sh **"
if [[ "$*" = "/usr/local/bin/run.sh" ]]; then
print_image_welcome
# 检测是否仅打印帮助信息(CMD 命令第一个参数以'-'起始)
[[ "${1:0:1}" == '-' ]] && "${APP_EXEC:-${APP_NAME:-/bin/bash}}" "$@" || exit
LOG_I "** Starting ${APP_NAME} setup **"
# 判断是否为 root 用户
if [[ "$(id -u)" == '0' ]]; then
print_image_welcome
/usr/local/bin/setup.sh
/usr/local/bin/init.sh
LOG_I "** ${APP_NAME} setup finished! **"
# 使用非 root 用户执行 CMD 命令,并替换当前进程
exec gosu "${APP_USER}" "$@"
fi
# 检测是否仅打印帮助信息
[ "${1:0:1}" = '-' ] && set -- "${APP_EXEC:-/bin/bash}" "$@"
print_command_help "$@"
LOG_I "Start container with command: $@"
# 执行'$@'定义的指令,并替换当前进程
exec "$@"
+7 -16
View File
@@ -1,11 +1,11 @@
#!/bin/bash
# Ver: 1.0 by Endial Fang (endial@126.com)
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 应用环境变量定义及初始化
# 通用设置
export ENV_DEBUG=${ENV_DEBUG:-false}
export ALLOW_ANONYMOUS_LOGIN="${ALLOW_ANONYMOUS_LOGIN:-no}"
export ALLOW_ANONYMOUS="${ALLOW_ANONYMOUS:-no}"
# 通过读取变量名对应的 *_FILE 文件,获取变量值;如果对应文件存在,则通过传入参数设置的变量值会被文件中对应的值覆盖
# 变量优先级: *_FILE > 传入变量 > 默认值
@@ -22,24 +22,15 @@ done
unset app_env_file_lists
# 应用路径参数
export APP_HOME_DIR="/usr/local/${APP_NAME}"
export APP_DEF_DIR="/etc/${APP_NAME}"
export APP_CONF_DIR="/srv/conf/${APP_NAME}"
export APP_DATA_DIR="/srv/data/${APP_NAME}"
export APP_DATA_LOG_DIR="/srv/datalog/${APP_NAME}"
export APP_CACHE_DIR="/var/cache/${APP_NAME}"
export APP_RUN_DIR="/var/run/${APP_NAME}"
export APP_LOG_DIR="/var/log/${APP_NAME}"
export APP_CERT_DIR="/srv/cert/${APP_NAME}"
export APP_DEF_DIR="${APP_BASE}/etc/${APP_NAME}"
export APP_CONF_DIR="/srv/${APP_NAME}/conf"
export APP_DATA_DIR="/srv/${APP_NAME}/data"
# 应用配置参数
export APP_CONF_FILE=${APP_CONF_DIR}/default.conf
# 内部变量
export APP_PID_FILE="${APP_PID_FILE:-${APP_RUN_DIR}/${APP_NAME}.pid}"
export APP_DAEMON_USER="${APP_NAME}"
export APP_DAEMON_GROUP="${APP_NAME}"
export APP_PID_FILE="${APP_PID_FILE:-/var/run/${APP_NAME}/${APP_NAME}.pid}"
# 个性化变量
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 应用初始化脚本
# 设置 shell 执行参数,可使用'-'(打开)'+'(关闭)控制。常用:
# -e: 命令执行错误则报错; -u: 变量未定义则报错; -x: 打印实际待执行的命令行; -o pipefail: 设置管道中命令遇到失败则报错
set -eu
set -o pipefail
. /usr/local/bin/common.sh # 应用专用函数库
. /usr/local/bin/environment.sh # 设置环境变量
LOG_I "** Processing init.sh **"
trap "${APP_NAME}_stop_server" EXIT
${APP_NAME}_verify_minimum_env
# 执行应用预初始化操作
${APP_NAME}_custom_preinit
# 执行应用初始化操作
${APP_NAME}_default_init
# 执行用户自定义初始化脚本
${APP_NAME}_custom_init
LOG_I "** Processing init.sh finished! **"
+11 -14
View File
@@ -1,29 +1,26 @@
#!/bin/bash
# Ver: 1.3 by Endial Fang (endial@126.com)
# Ver: 1.5 by Endial Fang (endial@126.com)
#
# 应用启动脚本
# 组合默认的配置参数及容器启动时传入的 CMD 参数,启动应用
# 设置 shell 执行参数,可使用'-'(打开)'+'(关闭)控制。常用:
# -e: 命令执行错误则报错(errexit); -u: 变量未定义则报错(nounset); -x: 打印实际待执行的命令行; -o pipefail: 设置管道中命令遇到失败则报错
set -eu
set -o pipefail
set -euo pipefail
. /usr/local/bin/common.sh # 应用专用函数库
. /usr/local/bin/environment.sh # 设置环境变量
LOG_I "** Processing run.sh **"
# 获取命令的实际路径信
readonly START_COMMAND="$(command -v ${APP_EXEC:-${APP_NAME}})"
readonly START_COMMAND="$(command -v ${APP_EXEC})"
# 配置默认启动参数(应用配置文件、前台方式启动)
flags=("-c" "${APP_CONF_FILE:-}")
[[ -z "${APP_EXTRA_FLAGS:-}" ]] || flags+=("${APP_EXTRA_FLAGS[@]}")
# 确保应用运行在前台
flags=("-f" "${APP_CONF_FILE:-}")
[[ -z "${APP_EXTRA_FLAGS:-}" ]] || flags=("${flags[@]}" "${APP_EXTRA_FLAGS[@]}")
# 增加 "@" 以使用用户在命令行添加的扩展标识
flags=("${flags[@]}" "$@")
# 使用 "$@" 添加镜像默认 CMD 内容或自定义命令内容
flags+=("$@")
LOG_I "** Starting ${APP_NAME} **"
#is_root && flags=("-u" "$APP_DAEMON_USER" "${flags[@]}")
LOG_I "Command: ${START_COMMAND[@]} ${flags[@]}"
LOG_I "Start ${APP_NAME} with command: ${START_COMMAND[@]} ${flags[@]}"
exec "${START_COMMAND[@]}" "${flags[@]}"
+23 -9
View File
@@ -1,22 +1,25 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
# Ver: 1.3 by Endial Fang (endial@126.com)
#
# 应用环境及依赖文件设置脚本
# 设置 shell 执行参数,可使用'-'(打开)'+'(关闭)控制。常用:
# -e: 命令执行错误则报错(errexit); -u: 变量未定义则报错(nounset); -x: 打印实际待执行的命令行; -o pipefail: 设置管道中命令遇到失败则报错
set -eu
set -o pipefail
set -euo pipefail
. /usr/local/scripts/libcommon.sh # 加载通用函数库
. /usr/local/scripts/libfs.sh # 加载文件操作函数库
. /usr/local/scripts/libos.sh # 加载系统管理函数库
. /colovu/lib/libcommon.sh # 加载通用函数库
. /colovu/lib/libfs.sh # 加载文件操作函数库
. /colovu/lib/libos.sh # 加载系统管理函数库
. /usr/local/bin/environment.sh # 设置环境变量
. /usr/local/bin/common.sh # 应用专用函数库
LOG_I "** Processing setup.sh **"
APP_DIRS="${APP_CONF_DIR:-} ${APP_DATA_DIR:-} ${APP_LOG_DIR:-} ${APP_CERT_DIR:-} ${APP_DATA_LOG_DIR:-}"
trap "app_stop_server" EXIT
APP_DIRS="/var/log/${APP_NAME} /var/run/${APP_NAME} /var/cache/${APP_NAME} ${APP_HOME}"
APP_DIRS+="${APP_HOME}/conf ${APP_HOME}/data ${APP_HOME}/cert"
LOG_I "Ensure directory exists: ${APP_DIRS}"
for dir in ${APP_DIRS}; do
@@ -29,6 +32,17 @@ if [[ ! -z "$(ls -A "${APP_DEF_DIR}")" ]]; then
ensure_config_file_exist "${APP_DEF_DIR}" $(ls -A "${APP_DEF_DIR}")
fi
is_root && ensure_user_exists "$APP_DAEMON_USER" -g "$APP_DAEMON_GROUP"
# 解决使用non-root后,[emerg] open() "/dev/stdout" failed (13: Permission denied)
LOG_D "Change permissions of stdout/stderr to 0662"
chmod 0662 /dev/stdout /dev/stderr
LOG_I "** Processing setup.sh finished! **"
app_verify_minimum_env
# 执行应用预初始化操作
app_custom_preinit
# 执行应用初始化操作
app_default_init
# 执行用户自定义初始化脚本
app_custom_init
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行)
set -eux
groupadd --gid 1001 --system ${APP_USER}
#useradd --gid 1001 --uid 1001 --shell /bin/bash --home /srv/data/${APP_NAME} --system ${APP_USER}
useradd --gid 1001 --uid 1001 --shell /usr/sbin/nologin --home /srv/data/${APP_NAME} --system ${APP_USER}
# 如果需要 sudo 权限,需要在 Dockerfile 中安装 su 软件包:RUN install_pkg sudo
#sed -i -e 's/^\sDefaults\s*secure_path\s*=/# Defaults secure_path=/' /etc/sudoers
#echo "${APP_USER} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# Ver: 1.3 by Endial Fang (endial@126.com)
#
# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行)
set -eux
APP_DIRS=" \
/srv/conf/${APP_NAME} \
/srv/data/${APP_NAME} \
/srv/datalog/${APP_NAME} \
/var/cache/${APP_NAME} \
/var/run/${APP_NAME} \
/var/log/${APP_NAME} \
/srv/cert/${APP_NAME}"
mkdir -p ${APP_DIRS}
chmod -R g+rwX ${APP_DIRS} /usr/local/${APP_NAME}
+4 -4
View File
@@ -6,18 +6,18 @@ version: '3.8'
# 当前配置仅保证可以启动容器;更多配置参数请参考镜像 README.md 文档中说明
services:
app1-name:
image: 'registry.cn-shenzhen.aliyuncs.com/colovu/app-name'
image: 'registry.cn-shenzhen.aliyuncs.com/colovu/app-name:latest'
ports:
- '8001:8000'
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
- ALLOW_ANONYMOUS=yes
app2-name:
image: 'registry.cn-shenzhen.aliyuncs.com/colovu/app-name'
image: 'registry.cn-shenzhen.aliyuncs.com/colovu/app-name:latest'
ports:
- '8002:8000'
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
- ALLOW_ANONYMOUS=yes
volumes:
- 'app_conf:/srv/conf'
depends_on:
+5 -14
View File
@@ -4,20 +4,11 @@ version: '3.8'
# 当前配置仅保证可以启动容器;更多配置参数请参考镜像 README.md 文档中说明
services:
app-name:
image: 'registry.cn-shenzhen.aliyuncs.com/colovu/app-name'
image: 'registry.cn-shenzhen.aliyuncs.com/colovu/app-name:latest'
restart: always
ports:
- '8000:8000'
volumes:
- 'app_conf:/srv/conf'
- '80:8080'
network_mode: bridge
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
- ALLOW_ANONYMOUS=yes
- ENV_DEBUG=yes
# 定义本地数据卷,由系统管理,需要手动删除
volumes:
app_conf:
driver: local
app_data:
driver: local
var_log:
driver: local
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Endial Fang (endial@126.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+108
View File
@@ -0,0 +1,108 @@
#!/bin/bash
# Ver: 1.5 by Endial Fang (endial@126.com)
#
# 通用函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
# 函数列表
# 打印包含包含Logo的欢迎信息
print_welcome_info() {
[[ -n "${APP_NAME:-}" ]] && github_repo="https://github.com/colovu/docker-${APP_NAME}"
LOG_I " ____ _ "
LOG_I " / ___|___ | | _____ ___ _ "
LOG_I "| | / _ \| |/ _ \ \ / / | | | Docker : ${BOLD}${APP_NAME:-undefined}${RESET}"
LOG_I "| |__| (_) | | (_) \ V /| |_| | Version: ${BOLD}${APP_VER:-0.0}${RESET}"
LOG_I " \____\___/|_|\___/ \_/ \__,_| PowerBy: ${BOLD}Endial@126.com${RESET}"
LOG_D " Project Repo: ${github_repo:-}"
LOG_I ""
}
# 根据需要打印欢迎信息
print_image_welcome() {
if [[ ! "$(id -u)" = "0" ]]; then
print_welcome_info
fi
}
# 检测可能导致容器执行后直接退出的命令,如"--help";如果存在,直接返回 0
# 参数:
# $1 - 待检测的参数表
print_command_help() {
local arg
for arg; do
case "$arg" in
-'?'|-H|-h|--help|-help|-V|-v|--version|-version)
exec "$@"
exit
;;
esac
done
}
# 检测应用相应的配置文件是否存在,如果不存在,则从默认配置文件目录拷贝一份
# 默认配置文件路径:/etc/${APP_NAME}
# 目标配置文件路径:/srv/conf/${APP_NAME}
# 参数:
# $1 - 基础路径
# $* - 基础路径下的文件及目录列表,以" "分割
# 例子:
# ensure_config_file_exist /etc/${APP_NAME} conf.d server.conf
ensure_config_file_exist() {
local -r base_path="${1:?paths is missing}"
local f=""
local dist=""
shift 1
LOG_D "List to check: $@"
while [ "$#" -gt 0 ]; do
f="${1}"
LOG_D " Process \"${f}\""
if [ -d "${base_path}/${f}" ]; then
dist="$(echo ${base_path}/${f} | sed -e 's/\/etc/\/srv\/conf/g')"
[[ ! -d "${dist}" ]] && LOG_D " Create directory: ${dist}" && mkdir -p "${dist}"
[[ ! -z $(ls -A "${base_path}/${f}") ]] && ensure_config_file_exist "${base_path}/${f}" $(ls -A "${base_path}/${f}")
else
dist="$(echo ${base_path}/${f} | sed -e 's/\/etc/\/srv\/conf/g')"
[[ ! -e "${dist}" ]] && LOG_D " Copy: ${base_path}/${f} ===> ${dist}" && cp "${base_path}/${f}" "${dist}" && rm -rf "/srv/conf/${APP_NAME}/.app_init_flag"
fi
shift
done
}
# 根据脚本扩展名及权限,执行相应的初始化脚本
# 参数:
# $1 - 文件列表,支持路径通配符
# 使用:
# process_init_files [file [file [...]]]
# 例子:
# process_init_files /src/conf/${APP_NAME}/initdb.d/*
process_init_files() {
echo
local f
for f; do
case "$f" in
*.sh)
if [ -x "$f" ]; then
LOG_I "$0: running $f"
"$f"
else
LOG_I "$0: sourcing $f"
. "$f"
fi
;;
*) LOG_W "$0: ignoring $f" ;;
esac
echo
done
}
# 检测当前脚本是被直接执行的,还是从其他脚本中使用 "source" 调用的
is_sourced() {
[ "${#FUNCNAME[@]}" -ge 2 ] \
&& [ "${FUNCNAME[0]}" = 'is_sourced' ] \
&& [ "${FUNCNAME[1]}" = 'source' ]
}
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# 文件操作函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
# 函数列表
# 检测"*_FILE"文件,并从文件中读取信息作为参数值;环境变量不允许 VAR 与 VAR_FILE 方式并存
# 变量:
# $1 - 需要设置的环境变量名称
# $2 - 该变量对应的默认值(Option)
#
# 使用: file_env ENV_VAR [DEFAULT]
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
LOG_E "Both $var and $fileVar are set (but are exclusive)"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
# 使用规则表达式在文件中替换数据
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 替代数据表达式
# $4 - 是否使用POSIX表达式. Default: true
replace_in_file() {
local filename="${1:?filename is required}"
local match_regex="${2:?match regex is required}"
local substitute_regex="${3:?substitute regex is required}"
local posix_regex=${4:-true}
local result
del=$'\001' # Use a non-printable character as a 'sed' delimiter to avoid issues
if [[ $posix_regex = true ]]; then
result="$(sed -E "s${del}${match_regex}${del}${substitute_regex}${del}g" "$filename")"
else
result="$(sed "s${del}${match_regex}${del}${substitute_regex}${del}g" "$filename")"
fi
echo "$result" > "$filename"
}
# 使用规则表达式在文件中删除数据
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 是否使用POSIX表达式. Default: true
remove_in_file() {
local filename="${1:?filename is required}"
local match_regex="${2:?match regex is required}"
local posix_regex=${3:-true}
local result
# 因部分系统兼容性问题,需要防止使用 'sed in-place' 方式操作
if [[ $posix_regex = true ]]; then
result="$(sed -E "/$match_regex/d" "$filename")"
else
result="$(sed "/$match_regex/d" "$filename")"
fi
echo "$result" > "$filename"
}
# 在符合条件的行后增加文本
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 待增加的文本
append_in_file() {
local file="${1:?missing file}"
local match_regex="${2:?missing pattern}"
local value="${3:?missing value}"
# We read the file in reverse, replace the first match (0,/pattern/s) and then reverse the results again
result="$(tac "$file" | sed -E "0,/($match_regex)/s||${value}\n\1|" | tac)"
echo "$result" > "$file"
}
+135
View File
@@ -0,0 +1,135 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 文件管理函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
# 函数列表
# 确保指定的 文件/路径 所属权为指定的 用户/组
# 参数:
# $1 - 文件路径
# $2 - 用户
ensure_owned_by() {
local path="${1:?path is missing}"
local owner="${2:?owner is missing}"
chown "$owner":"$owner" "$path"
}
# 检测目录是否存在,如果不存在则创建,同时修改为指定的用户
# 参数:
# $1 - 目录路径
# $2 - 用户
ensure_dir_exists() {
local dir="${1:?directory is missing}"
local owner="${2:-}"
mkdir -p "${dir}"
if [[ -n $owner ]]; then
ensure_owned_by "$dir" "$owner"
fi
}
# 检测目录是否存在或为空
# 参数:
# $1 - 目录路径
is_dir_empty() {
local dir="${1:?missing directory}"
if [[ ! -e "$dir" ]] || [[ -z "$(ls -A "$dir")" ]]; then
true
else
false
fi
}
# 检测指定的路径当前用户是否可写入
# 参数:
# $1 - 文件或路径
# 返回值:
# true / false
is_writable() {
local file="${1:?missing file}"
local dir
dir="$(dirname "$file")"
if [[ ( -f "$file" && -w "$file" ) || ( ! -f "$file" && -d "$dir" && -w "$dir" ) ]]; then
true
else
false
fi
}
# 循环设置目录中子目录及文件权限
# 参数:
# $1 - paths (as a string).
# Flags:
# -f|--file-mode - 文件权限模式
# -d|--dir-mode - 目录权限模式
# -u|--user - 用户
# -g|--group - 用户组
configure_permissions_ownership() {
local -r paths="${1:?paths is missing}"
local dir_mode=""
local file_mode=""
local user=""
local group=""
# Validate arguments
shift 1
while [ "$#" -gt 0 ]; do
case "$1" in
-f|--file-mode)
shift
file_mode="${1:?missing mode for files}"
;;
-d|--dir-mode)
shift
dir_mode="${1:?missing mode for directories}"
;;
-u|--user)
shift
user="${1:?missing user}"
;;
-g|--group)
shift
group="${1:?missing group}"
;;
*)
LOG_E "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
read -r -a filepaths <<< "$paths"
for p in "${filepaths[@]}"; do
if [[ -e "$p" ]]; then
LOG_D "Check $p"
if [[ -n ${dir_mode} ]]; then
LOG_D "Change permissions to ${dir_mode} of directories in $p"
find -L "$p" -type d -exec chmod "$dir_mode" {} \;
fi
if [[ -n ${file_mode} ]]; then
LOG_D "Change permissions to ${file_mode} of files in $p"
find -L "$p" -type f -exec chmod "$file_mode" {} \;
fi
if [[ -n $user ]] && [[ -n ${group} ]]; then
LOG_D "Change ownership to ${user}:${group} of files and directories in $p"
chown -LR "$user":"$group" "$p"
elif [[ -n $user ]] && [[ -z $group ]]; then
LOG_D "Change user to ${user} of files and directories in $p"
chown -LR "$user" "$p"
elif [[ -z $user ]] && [[ -n $group ]]; then
LOG_D "Change group to ${group} of files and directories in $p"
chgrp -LR "$group" "$p"
fi
else
LOG_E "$p does not exist"
fi
done
}
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# 日志输出函数库
#[[ ${ENV_DEBUG:-false} = true ]] && set -x
MODULE="$(basename "$0")"
RESET='\033[0m'
BOLD='\033[1m'
# 前景色
BLACK='\033[38;5;0m'
RED='\033[38;5;1m'
GREEN='\033[38;5;2m'
YELLOW='\033[38;5;3m'
BLUE='\033[38;5;4m'
MAGENTA='\033[38;5;5m'
CYAN='\033[38;5;6m'
WHITE='\033[38;5;7m'
# 背景色
ON_BLACK='\033[48;5;0m'
ON_RED='\033[48;5;1m'
ON_GREEN='\033[48;5;2m'
ON_YELLOW='\033[48;5;3m'
ON_BLUE='\033[48;5;4m'
ON_MAGENTA='\033[48;5;5m'
ON_CYAN='\033[48;5;6m'
ON_WHITE='\033[48;5;7m'
# 函数列表
# 打印输出到 STDERR 设备
stderr_print() {
printf "%b\\n" "${*}" >&2
}
# 输出实际日志信息
# 参数:
# $1 - 日志信息
LOG() {
local -r bool="${ENV_DEBUG:-false}"
shopt -s nocasematch
if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then
debugInfo="${CYAN}${APP_NAME:-}:${MODULE:-}"
else
debugInfo="${CYAN}${APP_NAME:-}"
fi
stderr_print "${debugInfo} ${MAGENTA}$(date "+%F %T.%3N")${RESET} ${*}"
}
# 输出调试类日志信息,尽量少使用
# 参数:
# $1 - 日志信息
LOG_D() {
local -r bool="${ENV_DEBUG:-false}"
shopt -s nocasematch
if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then
LOG "${BLUE}DBG${RESET}: ${*}"
fi
}
# 输出提示信息类日志信息
# 参数:
# $1 - 日志信息
LOG_I() {
LOG "${GREEN}INF${RESET}: ${*}"
}
# 输出警告类日志信息至sterr
# 参数:
# $1 - 日志信息
LOG_W() {
LOG "${YELLOW}WRN${RESET}: ${*}"
}
# 输出错误类日志信息至sterr,并退出脚本
# 参数:
# $1 - 日志信息
LOG_E() {
LOG "${RED}ERR${RESET}: ${*}"
}
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 文件管理函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
# 函数列表
# 域名解析
# 参数:
# $1 - 需要解析的主机名
# $2 - IP 地址版本 v4/v6, 为空时解析所有版本
# 返回值:
# IP地址
dns_lookup() {
local host="${1:?host is missing}"
local ip_version="${2:-}"
getent "ahosts${ip_version}" "$host" | awk '/STREAM/ {print $1 }' | head -n 1
}
# 尝试解析域名并返回对应的 IP
# 参数:
# $1 - 主机名
# $2 - 尝试次数
# $3 - 重试间隔时间(秒)
# 返回值:
# IP地址
wait_for_dns_lookup() {
local hostname="${1:?hostname is missing}"
local retries="${2:-5}"
local seconds="${3:-1}"
check_host() {
if [[ $(dns_lookup "$hostname") == "" ]]; then
false
else
true
fi
}
# Wait for the host to be ready
retry_while "check_host ${hostname}" "$retries" "$seconds"
dns_lookup "$hostname"
}
# 获取当前主机 IP
# 返回值:
# IP地址
get_machine_ip() {
local -a ip_addresses
local hostname
hostname="$(hostname)"
read -r -a ip_addresses <<< "$(dns_lookup "$hostname" | xargs echo)"
if [[ "${#ip_addresses[@]}" -gt 1 ]]; then
LOG_W "Found more than one IP address associated to hostname ${hostname}: ${ip_addresses[*]}, will use ${ip_addresses[0]}"
elif [[ "${#ip_addresses[@]}" -lt 1 ]]; then
LOG_E "Could not find any IP address associated to hostname ${hostname}"
exit 1
fi
echo "${ip_addresses[0]}"
}
# 检测指定的主机名是否可解析
# 参数:
# $1 - 待检测的主机名
# 返回值:
# true / false
is_hostname_resolved() {
local -r host="${1:?missing value}"
if [[ -n "$(dns_lookup "$host")" ]]; then
true
else
false
fi
}
# 解析 URL
# 参数:
# $1 - URI 字符串
# $2 - 类型字符串. 有效值 (scheme, authority, userinfo, host, port, path, query or fragment)
# 返回值:
# 字符串
parse_uri() {
local uri="${1:?uri is missing}"
local component="${2:?component is missing}"
# Solution based on https://tools.ietf.org/html/rfc3986#appendix-B with
# additional sub-expressions to split authority into userinfo, host and port
# Credits to Patryk Obara (see https://stackoverflow.com/a/45977232/6694969)
local -r URI_REGEX='^(([^:/?#]+):)?(//((([^@/?#]+)@)?([^:/?#]+)(:([0-9]+))?))?(/([^?#]*))?(\?([^#]*))?(#(.*))?'
# || | ||| | | | | | | | | |
# |2 scheme | ||6 userinfo 7 host | 9 port | 11 rpath | 13 query | 15 fragment
# 1 scheme: | |5 userinfo@ 8 :... 10 path 12 ?... 14 #...
# | 4 authority
# 3 //...
local index=0
case "$component" in
scheme)
index=2
;;
authority)
index=4
;;
userinfo)
index=6
;;
host)
index=7
;;
port)
index=9
;;
path)
index=10
;;
query)
index=13
;;
fragment)
index=14
;;
*)
stderr_print "unrecognized component $component"
return 1
;;
esac
[[ "$uri" =~ $URI_REGEX ]] && echo "${BASH_REMATCH[${index}]}"
}
+302
View File
@@ -0,0 +1,302 @@
#!/bin/bash
# Ver: 1.3 by Endial Fang (endial@126.com)
#
# 操作系统控制函数库
# 加载依赖项
. /colovu/lib/libfs.sh # 文件系统函数库
# 函数列表
# 检测指定用户账户是否存在
# 参数:
# $1 - 用户账户
# 返回值:
# 0 / 1
is_user_exists() {
local user="${1:?user is missing}"
id "$user" >/dev/null 2>&1
}
# 检测指定用户分组是否存在
# 参数:
# $1 - 用户组
# 返回值:
# 0 / 1
is_group_exists() {
local group="${1:?group is missing}"
getent group "$group" >/dev/null 2>&1
}
# 检测当前是否为 root 用户
# 返回值:
# true / false
is_root() {
if [[ "$(id -u)" = "0" ]]; then
LOG_D "Run as root."
true
else
LOG_D "Run as non-root: $(id -u)"
false
fi
}
# 确保指定用户组在系统中存在
# 参数:
# $1 - 用户组
# 标志位:
# -s|--system - 创建系统用户 (uid <= 999)
ensure_group_exists() {
local group="${1:?group is missing}"
local is_system_user=false
# 检测标志位
shift 1
while [ "$#" -gt 0 ]; do
case "$1" in
-s|--system)
is_system_user=true
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if ! is_group_exists "$group"; then
local -a args=("$group")
$is_system_user && args+=("--system")
groupadd "${args[@]}" >/dev/null 2>&1
fi
}
# 确保指定用户在系统中存在
# 参数:
# $1 - 用户
# 标志位:
# -g|--group - 用户组
# -h|--home - 用户家目录
# -s|--system - 创建系统用户 (uid <= 999)
ensure_user_exists() {
local user="${1:?user is missing}"
local group=""
local home=""
local is_system_user=false
# Validate arguments
shift 1
while [ "$#" -gt 0 ]; do
case "$1" in
-g|--group)
shift
group="${1:?missing group}"
;;
-h|--home)
shift
home="${1:?missing home directory}"
;;
-s|--system)
is_system_user=true
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if ! is_user_exists "$user"; then
local -a user_args=("-N" "$user")
$is_system_user && user_args+=("--system")
useradd "${user_args[@]}" >/dev/null 2>&1
fi
if [[ -n "$group" ]]; then
local -a group_args=("$group")
$is_system_user && group_args+=("--system")
ensure_group_exists "${group_args[@]}"
usermod -g "$group" "$user" >/dev/null 2>&1
fi
if [[ -n "$home" ]]; then
mkdir -p "$home"
usermod -d "$home" "$user" >/dev/null 2>&1
configure_permissions_ownership "$home" -d "775" -f "664" -u "$user" -g "$group"
fi
}
# 获取系统可用内存大小(MB)信息
# 返回值:
# 内存大小(兆字节)
get_total_memory() {
echo $(($(grep MemTotal /proc/meminfo | awk '{print $2}') / 1024))
}
# 获取以内存定量方式描述的机器类型
# 标志位:
# --memory - 内存大小 (MB,可选)
# 返回值:
# 类型名称
get_machine_size() {
local memory=""
# 检测标志位
while [[ "$#" -gt 0 ]]; do
case "$1" in
--memory)
shift
memory="${1:?missing memory}"
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if [[ -z "$memory" ]]; then
debug "Memory was not specified, detecting available memory automatically"
memory="$(get_total_memory)"
fi
sanitized_memory=$(convert_to_mb "$memory")
if [[ "$sanitized_memory" -gt 26000 ]]; then
echo 2xlarge
elif [[ "$sanitized_memory" -gt 13000 ]]; then
echo xlarge
elif [[ "$sanitized_memory" -gt 6000 ]]; then
echo large
elif [[ "$sanitized_memory" -gt 3000 ]]; then
echo medium
elif [[ "$sanitized_memory" -gt 1500 ]]; then
echo small
else
echo micro
fi
}
# 获取已定义的所有内存大小描述
# 返回值:
# 描述值列表
get_supported_machine_sizes() {
echo micro small medium large xlarge 2xlarge
}
# 将以字符串表示的内存大小转换为以MB为单位的内存大小值 (i.e. 2G -> 2048)
# 参数:
# $1 - 内存大小
# 返回值:
# 转换后的数值
convert_to_mb() {
local amount="${1:-}"
if [[ $amount =~ ^([0-9]+)(M|G) ]]; then
size="${BASH_REMATCH[1]}"
unit="${BASH_REMATCH[2]}"
if [[ "$unit" = "G" ]]; then
amount="$((size * 1024))"
else
amount="$size"
fi
fi
echo "$amount"
}
# 如果禁用调试模式,将输出信息重定向至 /dev/null
# 参数:
# $@ - 待执行的命令
debug_execute() {
local bool="${ENV_DEBUG:-false}"
shopt -s nocasematch
if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then
"$@"
else
"$@" >/dev/null 2>&1
fi
}
# 重试执行命令
# 参数:
# $1 - 命令 (字符串)
# $2 - 最大尝试次数. 默认值: 12
# $3 - 重试前等待时间(秒). 默认值: 5
# 返回值:
# 0 / 1
retry_while() {
local -r cmd="${1:?cmd is missing}"
local -r retries="${2:-12}"
local -r sleep_time="${3:-5}"
local return_value=1
read -r -a command <<< "$cmd"
for ((i = 1 ; i <= retries ; i+=1 )); do
"${command[@]}" && return_value=0 && break
sleep "$sleep_time"
done
return $return_value
}
# 生成随机字符串
# 标志位:
# -t|--type - 字符串类型 (ascii, alphanumeric, numeric). 默认值: ascii
# -c|--count - 字符串长度. 默认值: 32
# 返回值:
# 字符串
generate_random_string() {
local type="ascii"
local count="32"
local filter
local result
# 检测标志位
while [[ "$#" -gt 0 ]]; do
case "$1" in
-t|--type)
shift
type="$1"
;;
-c|--count)
shift
count="$1"
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
# 检测类型
case "$type" in
ascii)
filter="[:print:]"
;;
alphanumeric)
filter="a-zA-Z0-9"
;;
numeric)
filter="0-9"
;;
*)
echo "Invalid type ${type}" >&2
return 1
esac
# Obtain count + 10 lines from /dev/urandom to ensure that the resulting string has the expected size
# Note there is a very small chance of strings starting with EOL character
# Therefore, the higher amount of lines read, this will happen less frequently
result="$(head -n "$((count + 10))" /dev/urandom | tr -dc "$filter" | head -c "$count")"
echo "$result"
}
# 为指定字符串生成 MD5 值
# 参数:
# $1 - 字符串
# 返回值:
# 字符串对应的 MD5
generate_md5_hash() {
local -r str="${1:?missing input string}"
echo -n "$str" | md5sum | awk '{print $1}'
}
+212
View File
@@ -0,0 +1,212 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# 服务管理函数库
# shellcheck disable=SC1091
# 加载依赖项
. /colovu/lib/libvalidations.sh # 数据有效性检测函数库
# 函数列表
# 获取并返回服务 PID
# 参数:
# $1 - PID 文件
# 返回值:
# PID
get_pid_from_file() {
local pid_file="${1:?pid file is missing}"
if [[ -f "$pid_file" ]]; then
if [[ -n "$(< "$pid_file")" ]] && [[ "$(< "$pid_file")" -gt 0 ]]; then
echo "$(< "$pid_file")"
fi
fi
}
# 检测 PID 对应的服务是否在运行中
# 参数:
# $1 - PID
# 返回值:
# 0 / 1
is_service_running() {
local pid="${1:?pid is missing}"
kill -0 "$pid" 2>/dev/null
}
# 通过发送信号停止一个指定 PID 的服务
# 参数:
# $1 - PID 文件
# $2 - 信号 (可选)
stop_service_using_pid() {
local pid_file="${1:?pid file is missing}"
local signal="${2:-}"
local pid
pid="$(get_pid_from_file "$pid_file")"
[[ -z "$pid" ]] || ! is_service_running "$pid" && return
if [[ -n "$signal" ]]; then
kill "-${signal}" "$pid"
else
kill "$pid"
fi
local counter=10
while [[ "$counter" -ne 0 ]] && is_service_running "$pid"; do
sleep 1
counter=$((counter - 1))
done
}
# 启动一个 cron 守护进程
# 返回值:
# true / false
cron_start() {
if [[ -x "/usr/sbin/cron" ]]; then
/usr/sbin/cron
elif [[ -x "/usr/sbin/crond" ]]; then
/usr/sbin/crond
else
false
fi
}
# 为指定的服务生成 cron 配置文件
# 参数:
# $1 - 服务名称
# $2 - 命令
# 标志位:
# --run-as - 运行的用户. 默认值: root
# --schedule - Cron 周期配置. 默认值: * * * * *
generate_cron_conf() {
local service_name="${1:?service name is missing}"
local cmd="${2:?command is missing}"
local run_as="root"
local schedule="* * * * *"
local clean="true"
local clean="true"
# 检测标志位
shift 2
while [[ "$#" -gt 0 ]]; do
case "$1" in
--run-as)
shift
run_as="$1"
;;
--schedule)
shift
schedule="$1"
;;
--no-clean)
clean="false"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
mkdir -p /etc/cron.d
if "$clean"; then
echo "${schedule} ${run_as} ${cmd}" > /etc/cron.d/"$service_name"
else
echo "${schedule} ${run_as} ${cmd}" >> /etc/cron.d/"$service_name"
fi
}
# 为指定的服务生成 monit 配置文件
# 参数:
# $1 - 服务名
# $2 - PID 文件
# $3 - 启动命令
# $4 - 停止命令
# 标志位:
# --disabled - 是否禁用. 默认值: no
generate_monit_conf() {
local service_name="${1:?service name is missing}"
local pid_file="${2:?pid file is missing}"
local start_command="${3:?start command is missing}"
local stop_command="${4:?stop command is missing}"
local monit_conf_dir="/etc/monit/conf.d"
local disabled="no"
# 检测标志位
shift 4
while [[ "$#" -gt 0 ]]; do
case "$1" in
--disabled)
shift
disabled="$1"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
is_boolean_yes "$disabled" && conf_suffix=".disabled"
mkdir -p "$monit_conf_dir"
cat >"${monit_conf_dir}/${service_name}.conf${conf_suffix:-}" <<EOF
check process ${service_name}
with pidfile "${pid_file}"
start program = "${start_command}" with timeout 90 seconds
stop program = "${stop_command}" with timeout 90 seconds
EOF
}
# 为指定的服务生成 Logrotate 配置文件
# 参数:
# $1 - 应用名称
# $2 - 日志路径
# 标志位:
# --period - 周期
# --rotations - Rotations 存储的数量
# --extra - 扩展参数 (可选)
generate_logrotate_conf() {
local service_name="${1:?service name is missing}"
local log_path="${2:?log path is missing}"
local period="weekly"
local rotations="150"
local extra=""
local logrotate_conf_dir="/etc/logrotate.d"
local var_name
# 检测标志位
shift 2
while [[ "$#" -gt 0 ]]; do
case "$1" in
--period|--rotations|--extra)
var_name="$(echo "$1" | sed -e "s/^--//" -e "s/-/_/g")"
shift
declare "$var_name"="${1:?"$var_name" is missing}"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
mkdir -p "$logrotate_conf_dir"
cat <<EOF | sed '/^\s*$/d' >"${logrotate_conf_dir}/${service_name}"
${log_path} {
${period}
rotate ${rotations}
dateext
compress
copytruncate
missingok
$(indent "$extra" 2)
}
EOF
}
+234
View File
@@ -0,0 +1,234 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# 数据有效性校验函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
# 函数列表
# 检测数据是否为整数
# 参数:
# $1 - 待检测的数据
# 返回值:
# true / false
is_int() {
local -r int="${1:?missing value}"
if [[ "$int" =~ ^-?[0-9]+ ]]; then
true
else
false
fi
}
# 检测数据是否为正整数
# 参数:
# $1 - 待检测的数据
# 返回值:
# true / false
is_positive_int() {
local -r int="${1:?missing value}"
if is_int "$int" && (( "${int}" >= 0 )); then
true
else
false
fi
}
# 检测数据是否为布尔值 '1' 或字符串 'yes/true'
# 参数:
# $1 - 待检测的数据
# 返回值:
# true / false
is_boolean_yes() {
local -r bool="${1:-}"
# comparison is performed without regard to the case of alphabetic characters
shopt -s nocasematch
if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then
true
else
false
fi
}
# 检测数据是否为字符串 'yes/no'
# 参数:
# $1 - 待检测的数据
# 返回值:
# true / false
is_yes_no_value() {
local -r bool="${1:-}"
if [[ "$bool" =~ ^(yes|no)$ ]]; then
true
else
false
fi
}
# 检测数据是否为字符串 'true/false'
# 参数:
# $1 - 待检测的数据
# 返回值:
# true / false
is_true_false_value() {
local -r bool="${1:-}"
if [[ "$bool" =~ ^(true|false)$ ]]; then
true
else
false
fi
}
# 检测提供的参数是否为空字符串或未定义
# 参数:
# $1 - 待检测的数据
# 返回值:
# true / false
is_empty_value() {
local -r val="${1:-}"
if [[ -z "$val" ]]; then
true
else
false
fi
}
# 检测数据是否为有效的端口号
# 标志位:
# -unprivileged - 没有特权的端口
# 参数:
# $1 - 待检测的数据
# 返回值:
# true / false 或 错误消息
validate_port() {
local value
local unprivileged=0
# Parse flags
while [[ "$#" -gt 0 ]]; do
case "$1" in
-unprivileged)
unprivileged=1
;;
--)
shift
break
;;
-*)
LOG_E "unrecognized flag $1"
return 1
;;
*)
break
;;
esac
shift
done
if [[ "$#" -gt 1 ]]; then
LOG_E "too many arguments provided"
return 2
elif [[ "$#" -eq 0 ]]; then
LOG_E "missing port argument"
return 1
else
value=$1
fi
if [[ -z "$value" ]]; then
LOG_E "the value is empty"
return 1
else
if ! is_int "$value"; then
LOG_W "value is not an integer"
return 2
elif [[ "$value" -lt 0 ]]; then
LOG_W "negative value provided"
return 2
elif [[ "$value" -gt 65535 ]]; then
LOG_W "requested port is greater than 65535"
return 2
elif [[ "$unprivileged" = 1 && "$value" -lt 1024 ]]; then
LOG_W "privileged port requested"
return 3
fi
fi
}
# 检测数据是否为有效的IPv4地址
# 参数:
# $1 - 待检测的数据
# 返回值:
# 0 / 1
validate_ipv4() {
local ip="${1:?ip is missing}"
local stat=1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
read -r -a ip_array <<< "$(tr '.' ' ' <<< "$ip")"
[[ ${ip_array[0]} -le 255 && ${ip_array[1]} -le 255 \
&& ${ip_array[2]} -le 255 && ${ip_array[3]} -le 255 ]]
stat=$?
fi
return $stat
}
# 校验字符串格式
# 标志位:
# -min-length - 最小长度
# -max-length - 最大长度
# 参数:
# $1 - 待检测的数据
# 返回值:
# 0 / 1
validate_string() {
local string
local min_length=-1
local max_length=-1
# Parse flags
while [ "$#" -gt 0 ]; do
case "$1" in
-min-length)
shift
min_length=${1:-}
;;
-max-length)
shift
max_length=${1:-}
;;
--)
shift
break
;;
-*)
LOG_E "unrecognized flag $1"
return 1
;;
*)
break
;;
esac
shift
done
if [ "$#" -gt 1 ]; then
LOG_E "too many arguments provided"
return 2
elif [ "$#" -eq 0 ]; then
LOG_W "missing string"
return 1
else
string=$1
fi
if [[ "$min_length" -ge 0 ]] && [[ "${#string}" -lt "$min_length" ]]; then
LOG_I "string length is less than $min_length"
return 1
fi
if [[ "$max_length" -ge 0 ]] && [[ "${#string}" -gt "$max_length" ]]; then
LOG_I "string length is great than $max_length"
return 1
fi
}
+13
View File
@@ -0,0 +1,13 @@
Types: deb
# http://snapshot.debian.org/archive/debian/20230703T000000Z
URIs: http://mirrors.aliyun.com/debian
Suites: bookworm bookworm-updates
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
# http://snapshot.debian.org/archive/debian-security/20230703T000000Z
URIs: http://mirrors.aliyun.com/debian-security
Suites: bookworm-security
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
+13
View File
@@ -0,0 +1,13 @@
Types: deb
# http://snapshot.debian.org/archive/debian/20230703T000000Z
URIs: http://deb.debian.org/debian
Suites: bookworm bookworm-updates
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
# http://snapshot.debian.org/archive/debian-security/20230703T000000Z
URIs: http://deb.debian.org/debian-security
Suites: bookworm-security
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
+13
View File
@@ -0,0 +1,13 @@
Types: deb
# http://snapshot.debian.org/archive/debian/20230703T000000Z
URIs: http://mirrors.ustc.edu.cn/debian
Suites: bookworm bookworm-updates
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
# http://snapshot.debian.org/archive/debian-security/20230703T000000Z
URIs: http://mirrors.ustc.edu.cn/debian-security
Suites: bookworm-security
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
+163
View File
@@ -0,0 +1,163 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行)
set -eux
print_usage() {
echo "Usage: download_pkg <COMMAND> <PACKAGE-NAME> \"<URLS>\" [OPTIONS]"
echo ""
echo "Download and install Third-Part packages"
echo ""
echo "Commands:"
echo " download Download a package."
echo " install Download and install a package."
echo " unpack Download and unpack a package."
echo ""
echo "Options:"
echo " -g, --checkpgp Package release bucket."
echo " -s, --checksum SHA256 verification checksum."
echo " -h, --help Show this help message and exit."
echo ""
echo "PACKAGE-NAME: Name with extern name"
echo "URLS: String with URL list"
echo ""
echo "Examples:"
echo " - Unpack package"
echo " \$ download_pkg unpack redis-5.0.8.tar.gz \"http://download.redis.io/releases\""
echo ""
echo " - Verify and Install package"
echo " \$ download_pkg install redis-5.0.8.tar.gz \"http://download.redis.io/releases\" -s 42cf86a114d2a451b898fcda96acd4d01062a7dbaaad2801d9164a36f898f596"
echo ""
}
check_pgp() {
local name_asc=${1:?missing asc file name}
local name=${2:?missing file name}
local keys="${3:?missing key id}"
GNUPGHOME="$(mktemp -d)"
if which gpg >/dev/null 2>&1; then
for key in $keys; do
gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "${key}" ||
gpg --batch --keyserver pgp.mit.edu --recv-keys "${key}" ||
gpg --batch --keyserver keys.gnupg.net --recv-keys "${key}" ||
gpg --batch --keyserver keyserver.pgp.com --recv-keys "${key}";
done
gpg --batch --verify "$name_asc" "$name"
command -v gpgconf > /dev/null && gpgconf --kill all
fi
}
# 获取并解析参数
ARGS=$(getopt -o g:s:h -l "checkpgp:,checksum:,help" -n "download-pkg" -- "$@")
if [ $? -ne 0 ];
then
exit 1
fi
eval set -- "$ARGS";
while true; do
case "$1" in
-g|--checkpgp)
shift
if [ -n "$1" ]; then
PACKAGE_KEYS=$1
shift
fi
;;
-s|--checksum)
shift
if [ -n "$1" ]; then
PACKAGE_SHA256=$1
shift
fi
;;
-h|--help)
print_usage
exit 0
;;
--)
shift
break
;;
esac
done
# 检测输入的命令是否合法
case "$1" in
download|install|unpack) ;;
*)
error "Unrecognized command: $1"
print_usage
exit 1
;;
esac
# 检测输入参数是否足够,需要至少提供软件包名称 及 下载路径
if [ $# -lt 3 ]; then
print_usage
exit 1
fi
INSTALL_ROOT=/usr/local
CACHE_ROOT=/tmp
PACKAGE="$2"
PACKAGE_URLS=$3
cd $INSTALL_ROOT
echo "Downloading $PACKAGE package"
for url in $PACKAGE_URLS; do
echo "Try $url/$PACKAGE"
if wget -O "$CACHE_ROOT/$PACKAGE" "$url/$PACKAGE" && [ -s "$CACHE_ROOT/$PACKAGE" ]; then
if [ -n "${PACKAGE_KEYS:-}" ]; then
wget -O "$CACHE_ROOT/$PACKAGE.asc" "$url/$PACKAGE.asc" || wget -O "$CACHE_ROOT/$PACKAGE.asc" "$url/$PACKAGE.sign" || :
if [ ! -e "$CACHE_ROOT/$PACKAGE.asc" ]; then
exit 1
fi
fi
break
fi
done
if [ -n "${PACKAGE_SHA256:-}" ]; then
echo "Verifying package integrity"
echo "$PACKAGE_SHA256 *$CACHE_ROOT/$PACKAGE" | sha256sum -c -
fi
if [ -e "$CACHE_ROOT/$PACKAGE.asc" ]; then
echo "Verifying package with PGP"
check_pgp "$CACHE_ROOT/$PACKAGE.asc" "$CACHE_ROOT/$PACKAGE" "$PACKAGE_KEYS"
fi
# If the tarball has too many files, it can trigger a bug
# in overlayfs when using tar. Install bsdtar in the container image
# to workaround it. As the overhead is too big (~40 MB), it is not added by
# default. Source: https://github.com/coreos/bugs/issues/1095
# 安装或解压软件
case "$1" in
download)
echo "Download success: $CACHE_ROOT/$PACKAGE"
;;
install)
echo "Installing $PACKAGE"
cp $CACHE_ROOT/$PACKAGE /usr/local/sbin/
;;
unpack)
if ! tar -taf $CACHE_ROOT/$PACKAGE >/dev/null 2>&1; then
echo "Invalid or corrupt '$PACKAGE' package."
exit 1
fi
echo "Unpacking $PACKAGE to $CACHE_ROOT"
cd $CACHE_ROOT
if which bsdtar >/dev/null 2>&1; then
bsdtar -xf $CACHE_ROOT/$PACKAGE
else
tar --no-same-owner -xaf $CACHE_ROOT/$PACKAGE
fi
;;
esac
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# Ver: 1.0 by Endial Fang (endial@126.com)
#
# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行)
set -eux
print_usage() {
echo "Usage: install_pkg <PACKAGE-NAME>"
echo ""
echo "Download and install packages"
echo ""
echo "Options:"
echo " -h, --help Show this help message and exit."
echo ""
echo "Examples:"
echo " - Install bash & curl"
echo " \$ install_pkg bash curl"
echo ""
}
if [ $# -lt 1 ]; then
print_usage
exit 1
fi
case "$1" in
-h|--help)
print_usage
exit 0
;;
esac
retry=0
max=2
until [ $retry -gt $max ]; do
set +e
(
export DEBIAN_FRONTEND=noninteractive &&
apt-get update &&
apt-get upgrade -y &&
apt-get install -y --no-install-recommends "$@"
)
CODE=$?
set -e
if [ $CODE -eq 0 ]; then
break
fi
if [ $retry -eq $max ]; then
exit $CODE
fi
echo "apt failed, retrying"
retry=$(($retry + 1))
done
apt-get purge -y --auto-remove
apt-get autoclean -y
rm -rf /var/lib/apt/lists /var/cache/apt/archives || :
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行)
set -eux
cp /etc/apt/sources/${1:-default}.sources /etc/apt/sources.list.d/debian.sources