前言
最近看jenkins as code这个概念在很多文章中提起,持续交付中八大原则也有把一切都放入版本管理,最近准备把我们公司用的一些jenkins上的job的配置也放到git中,由于https://github.com/jenkinsci/job-dsl-plugin的支持是groovy。我不懂groovy,所以找了一些网上的groovy脚本改了改,并且通过go template实现这个模板,还能练习使用go。
所有用的到文件简介
一共用到3个文件,只有一个模板,如果有多个情景可以多写几个模板
bogon:jenkins-dsl hongzhi.wang$ tree
.
├── config.yaml #配置文件
├── generate_dsl.go #go 源码文件
└── springboot.tpl #go template 文件
配置文件
这次准备通过yaml文件实现模板的渲染,配置文件如下,通过yaml来配置文件比较清晰。
- department_name: java
cn_name: java组
apps:
- app_name: app-1
nodes:
- node1
- node2
- node3
ansible_playbook_name: deploy-springboot-jar.yml
jvm_size: 2048m
template_name: springboot.tpl
- app_name: app-2
nodes:
- node1
- node2
- node3
ansible_playbook_name: deploy-springboot-jar.yml
jvm_size: 512m
template_name: springboot.tpl
- department_name: java2
cn_name: java2组
apps:
- app_name: app-4
nodes:
- node1
- node2
- node3
ansible_playbook_name: deploy-springboot-war.yml
jvm_size: 2048m
template_name: springboot.tpl
GO Template
这里面主要是用到了自定函数ProcessNodes
,其他的都是模板的正常用法,还有就是我们的jenkins主要是传参数拉代码,然后通过ansible-playbook实现应用部署。
def app_name = '{{ .App.AppName }}'
def gitUrl = "git@gitlab.wis.com:{{ .Name }}/{{ .App.AppName }}.git"
job("{{ .Name }}-${app_name}") {
description()
keepDependencies(false)
parameters {
choiceParam("Mode", ["release", "deploy"], "")
choiceParam("app_name", ["${app_name}"], "")
choiceParam("NODES", {{ ProcessNodes .App.Nodes }}, "")
gitParam('VERSION') {
sortMode('DESCENDING')
tagFilter('*')
}
}
scm {
git {
remote {
url(gitUrl)
credentials("git")
}
branch("$VERSION")
}
}
disabled(false)
concurrentBuild(false)
steps {
maven{
configure { node ->
node / 'mavenName' ('maven')
}
goals('clean')
}
maven{
configure { node ->
node / 'mavenName' ('maven')
}
goals('install -Pproduct')
}
shell("""
source /opt/py3/bin/activate
if [ $Mode == release ]
then
START_TASK="copy app code"
fi
echo $JOB_NAME
cd /ansible/
echo $WORKSPACE
for host in $NODES
do
ansible-playbook --start-at-task="$START_TASK" {{ .App.AnsiblePlaybookName }} --extra-vars "target_host=$host workspace=$WORKSPACE app_name=$app_name version=$VERSION jvm_size={{ .App.JvmSize }}"
done
""")
}
}
publishers {
postBuildScripts {
steps {
steps {
shell("""
/ops/scripts/dingding.py {{ .Name }} `git show -s --pretty=%cN` `git tag -ln $VERSION` $NODES
""")
}
}
markBuildUnstable(false)
}
}
}
listView("{{ .CnName }}") {
jobs {
regex(/{{ .Name }}-.+/)
}
columns {
status()
weather()
name()
lastSuccess()
lastFailure()
lastDuration()
buildButton()
}
}
GO源码
这个源码主要按照配置文件,把已经去掉的部门目录还有应用groovy脚本给删了,并且推送到远程分支,groovy脚本的名字里面不能有-
,因为groovy的脚本名字会解析成java的类名,-
是不能放在java语言的类名里面了。所以只能把应用名字都的-
转成_
。
package main
import (
"io/ioutil"
"github.com/ghodss/yaml"
"fmt"
"text/template"
"os"
"log"
"strings"
"os/exec"
"bytes"
)
type Department struct {
Name string `json:"department_name"`
CnName string `json:"cn_name"`
Apps []App `json:"apps"`
}
type App struct {
AppName string `json:"app_name"`
Nodes []string `json:"nodes"`
PythonVersion string `json:"python_version"`
JvmSize string `json:"jvm_size"`
TemplateName string `json:"template_name"`
AnsiblePlaybookName string `json:"ansible_playbook_name"`
DestPath string `json:"dest_path"`
TomcatGitUrl string `json:"tomcat_git_url"`
TomcatPort string `json:"tomcat_port"`
TomcatAdminPort string `json:"tomcat_admin_port"`
TomcatName string `json:"tomcat_name"`
TomcatWebApp string `json:"tomcat_web_app"`
}
func In(i string,l []string) bool{
for _, item:= range l{
if item == i{
return true
}
}
return false
}
func checkErr(err error) {
if err != nil{
log.Fatal(err)
}
}
func ProcessNodes(l []string) string {
if len(l) == 1{
return fmt.Sprintf(`["%s"]`,strings.Join(l," "))
}
return fmt.Sprintf(`["%s","%s"]`,l[0],strings.Join(l," "))
}
func AppNameToFileName(appname string) string {
appname = strings.Replace(appname,"-","_",-1)
return fmt.Sprintf("%s.groovy",appname)
}
func SysCommand(command string){
commandList := strings.Fields(command)
var out bytes.Buffer
cmd := exec.Command(commandList[0],commandList[1:]...)
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
if err != nil{
log.Printf("commd error: %v",command)
}
log.Println(out.String())
}
func main() {
content,err := ioutil.ReadFile("config.yaml")
checkErr(err)
var d []Department
err = yaml.Unmarshal(content, &d)
checkErr(err)
DepartmentNames := []string{}
funcMap := template.FuncMap{
"ProcessNodes": ProcessNodes,
}
for _, department := range d{
err := os.MkdirAll(department.Name,0755)
checkErr(err)
DepartmentNames = append(DepartmentNames, department.Name)
if AppFileNames := []string{}; department.Apps != nil{
for _, app := range department.Apps{
FileName := AppNameToFileName(app.AppName)
AppFileNames = append(AppFileNames,FileName)
data := map[string]interface{}{
"Name":department.Name,
"CnName":department.CnName,
"App":app,
}
templ,err := template.New(app.TemplateName).Funcs(funcMap).ParseFiles(app.TemplateName)
checkErr(err)
f, err := os.Create(fmt.Sprintf("%s/%s",department.Name, FileName))
defer f.Close()
checkErr(err)
err = templ.Execute(f, data)
checkErr(err)
}
ExistFiles,err := ioutil.ReadDir(department.Name)
checkErr(err)
for _,v := range ExistFiles{
if ! In(v.Name(),AppFileNames){
log.Println("going to delete file: ",v.Name())
SysCommand(fmt.Sprintf("git rm %s/%s",department.Name,v.Name()))
}
}
}
}
ExistDirs,err := ioutil.ReadDir("./")
checkErr(err)
for _,dir := range ExistDirs{
if dir.IsDir()&& dir.Name()!=".idea" && dir.Name()!=".git" && ! In(dir.Name(),DepartmentNames){
log.Println("going to delete dir: ",dir.Name())
log.Printf("git rm -r %s
",dir.Name())
SysCommand(fmt.Sprintf("git rm -r %s",dir.Name()))
checkErr(err)
}
}
SysCommand("git add * ")
SysCommand("git commit -m 'update' ")
//SysCommand("git push ")
log.Println("finished")
}
templ,err := template.New(app.TemplateName).Funcs(funcMap).ParseFiles(app.TemplateName)
这个之所以这样是因为要传入自定的函数,parsefile的返回值是两个不能直接用Funcs。这个就要详见Stack Overflow上的答案了。
脚本运行的结果
bogon:jenkins-dsl hongzhi.wang$ go run generate_dsl.go
bogon:jenkins-dsl hongzhi.wang$ tree
.
├── config.yaml
├── generate_dsl.go
├── java
│ ├── app_1.groovy
│ └── app_2.groovy
├── java2
│ └── app_4.groovy
└── springboot.tpl
剩余的工作
这个脚本把代码推送到gitlab之后,gitlab通过webhook触发一个jenkins-seed-job,这个job再运行groovy脚本,这个groovy脚本运行之后我们线上的 job 就和 YAML 文件描述的一致了,我们是一个部门对应一个jenkins-seed-job,一个webhook。如果config.yaml太大了,可以把一些部门的配置放到另一个源码库里。
steps {
dsl {
ignoreExisting(false)
removeAction("DELETE")
removeViewAction("IGNORE")
lookupStrategy("JENKINS_ROOT")
}
}
更新于2019年3月
最近由于有的组需要频繁发版,所以把部署的权限给了这些组的负责人,但是我们运维又需要知道他们发版的时间和具体原因,所以我们这边的方法是通过 http 调用发送响应的信息到对应的组的钉钉群。调研了一下,直接通过 curl 调用钉钉的接口变量老是传不过去,而且 curl 调用不是很灵活,所以通过在 jenkins 服务器上放置 python 脚本的方式发送钉钉消息,脚本传入的参数分别是组名,git commiter 名字,git tag 和对应 tag 的详细描述,部署的生产服务器,脚本根据组名来分辨具体发送到哪个组。只有构建成功的作业才会发送到钉钉。
publishers {
postBuildScripts {
steps {
steps {
shell("""
/ops/scripts/dingding.py {{ .Name }} `git show -s --pretty=%cN` `git tag -ln $VERSION` $NODES
""")
}
}
markBuildUnstable(false)
}
}
}
总结
这次主要是把jenkins的job管理纳入版本控制,并且练习一下go。这里主要是做个笔记o( ̄︶ ̄)o。
重构
最近由于新的需求把上面go的代码重构了一下,如下:
package main
import (
"bytes"
"fmt"
"github.com/ghodss/yaml"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"text/template"
)
const (
ImageTemplatePath = "image_dsl"
)
var (
TemplateName = "common.tpl"
SystemDirs = []string{".git", "image_dsl", ".idea", "templates"}
)
func main() {
SysCommandErrorExit("git pull")
content, err := ioutil.ReadFile("config.yaml")
checkErr(err)
var company Company
err = yaml.Unmarshal(content, &company.Departments)
checkErr(err)
company.JenkinsDsl()
company.CleanInactiveDepartments()
SysCommand("git add * ")
SysCommand("git commit -m 'update' ")
SysCommand("git push ")
log.Println("finished")
}
type Company struct {
Departments []Department `json:"departments"`
}
func (c Company) DepartmentNames() (DepartmentNames []string) {
for _, department := range c.Departments {
DepartmentNames = append(DepartmentNames, department.Name)
}
return
}
func (c Company) JenkinsDsl() {
for _, department := range c.Departments {
department.JenkinsDsl()
}
}
func (c Company) CleanInactiveDepartments() {
ExistDirs, err := ioutil.ReadDir("./")
checkErr(err)
for _, dir := range ExistDirs {
if dir.IsDir() && ! In(dir.Name(), SystemDirs) && ! In(dir.Name(), c.DepartmentNames()) {
log.Println("going to delete dir: ", dir.Name())
log.Printf("git rm -r %s
", dir.Name())
SysCommand(fmt.Sprintf("git rm -r %s", dir.Name()))
}
}
}
type Department struct {
Name string `json:"department_name"`
CnName string `json:"cn_name"`
Apps []App `json:"apps"`
}
func (d Department) JenkinsDsl() {
d.DepartmentDir()
if d.Apps != nil {
for _, app := range d.Apps {
app.JenkinsDsl(d)
}
d.CleanOfflineApps()
}
}
func (d Department) CleanOfflineApps() {
ExistFiles, err := ioutil.ReadDir(d.Name)
checkErr(err)
for _, v := range ExistFiles {
if ! In(v.Name(), d.AppFileNames()) {
log.Println("going to delete file: ", v.Name())
SysCommand(fmt.Sprintf("git rm %s/%s", d.Name, v.Name()))
}
}
}
func (d Department) AppFileNames() (AppFileNames []string) {
for _, app := range d.Apps {
AppFileNames = append(AppFileNames, AppNameToFileName(app.AppName))
}
return
}
func (d Department) DepartmentDir() {
err := os.MkdirAll(d.Name, 0755)
checkErr(err)
}
type App struct {
AppName string `json:"app_name"`
Department Department `json:"department"`
BuildImage bool `json:"build_image"`
ArtifactFormat string `json:"artifact_format"`
JvmSize string `json:"jvm_size"`
JavaVersion string `json:"java_version"`
Clusters []Cluster `json:"clusters"`
ImageTPL string `json:"image_tpl"`
Values map[string]interface{} `json:"values"`
}
type Cluster struct {
Name string `json:"name"`
ReplicaCount int `json:"replicaCount"`
ReplicaCountMax int `json:"replicaCountMAX"`
}
func (a App) JenkinsDsl(d Department) {
a.Department = d
f := a.file()
err := a.Template().Execute(f, a.TemplateData())
defer f.Close()
checkErr(err)
}
func (a App) ValuesYamlString() (values string) {
Values, err := yaml.Marshal(a.Values)
checkErr(err)
return string(Values)
}
func (a App) TemplateData() (data map[string]interface{}) {
data = map[string]interface{}{
"Name": a.Department.Name,
"CnName": a.Department.CnName,
"App": a,
"Values": a.ValuesYamlString(),
}
return
}
func (a App) Template() (templ *template.Template) {
helmTpl := fmt.Sprintf("templates/helm_dsl/%s", TemplateName)
templ, err := template.New(TemplateName).ParseFiles(helmTpl)
checkErr(err)
return
}
func (a App) file() (*os.File) {
FileName := AppNameToFileName(a.AppName)
f, err := os.Create(fmt.Sprintf("%s/%s", a.Department.Name, FileName))
checkErr(err)
return f
}
func In(i string, l []string) bool {
for _, item := range l {
if item == i {
return true
}
}
return false
}
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
func AppNameToFileName(appname string) string {
appname = strings.Replace(appname, "-", "_", -1)
return fmt.Sprintf("%s.groovy", appname)
}
func SysCommandErrorExit(command string) {
commandList := strings.Fields(command)
var out bytes.Buffer
cmd := exec.Command(commandList[0], commandList[1:]...)
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
if err != nil {
log.Fatalln("command error: %v", command)
}
log.Println(out.String())
}
func SysCommand(command string) {
commandList := strings.Fields(command)
var out bytes.Buffer
cmd := exec.Command(commandList[0], commandList[1:]...)
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
if err != nil {
log.Printf("command error: %v", command)
}
log.Println(out.String())
}