zoukankan      html  css  js  c++  java
  • jenkins as code 与go语言学习

    前言

    最近看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())
    }
    
    
  • 相关阅读:
    python高阶1--is 和==
    python基础知识 -- 输入与输出
    Linux忘记用户名密码
    pip 安装第三方库报错
    python读取ini文件(含中文)
    fiddler之手机抓包
    python接口测试之参数关联遇到的问题
    (十一)TestNG 其他使用技巧
    (十二)TestNG 生成测试报告
    (十) TestNG 多线程运行用例
  • 原文地址:https://www.cnblogs.com/WisWang/p/9533185.html
Copyright © 2011-2022 走看看