""" 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 =, 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://:", "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())