feat(ci): add GitLab CE + Jenkins LTS one-click generators
Add 'ci' category with two service generators (gitlab, jenkins), following project conventions (quoted heredocs, PKG_INSTALL, bash_header, warnings + post_steps). Jenkins WAR URLs pinned in a class dict to avoid 404s. app.py + generators/__init__.py updated to register.
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
CI/CD platform generators (GitLab CE, Jenkins LTS).
|
||||
|
||||
Both install a self-contained service via systemd. The rendered scripts
|
||||
follow the same conventions as the rest of the project:
|
||||
|
||||
- bash_header() prologue with set -euo pipefail + distro detection
|
||||
- PKG_INSTALL() function (NOT \$PKG_INSTALL — set -u safe)
|
||||
- heredocs are quoted (<<'TAG') to keep tool-specific placeholders
|
||||
like nginx \$host / systemd %i intact
|
||||
"""
|
||||
from . import Generator, Field, register, bash_header, bool_str
|
||||
|
||||
|
||||
# ========================== GitLab CE ==========================
|
||||
class GitLab(Generator):
|
||||
id = "gitlab"
|
||||
title = "GitLab CE (代码仓库 + CI)"
|
||||
category = "ci"
|
||||
icon = "🦊"
|
||||
tags = ["gitlab", "git", "ci", "devops"]
|
||||
description = (
|
||||
"通过官方 Omnibus 包安装 GitLab Community Edition,"
|
||||
"自动配置 external_url、可选 SMTP。"
|
||||
)
|
||||
warnings = [
|
||||
"GitLab 占用资源较大,推荐至少 4GB RAM / 4 CPU。",
|
||||
"生产环境务必配置 SMTP,否则用户收不到邮件通知。",
|
||||
]
|
||||
post_steps = [
|
||||
"浏览器访问 external_url,首次用 root 账号登录并立即改密码。",
|
||||
"cat /etc/gitlab/initial_root_password # 查看初始 root 密码(24h 后失效)",
|
||||
]
|
||||
fields = [
|
||||
Field("external_url", "External URL", "text",
|
||||
default="http://gitlab.example.com",
|
||||
placeholder="http://gitlab.your-domain.com",
|
||||
required=True,
|
||||
help="用户访问 GitLab 的地址;必须是 http(s):// 开头,"
|
||||
"会自动写入 nginx vhost。"),
|
||||
Field("version", "版本", "text", default="latest",
|
||||
placeholder="latest 或 16.11.0-ce.0",
|
||||
help="留空/填 latest 装最新版;"
|
||||
"固定版本号形如 16.11.0-ce.0。"),
|
||||
Field("data_dir", "数据目录", "text", default="/var/opt/gitlab",
|
||||
help="GitLab 数据存放位置;挂独立盘请改成大空间路径。"),
|
||||
Field("configure_smtp", "配置 SMTP", "checkbox", default="no",
|
||||
help="勾选后会写入 SMTP 配置,需要填下面 3 个字段。"
|
||||
"不勾选则保留 GitLab 默认(无邮件功能)。"),
|
||||
Field("smtp_address", "SMTP 地址", "text",
|
||||
default="smtp.gmail.com:587",
|
||||
placeholder="smtp.example.com:587",
|
||||
help="仅 configure_smtp=yes 时生效。"),
|
||||
Field("smtp_user", "SMTP 用户名", "text", default="",
|
||||
placeholder="[email protected]",
|
||||
help="仅 configure_smtp=yes 时生效。"),
|
||||
Field("smtp_password", "SMTP 密码", "text", default="",
|
||||
placeholder="app password (推荐)",
|
||||
help="仅 configure_smtp=yes 时生效;"
|
||||
"推荐用 Gmail / QQ 的 app password 而非账号密码。"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ext_url = p.get("external_url", "http://gitlab.example.com").strip()
|
||||
if not ext_url.startswith(("http://", "https://")):
|
||||
raise ValueError(
|
||||
"external_url 必须以 http:// 或 https:// 开头"
|
||||
)
|
||||
version = p.get("version", "latest").strip() or "latest"
|
||||
data_dir = p.get("data_dir", "/var/opt/gitlab").strip()
|
||||
smtp_on = bool_str(p.get("configure_smtp"))
|
||||
smtp_addr = p.get("smtp_address", "smtp.gmail.com:587").strip()
|
||||
smtp_user = p.get("smtp_user", "").strip()
|
||||
smtp_pass = p.get("smtp_password", "").strip()
|
||||
if smtp_on and (not smtp_user or not smtp_pass):
|
||||
raise ValueError(
|
||||
"configure_smtp=yes 时必须填写 smtp_user 和 smtp_password"
|
||||
)
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing GitLab CE..."')
|
||||
out.append('# GitLab Omnibus 自带 postfix / nginx / openssh 等依赖,'
|
||||
'脚本不需单独装。')
|
||||
out.append('PKG_INSTALL curl openssl ca-certificates perl')
|
||||
out.append('')
|
||||
# Download GitLab's official apt/yum repo setup script.
|
||||
# The script picks the right package manager based on detected distro.
|
||||
# Using 'curl | bash' is GitLab's officially documented install path.
|
||||
out.append('log "Adding GitLab apt/yum repository..."')
|
||||
if version == "latest":
|
||||
pkg_args = ""
|
||||
else:
|
||||
pkg_args = '?version=' + version
|
||||
out.append('curl -fsSL https://packages.gitlab.com/install/repositories/'
|
||||
'gitlab/gitlab-ce/script.deb.sh' + pkg_args
|
||||
+ ' -o /tmp/gl-repo.sh')
|
||||
# The .rpm.sh variant is what GitLab's deb.sh script also supports;
|
||||
# but the deb.sh handles apt only. For RHEL family we fall back to rpm.sh.
|
||||
# To keep things simple and reliable, we just attempt deb.sh first;
|
||||
# if the host is RHEL-like, GitLab's script will detect and abort with
|
||||
# a hint pointing to the rpm.sh variant. We surface that as an error.
|
||||
out.append('bash /tmp/gl-repo.sh')
|
||||
out.append('rm -f /tmp/gl-repo.sh')
|
||||
out.append('')
|
||||
# Install the package. For specific version we use =<ver>, otherwise bare.
|
||||
if version == "latest":
|
||||
install_cmd = 'gitlab-ce'
|
||||
else:
|
||||
install_cmd = 'gitlab-ce=' + version
|
||||
out.append('log "Installing GitLab CE package (this may take a few minutes)..."')
|
||||
out.append('PKG_INSTALL ' + install_cmd)
|
||||
out.append('')
|
||||
# Generate /etc/gitlab/gitlab.rb
|
||||
# NOTE: heredoc is QUOTED — keeps any literal $ / % in our config
|
||||
# intact for reconfigure to interpret, and is safe under 'set -u'.
|
||||
out.append('log "Writing /etc/gitlab/gitlab.rb..."')
|
||||
rb_lines = [
|
||||
"external_url '" + ext_url.replace("'", "'\\''") + "'",
|
||||
"git_data_dir '" + data_dir + "'",
|
||||
]
|
||||
if smtp_on:
|
||||
# GitLab rb SMTP block (escaped single quotes inside heredoc are fine
|
||||
# because the heredoc delimiter is quoted; nothing will be expanded).
|
||||
rb_lines += [
|
||||
"gitlab_rails['smtp_enable'] = true",
|
||||
"gitlab_rails['smtp_address'] = '" + smtp_addr.replace("'", "'\\''") + "'",
|
||||
"gitlab_rails['smtp_port'] = 587",
|
||||
"gitlab_rails['smtp_user_name'] = '" + smtp_user.replace("'", "'\\''") + "'",
|
||||
"gitlab_rails['smtp_password'] = '" + smtp_pass.replace("'", "'\\''") + "'",
|
||||
"gitlab_rails['smtp_domain'] = '" +
|
||||
smtp_addr.split(":")[0].replace("'", "'\\''") + "'",
|
||||
"gitlab_rails['smtp_authentication'] = 'login'",
|
||||
"gitlab_rails['smtp_enable_starttls_auto'] = true",
|
||||
"gitlab_rails['smtp_tls'] = false",
|
||||
]
|
||||
out.append("cat > /etc/gitlab/gitlab.rb <<'GITLAB_RB_EOF'\n"
|
||||
+ "\n".join(rb_lines) + "\n"
|
||||
"GITLAB_RB_EOF")
|
||||
out.append('')
|
||||
out.append('log "Running gitlab-ctl reconfigure (this takes 3-5 min)..."')
|
||||
out.append('gitlab-ctl reconfigure')
|
||||
out.append('')
|
||||
# Extract bound port from external_url (best effort: default 80/443)
|
||||
bound_port = "80"
|
||||
if ext_url.startswith("https://"):
|
||||
bound_port = "443"
|
||||
out.append('log "Verifying GitLab is reachable..."')
|
||||
out.append('sleep 5')
|
||||
out.append('gitlab-ctl status >/dev/null && log "GitLab services are running" '
|
||||
'|| warn "Some GitLab services may not be up yet — check: gitlab-ctl status"')
|
||||
out.append('curl -fsS -o /dev/null -w "HTTP %{http_code} on :' + bound_port
|
||||
+ '\\n" http://127.0.0.1:' + bound_port + '/')
|
||||
out.append('log "Done. Open ' + ext_url + ' in your browser."')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Jenkins LTS ==========================
|
||||
class Jenkins(Generator):
|
||||
id = "jenkins"
|
||||
title = "Jenkins LTS (持续集成)"
|
||||
category = "ci"
|
||||
icon = "🤖"
|
||||
tags = ["jenkins", "ci", "cd", "pipeline"]
|
||||
description = (
|
||||
"下载 Jenkins LTS WAR 包,systemd 跑 java -jar,"
|
||||
"默认 JDK 17 / 21。"
|
||||
)
|
||||
warnings = [
|
||||
"首次启动会在 /var/lib/jenkins/secrets/initialAdminPassword 生成初始密码。",
|
||||
"推荐在 Manage Jenkins → Configure Global Security 关掉 'Anyone can do anything'。",
|
||||
]
|
||||
post_steps = [
|
||||
"浏览器访问 http://<host>:<port>",
|
||||
"cat /var/lib/jenkins/secrets/initialAdminPassword",
|
||||
"解锁后选 'Install suggested plugins'",
|
||||
]
|
||||
fields = [
|
||||
Field("version", "Jenkins LTS 版本", "select", default="2.541.3",
|
||||
options=["2.541.3", "2.528.2", "2.568.2", "latest"],
|
||||
help="2.541.3/2.528.2/2.568.2 是 LTS;"
|
||||
"latest 是滚动版(可能不稳定)。"),
|
||||
Field("port", "HTTP 端口", "number", default="8080", min_=1, max_=65535),
|
||||
Field("java_opts", "JVM 参数", "text",
|
||||
default="-Xms512m -Xmx1024m -Djenkins.install.runSetupWizard=false",
|
||||
help="JAVA_OPTS。-Xms/-Xmx 控制堆内存。"),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/jenkins"),
|
||||
Field("jenkins_home", "JENKINS_HOME", "text", default="/var/lib/jenkins",
|
||||
help="jobs / plugins / 配置存放目录。挂独立盘请改这里。"),
|
||||
Field("user", "运行用户", "text", default="jenkins"),
|
||||
]
|
||||
|
||||
# Pre-validated URLs. Always pin to a real release; if 'latest' is selected
|
||||
# we use the rolling URL. Listed URLs were all confirmed 200/302 at design
|
||||
# time (see PR notes); Jenkins get.jenkins.io redirects 302 to the CDN.
|
||||
_WAR_URLS = {
|
||||
"2.541.3": "https://get.jenkins.io/war-stable/2.541.3/jenkins.war",
|
||||
"2.528.2": "https://get.jenkins.io/war-stable/2.528.2/jenkins.war",
|
||||
"2.568.2": "https://get.jenkins.io/war-stable/2.568.2/jenkins.war",
|
||||
"latest": "https://get.jenkins.io/war/latest/jenkins.war",
|
||||
}
|
||||
|
||||
def render(self, p):
|
||||
version = p.get("version", "2.541.3")
|
||||
port = str(p.get("port", "8080"))
|
||||
java_opts = p.get(
|
||||
"java_opts",
|
||||
"-Xms512m -Xmx1024m -Djenkins.install.runSetupWizard=false",
|
||||
)
|
||||
install_dir = p.get("install_dir", "/opt/jenkins")
|
||||
jh = p.get("jenkins_home", "/var/lib/jenkins")
|
||||
user = p.get("user", "jenkins")
|
||||
|
||||
if version not in self._WAR_URLS:
|
||||
raise ValueError(
|
||||
f"未知 Jenkins 版本: {version}。可选: "
|
||||
+ ", ".join(sorted(self._WAR_URLS.keys()))
|
||||
)
|
||||
url = self._WAR_URLS[version]
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing JDK + Jenkins LTS..."')
|
||||
out.append('command -v java >/dev/null || '
|
||||
'PKG_INSTALL openjdk-17-jdk-headless || '
|
||||
'PKG_INSTALL java-17-openjdk-devel || '
|
||||
'PKG_INSTALL default-jdk-headless || '
|
||||
'PKG_INSTALL default-jdk')
|
||||
out.append('command -v java >/dev/null || die "Java not found after install — pick a different JDK"')
|
||||
out.append('')
|
||||
# User + dirs
|
||||
out.append(f'useradd -r -s /bin/false -d {jh} {user} 2>/dev/null || true')
|
||||
out.append(f'mkdir -p {install_dir} {jh} /var/log/jenkins')
|
||||
out.append(f'chown -R {user}:{user} {jh} /var/log/jenkins')
|
||||
out.append('')
|
||||
# Download WAR
|
||||
out.append(f'log "Downloading Jenkins WAR ({version})..."')
|
||||
out.append(f'curl -fsSL -o {install_dir}/jenkins.war "{url}"')
|
||||
out.append(f'chown {user}:{user} {install_dir}/jenkins.war')
|
||||
out.append('')
|
||||
# systemd unit — QUOTED heredoc so %i / $PORT placeholders stay intact
|
||||
out.append('log "Writing systemd unit..."')
|
||||
# Quote java_opts for systemd Environment= line — it must be a single
|
||||
# value, and embedded '"' or '\' in user input would otherwise break
|
||||
# the unit file. We use double-quote escaping (\\" within the value).
|
||||
safe_java_opts = java_opts.replace("\\", "\\\\").replace('"', '\\"')
|
||||
out.append(
|
||||
f"cat > /etc/systemd/system/jenkins.service <<'UNIT_EOF'\n"
|
||||
'[Unit]\n'
|
||||
'Description=Jenkins LTS Continuous Integration Server\n'
|
||||
'After=network.target\n\n'
|
||||
'[Service]\n'
|
||||
'Type=simple\n'
|
||||
f'User={user}\n'
|
||||
f'Group={user}\n'
|
||||
f'Environment="JENKINS_HOME={jh}"\n'
|
||||
f'Environment="JAVA_OPTS={safe_java_opts}"\n'
|
||||
f'Environment="JENKINS_PORT={port}"\n'
|
||||
f'WorkingDirectory={jh}\n'
|
||||
f'ExecStart=/usr/bin/java $JAVA_OPTS -jar {install_dir}/jenkins.war '
|
||||
f'--httpPort=$JENKINS_PORT\n'
|
||||
'Restart=on-failure\n'
|
||||
'RestartSec=10\n\n'
|
||||
'[Install]\n'
|
||||
'WantedBy=multi-user.target\n'
|
||||
'UNIT_EOF'
|
||||
)
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now jenkins')
|
||||
out.append('log "Waiting for Jenkins to come up..."')
|
||||
# Jenkins can take 30-90s on first start while extracting plugins
|
||||
out.append('for i in $(seq 1 30); do')
|
||||
out.append(' if curl -fsS -o /dev/null http://127.0.0.1:' + port + '/ ; then')
|
||||
out.append(' log "Jenkins is responding on :' + port + '"')
|
||||
out.append(' break')
|
||||
out.append(' fi')
|
||||
out.append(' sleep 3')
|
||||
out.append('done')
|
||||
out.append('if [ ! -f ' + jh + '/secrets/initialAdminPassword ]; then')
|
||||
out.append(' warn "Initial admin password not found yet — Jenkins may still be starting."')
|
||||
out.append(' warn "Check: systemctl status jenkins / journalctl -u jenkins -n 50"')
|
||||
out.append('else')
|
||||
out.append(' log "Initial admin password (copy this):"')
|
||||
out.append(' cat ' + jh + '/secrets/initialAdminPassword')
|
||||
out.append('fi')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
register(GitLab())
|
||||
register(Jenkins())
|
||||
Reference in New Issue
Block a user