From 78e824af517a9b8ec5bc873660ad455061435300 Mon Sep 17 00:00:00 2001 From: Kaiser-Yang <624626089@qq.com> Date: Wed, 9 Oct 2024 22:35:46 +0800 Subject: [PATCH] Finish the document for deployment We finish the document for deployment, and we add two options to configure the static resources. This solved #58. And this solved #62 partly. --- README-zh.md | 166 +++++++++++++++++- config_debug.json | 6 +- config_default.json | 6 +- config_user.json | 1 - deploy_ubuntu.sh | 2 +- script/deploy_helper.py | 32 +++- .../java/edu/cmipt/gcs/config/WebConfig.java | 14 +- src/main/resources/application.yml | 7 + .../edu/cmipt/gcs/config/WebConfigTest.java | 19 +- 9 files changed, 222 insertions(+), 31 deletions(-) delete mode 100644 config_user.json diff --git a/README-zh.md b/README-zh.md index 375af27..0240a57 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,7 +1,65 @@ # gcs-back-end `git` 中央仓库服务的后端实现。 -# 部署设置说明 +# 部署介绍 +## 使用 `deploy_ubuntu.sh` 脚本进行部署 +如果使用 `deploy_ubuntu.sh` 脚本进行自动部署,那么你只需要将仓库克隆到一个 `ubuntu` 的主机上,然后 +使用 `root` 用户执行脚本即可实现部署。在部署的过程中会使用 `apt` 安装以下的包,你需要保证 `apt` 的 +源能够获取到这些包: +* `postgresql` +* `postgresql-client` +* `openjdk-17-jdk-headless` +* `maven` +* `sudo` +* `git` +* `openssh-server` +* `systemd` (可选) + +其中 `systemd` 是可选项,是否需要取决于 `serviceType` 的值是否是 `"systemd"`,当值为 `"systemd"` 的 +时候需要保证 `systemd` 可以被 `apt` 安装。 + +自动部署是通过 `bash deploy_ubuntu.sh [config_file]` 来进行部署,当不指定 `config_file` 选项时将会 +使用 `config.json` 作为默认值,所以推荐将配置直接写入到 `config.json` 中,然后通过 +`bash deploy_ubuntu.sh` 进行部署。 + +在进行配置的时候,如果用户没有指定配置项,那么将会使用 `config_default.json` 中的值作为默认值,所以 +请不要修改 `config_default.json` 文件中的内容。 + +通常情况下,用户配置的时候需要特别注意以下的配置选项: +* `gitUserpassword`:`git` 用户的密码,`git` 用户用于执行与 `git` 命令相关的操作的以及用户通过 `ssh` +协议获取仓库时将会使用 `git` 用户进行登录。 +* `gitServerDomain`:`git` 服务器的域名,生成 `ssh` 链接时将会使用该域名。 +* `gitServerPort`:`git` 服务器的端口,生成 `ssh` 链接时将会使用该端口。 +* `deployWithDocker`:是否使用 `Docker` 进行部署,如果为 `true`,那么将会通过 +`3rdparty/docker-script` 中的脚本自动创建 `docker` 并将程序部署在 `docker` 中。 +* `dockerImage`:`Docker` 镜像,如果 `deployWithDocker` 为 `true`,那么将会使用该镜像。目前支持 +`ubuntu`。 +* `dockerPortMapping`:`Docker` 端口映射,如果 `deployWithDocker` 为 `true`,那么将会使用该端口映射。 +* `serviceType`:部署的服务类型,如果 `deployWithDocker` 为真,那么该值只有为 `sys-init-v` 时脚本才 +能正确执行,当然如果直接在 `docker` 内部执行自动部署脚本,那么该值可以为 `systemd` (`docker` 必须 +使用 `--privileged` 选项进行创建)。 +* `serviceUserPassword`:服务的用户密码,部署的服务 (`Java` 程序) 会以一个新的用户身份执行,该用户 +的密码将会被设置为该值。 +* `postgresUserPassword`:操作系统中的 `postgres` 用户的密码,`postgres` 用户会在安装 `postgresql` +的时候自动创建,部署脚本会将 `postgres` 用户的密码更改为该值。 +* `postgresqlUserPassword`:`Postgres` 数据库用户的密码,部署脚本默认会创建一个名为 `gcs` 数据库 +用户,该用户的密码将会被设置为该值。 +* `postgresqlHost`:`Postgres` 数据库的主机地址,部署脚本会使用该地址进行数据库的连接。 +* `postgresqlPort`:`Postgres` 数据库的端口,部署脚本会使用该端口进行数据库的连接。 +* `druidLoginPassword`:`Druid` 登录密码。 +* `md5Salt`:`MD5` 加密盐值,用于对用户密码进行加密。当完成部署后请不要修改此值,否则用户密码将无法 +正确验证。推荐使用随机数生成的 `sha1` 等 `hash` 值。 +* `frontEndUrl`: 前端地址,用于进行跨域配置。 +* `staticPathPattern`:静态资源的匹配模式。 +* `staticLocations`:静态资源的路径,需要使用绝对路径。 + +**注意**:需要注意的是,所有的后端接口均是以 `gcs` 开头,所以在静态资源路径下面不应该有名为 `gcs` +的文件或者文件夹 + +**注意**:如果将前端直接部署在同一个域上面,那么可以设置 `frontEndUrl` 为空字符串,然后配置 +`staticPathPattern` 和 `staticLocations` 为前端的静态资源路径。 + +下面列出了完整的配置选项: | 变量 | 类型 | 默认值 | 说明 | | - | - | - | - | | `deploy` | `bool` | `true` | 是否进行部署,当为 `false` 只进行打包操作。 | @@ -49,7 +107,111 @@ | `postgresqlPort` | `int` | `5432` | `Postgres` 端口。 | | `druidLoginUsername` | `string` | `"druid"` | `Druid` 登录用户名。 | | `druidLoginPassword` | `string` | `"druid"` | `Druid` 登录密码。 | -| `frontEndUrl` | `string` | `"http://localhost:3000"` | 前端地址。 | | `deleteGitUser` | `bool` | `true` | 清理时是否删除 `git` 用户。 | | `deleteServiceUser` | `bool` | `true` | 清理时是否删除 `service` 用户。 | | `md5Salt` | `string` | `""` | `MD5` 加密盐值。 | +| `frontEndUrl` | `string` | `"http://localhost:3000"` | 前端地址。 | +| `staticPathPattern` | `string` | `null` | 静态资源的匹配模式。 | +| `staticLocations` | `list` | `null` | 静态资源的路径,使用绝对路径,例如 `['/home/gcs/static']`。 | + +## 手动部署 +手动部署可以在任意的 `UNIX-like` 系统上面进行,下面依次介绍你需要手动完成的操作。 + +### 前置环境安装 +以下的软件包你必须进行安装: +* `postgresql` +* `postgresql-client` +* `openjdk-17-jdk-headless` +* `maven` +* `sudo` +* `git` +* `openssh-server` + +### 初始化数据库 +当你完成了数据库的配置后 (通常包括创建一个新的数据库和用户以及相关的授权工作),你可以直接执行 +`bash database/database_deploy.sh ` 来完成 +数据库的自动初始化 (请确保工作目录为仓库的根目录)。 + +### `git` 用户创建 +你需要创建一个用户用于执行仓库创建、克隆等操作,通常取名为 `git`,并且在其家目录下创建 `.ssh` 文件夹 +并将权限修改为 `700`。 + +### 修改 `sudo` 配置 +你需要保证你运行 `Java` 程序的用户能够在执行 `sudo -u rm`,`sudo -u tee` 以及 +`sudo -u git` 时不需要输入密码,其中 `` 为 [git 用户创建](#git-用户创建) 时 +创建的用户名。 + +通常你需要追加 ` ALL=() NOPASSWD:/usr/bin/git, /usr/bin/tee, /usr/bin/rm` 到 +`/etc/sudoers` 文件中,其中 `` 为你运行 `Java` 程序的用户。 + +### 配置 `application.yml` 或者 `application.properties` +你需要完成以下配置,这里以 `application.properties` 为例: + +```properties +# 数据库用户名 +spring.datasource.druid.username= +# 数据库用户密码 +spring.datasource.druid.password= +# 数据库的主机地址 +spring.datasource.druid.url= +# druid 的登录用户名 +spring.datasource.druid.stat-view-servlet.login-username= +# druid 的登录密码 +spring.datasource.druid.stat-view-servlet.login-password= +# 启动的配置文件,通常设置为 prod +spring.profiles.active= +# git 服务器的域名,通常为部署机器的公网 IP +git.server.domain= +# git 服务器的端口,设置为 ssh 连接时使用的端口 +git.server.port= +# `git` 用户创建部分创建的用户名称 +git.user.name= +# `git` 用户创建部分创建的家目录 +git.home.directory= +# 仓库保存位置,需要确保 git.user.name 用户拥有 rwx 的权限 +git.repository.directory= +# 仓库后缀,通常设置为 .git +git.repository.suffix= +# md5 的盐值,用于对用户密码进行加密 +md5.salt= +# 前端地址,用于进行跨域配置 +front-end.url= +# 静态资源的映射规则,/** 表示所有的静态资源都会被映射 +spring.mvc.static-path-pattern= +# 静态资源的路径,需要使用绝对路径,例如 file:/static +spring.resources.static-locations= +``` + +**注意**:需要注意的是,所有的后端接口均是以 `gcs` 开头,所以在静态资源路径下面不应该有名为 `gcs` +的文件或者文件夹 + +**注意**:如果将前端直接部署在同一个域上面,那么可以设置 `front-end.url` 为空字符串,然后配置 +`spring.mvc.static-path-pattern` 和 `spring.resources.static-locations` 为前端的静态资源路径。 + +### 选择喜欢的方式进行部署 +完成了上述操作后,此时已经可以通过 `mvn spring-boot:run` 直接启动了。之后你只需要选择自己喜欢的方式 +进行部署即可,例如可以通过 `mvn package` 将项目打包成一个 `jar` 包然后部署成一个服务。 + +## 自动清理功能 +等你使用 `deploy_ubuntu.sh` 进行自动部署时,你可以使用配套的 `clean_ubuntu.sh` 进行清理工作。 + +清理脚本的使用与部署脚本类似,你也需要指定配置文件位置,如果不指定则使用 `config.json` 作为默认值。 +请确保清理脚本指定的配置文件和部署脚本指定的配置文件内容是一致的。 + +清理脚本的主要工作逻辑如下: + +```bash +if deployWithDocker then + stop docker +else + stop service + delete service files except for the log file + delete /etc/sudoers.d/{serviceUser} + if deleteGitUser then + delete git user but keep the home directory + fi + if deleteServiceUser then + delete service user but keep the home directory + fi +fi +``` diff --git a/config_debug.json b/config_debug.json index f0f4525..23bf88e 100644 --- a/config_debug.json +++ b/config_debug.json @@ -5,11 +5,13 @@ "profiles": [ "dev" ], - "serviceType": "systemd", "postgresqlUserName": "gcs_debug", "postgresqlUserPassword": "gcs_debug", "postgresqlDatabaseName": "gcs_debug", "deleteGitUser": false, "deleteServiceUser": false, - "md5Salt": "Is that the best you can do?" + "frontEndUrl": null, + "md5Salt": "Is that the best you can do?", + "staticPathPattern": "/**", + "staticLocations": ["/home/kaiser/test1", "/home/kaiser/test2"] } diff --git a/config_default.json b/config_default.json index c9db495..c968ac3 100644 --- a/config_default.json +++ b/config_default.json @@ -54,8 +54,10 @@ "postgresqlPort": 5432, "druidLoginUsername": "druid", "druidLoginPassword": "druid", - "frontEndUrl": "http://localhost:3000", "deleteGitUser": true, "deleteServiceUser": true, - "md5Salt": "" + "md5Salt": "", + "frontEndUrl": "http://localhost:3000", + "staticPathPattern": null, + "staticLocations": null } diff --git a/config_user.json b/config_user.json deleted file mode 100644 index 0967ef4..0000000 --- a/config_user.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/deploy_ubuntu.sh b/deploy_ubuntu.sh index fed03a3..25e1338 100644 --- a/deploy_ubuntu.sh +++ b/deploy_ubuntu.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -config_file=${1:-"config_user.json"} +config_file=${1:-"config.json"} log_error () { echo -e "\e[31m[ERROR]: $1\e[0m" diff --git a/script/deploy_helper.py b/script/deploy_helper.py index e37966f..fa869e4 100644 --- a/script/deploy_helper.py +++ b/script/deploy_helper.py @@ -13,9 +13,8 @@ import logging import inspect -essential_packages = ['python-is-python3', 'postgresql postgresql-client', - 'openjdk-17-jdk-headless', 'maven', 'systemd', 'sudo', 'git', - 'openssh-server'] +essential_packages = ['postgresql postgresql-client', 'openjdk-17-jdk-headless', 'maven', + 'systemd', 'sudo', 'git', 'openssh-server'] sudo_cmd = os.popen('command -v sudo').read().strip() apt_updated = False message_tmp = '''\ @@ -312,12 +311,20 @@ def init_database(config): config_datasource(config) -def create_or_update_user(username, password): +def create_or_update_user(username, password, homeDirectory = None): if username == None or username == "": return if os.system(f"cat /etc/passwd | grep -w -E '^{username}'") != 0: # use -m to create the home directory for user - command = f'{sudo_cmd} useradd -m {username}' + command = f'{sudo_cmd} useradd -m ' + if homeDirectory is not None: + command += f'-d {homeDirectory} ' + command += username + res = os.system(command) + message = message_tmp.format(command, res) + command_checker(res, message) + elif homeDirectory is not None: # update the home directory + command = f'{sudo_cmd} usermod -d {homeDirectory} {username}' res = os.system(command) message = message_tmp.format(command, res) command_checker(res, message) @@ -346,7 +353,18 @@ def write_other_config(config): "gitRepositoryDirectory": "git.repository.directory", "gitRepositorySuffix": "git.repository.suffix", "md5Salt": "md5.salt", + "staticPathPattern": "spring.mvc.static-path-pattern", + "staticLocations": "spring.web.resources.static-locations", } + if config.frontEndUrl is None: + config.frontEndUrl = "" + if config.staticPathPattern is None: + config.staticPathPattern = "" + if config.staticLocations is None: + config.staticLocations = "" + else: + config.staticLocations = ['file:' + location for location in config.staticLocations] + config.staticLocations = parse_iterable_into_str(config.staticLocations, ',') try: with open(application_config_file_path, 'a') as f: for key, value in other_config_map.items(): @@ -362,7 +380,7 @@ def deploy_on_ubuntu(config): essential_packages.remove('systemd') apt_install_package(parse_iterable_into_str(essential_packages)) init_database(config) - create_or_update_user(config.gitUserName, config.gitUserPassword) + create_or_update_user(config.gitUserName, config.gitUserPassword, config.gitHomeDirectory) if not os.path.exists(f'{config.gitHomeDirectory}/.ssh'): os.system(f"{sudo_cmd} -u {config.gitUserName} mkdir -p {config.gitHomeDirectory}/.ssh") os.system(f"{sudo_cmd} -u {config.gitUserName} chmod 700 {config.gitHomeDirectory}/.ssh") @@ -418,8 +436,6 @@ def clean(config): if config.deployWithDocker: res = os.system(f"docker stop {config.dockerName}") command_checker(res, f"Failed to stop {config.dockerName}") - res = os.system(f"docker rm {config.dockerName}") - command_checker(res, f"Failed to remove {config.dockerName}") return if config.serviceType == 'systemd': command = f'{sudo_cmd} systemctl disable {config.serviceName}' diff --git a/src/main/java/edu/cmipt/gcs/config/WebConfig.java b/src/main/java/edu/cmipt/gcs/config/WebConfig.java index 8de4365..1154edb 100644 --- a/src/main/java/edu/cmipt/gcs/config/WebConfig.java +++ b/src/main/java/edu/cmipt/gcs/config/WebConfig.java @@ -16,7 +16,7 @@ @Configuration public class WebConfig { - @Value("${front-end.url:http://localhost:3000}") + @Value("${front-end.url}") private String frontEndUrl; @Bean @@ -38,11 +38,13 @@ FilterRegistrationBean corsFilterDev() { @Profile({ApplicationConstant.PROD_PROFILE, ApplicationConstant.TEST_PROFILE}) FilterRegistrationBean corsFilterNonDev() { CorsConfiguration config = new CorsConfiguration(); - config.addAllowedOrigin(frontEndUrl); - config.addAllowedMethod(HttpMethod.GET); - config.addAllowedMethod(HttpMethod.POST); - config.addAllowedMethod(HttpMethod.DELETE); - config.addAllowedHeader("*"); + if (frontEndUrl != null && frontEndUrl.length() > 0) { + config.addAllowedOrigin(frontEndUrl); + config.addAllowedMethod(HttpMethod.GET); + config.addAllowedMethod(HttpMethod.POST); + config.addAllowedMethod(HttpMethod.DELETE); + config.addAllowedHeader("*"); + } UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(ApiPathConstant.ALL_API_PREFIX + "/**", config); FilterRegistrationBean bean = diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4a1d423..89aa41b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -52,3 +52,10 @@ mybatis-plus: logging: include-application-name: false + +# force to add encoding UTF-8 to response header +server: + servlet: + encoding: + charset: UTF-8 + force: true diff --git a/src/test/java/edu/cmipt/gcs/config/WebConfigTest.java b/src/test/java/edu/cmipt/gcs/config/WebConfigTest.java index 7bd8ce9..01754ed 100644 --- a/src/test/java/edu/cmipt/gcs/config/WebConfigTest.java +++ b/src/test/java/edu/cmipt/gcs/config/WebConfigTest.java @@ -19,24 +19,25 @@ @AutoConfigureMockMvc @ActiveProfiles({ApplicationConstant.TEST_PROFILE}) public class WebConfigTest { - @Value("${front-end.url:http://localhost:3000}") + @Value("${front-end.url}") private String frontEndUrl; @Autowired private MockMvc mockMvc; @Test public void testCorsFilter() throws Exception { - mockMvc.perform( - options(ApiPathConstant.DEVELOPMENT_GET_API_MAP_API_PATH) - .header("Origin", frontEndUrl) - .header("Access-Control-Request-Method", "GET")) - .andExpectAll( - status().isOk(), - header().string("Access-Control-Allow-Origin", frontEndUrl)); - mockMvc.perform( options(ApiPathConstant.DEVELOPMENT_GET_API_MAP_API_PATH) .header("Origin", frontEndUrl + "/INVALID")) .andExpectAll(status().isForbidden()); + if (frontEndUrl != null && frontEndUrl.length() > 0) { + mockMvc.perform( + options(ApiPathConstant.DEVELOPMENT_GET_API_MAP_API_PATH) + .header("Origin", frontEndUrl) + .header("Access-Control-Request-Method", "GET")) + .andExpectAll( + status().isOk(), + header().string("Access-Control-Allow-Origin", frontEndUrl)); + } } }