暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Golang实现版本发布插件

运维DevOps 2024-11-21
112

介绍

在用 jenkins 实现远程发布的时候,除了通过 ansible 外,还可以通过自定义接口的方式来实现文件传输与版本发布,本例子演示通过 golang 自定义接口来实现文件发布。

实现原理

golang 服务端定义两个接口/upload/fparam,其中/upload接口主要用于将本地文件发送到远程机器上,/fparam 接口用于调用远程机器上的命令和脚本来实现发布,详细步骤如下:

  1. 用户端调用服务端的/upload接口将要发布的文件传送到服务端(远程机器)
  2. 服务端在接收到文件后,储存起来并计算文件的md5值,计算后将此md5的值返回给用户端,用户端通过返回的md5值和自己发送的文件作对比,如果一样,说明文件发送过程中无损失
  3. 用户端再次调用/fpram接口,将刚才发送的文件名再次发送给服务端,服务端接收到文件名后,根据名字去调用函数,函数中通过文件名已经路径并使用命令将此文件发送到要发布的机器上,发送方式有ansible和scp两种方式(纯内网发送一般不会存在文件不完整),发送后再次调用脚本执行备份和发布操作
服务端脚本deployserver.go内容如下:


    package main


    import (
    "crypto/md5"
    "encoding/hex"
    "errors"
    "fmt"
    "github.com/gin-gonic/gin"
    "io"
    "log"
    "os"
    "os/exec"
    )


    // 获取文件的md5值
    func md5Value(filename string) string {
    f, err := os.Open(filename)
    if err != nil {
    log.Fatal(err)
    }
    defer f.Close()
    hash := md5.New()
    if _, err := io.Copy(hash, f); err != nil {
    log.Fatal(err)
    }
    hashBytes := hash.Sum(nil)
    hashString := hex.EncodeToString(hashBytes)
    return hashString
    }


    // 通过scp和ssh方式
    func execAnsible(hostname, srcpath, destpath string) error {
    scpSlice := []string{fmt.Sprintf("%s", srcpath), fmt.Sprintf("%s:%s", hostname, destpath)}
    backSlice := []string{"-t", fmt.Sprintf("%s", hostname), "/data/script/backup.sh"}
    deploySlice := []string{"-t", fmt.Sprintf("%s", hostname), "/data/script/deploy.sh"}
    mapp := make(map[int][]string)
    mapp[0] = scpSlice
    mapp[1] = backSlice
    mapp[2] = deploySlice
    for k, v := range mapp {
    if k == 0 {
    cmd := exec.Command("scp", v...)
    _, err := cmd.CombinedOutput()
    if err != nil {
    return err
    }
    } else {
    cmd := exec.Command("ssh", v...)
    _, err := cmd.CombinedOutput()
    if err != nil {
    return err
    }
    }
    }
    return nil
    }


    // 通过ansible方式,跟上面的通过scp和ssh方式一样,根据情况选择
    //
    // func execAnsible(hostname, srcpath, destpath string) error {
    // cmdSlice := [][]string{
    // {fmt.Sprintf("%s", hostname), "-m", "copy", "-a", fmt.Sprintf("src=%s dest=%s", srcpath, destpath)},
    // {fmt.Sprintf("%s", hostname), "-m", "shell", "-a", "bash data/script/backup.sh"},
    // {fmt.Sprintf("%s", hostname), "-m", "shell", "-a", "bash data/script/deploy.sh"},
    // }
    // for _, v := range cmdSlice {
    // //v是一个一维切片,...可以将切片中元素拆出来作为参数传递给Command,Command至少需要两个参数
    // cmd := exec.Command("ansible", v...)
    // _, err := cmd.CombinedOutput()
    // if err != nil {
    // fmt.Println("errrs:", err)
    // return err
    // }
    // // continue
    // }
    // return nil
    // }
    func checkFilename(filename string) error {
    if filename == "api-order.jar" {
    //api-order.jar如何部署在了多个节点上,那么就循环节点信息,将文件发送到多个节点,app0和app2表示节点主机名
    orderSlice := []string{"app0", "app2"}
    for _, node := range orderSlice {
    err := execAnsible(node, "/data/deploy/api-order.jar", "/data/deploy")
    if err != nil {
    return err
    }
    }
    return nil
    } else if filename == "a.jar" {
    err := execAnsible("192.168.49.186", "/tmp/a.jar", "/data/linshi")
    if err != nil {
    return err
    }
    return nil
    } else {
    return errors.New("不存在这个文件!!!")
    }
    }
    func main() {
    router := gin.Default()
    router.POST("/upload", func(c *gin.Context) {
    _, file, err := c.Request.FormFile("filename")
    if err != nil {
    log.Fatal("err:", err)
    }
    err = c.SaveUploadedFile(file, "/data/deploy/"+file.Filename)
    if err != nil {
    log.Fatal(err)
    }
    //获取md5值后返回给客户端做对比
    md5v := md5Value(fmt.Sprintf("/data/deploy/%s", file.Filename))
    c.JSON(200, gin.H{
    "code": 200,
    "data": md5v,
    })
    })
    //客户端传递文件名过来,根据文件名在服务端做文件copy和备份发布操作
    router.POST("/fparam", func(c *gin.Context) {
    filename := c.PostForm("fname")
    err := checkFilename(filename)
    if err != nil {
    c.JSON(200, gin.H{"code": 100, "data": err})
    } else {
    c.JSON(200, gin.H{
    "code": 200, "data": filename,
    })
    }
    })
    router.Run(":8888")
    }

    注意:checkFilename函数中,根据传入的文件名不同,来判断发送到不同的主机上,可根据需要添加多个

    用户端脚本deployagent.go内容如下:

      package main


      import (
      "bytes"
      "crypto/md5"
      "encoding/hex"
      "encoding/json"
      "fmt"
      "io"
      "io/ioutil"
      "log"
      "mime/multipart"
      "net/http"
      "os"
      "path/filepath"
      // "time"
      )


      type jtos struct {
      Code int `json: code` 结构体标签加和不加,影响不大
      Data string `json: data`
      }


      // 将json反序列化到结构体中,为了获取json中的data内容,也就是远程文件的md5值
      func jsonToStruct(j string) string {
      jts := jtos{}
      err := json.Unmarshal([]byte(j), &jts)
      if err != nil {
      log.Fatal(err)
      }
      return jts.Data


      }
      func md5Value(filename string) string {
      f, err := os.Open(filename)
      if err != nil {
      log.Fatal(err)
      }
      defer f.Close()
      hash := md5.New()
      if _, err := io.Copy(hash, f); err != nil {
      log.Fatal(err)
      }
      hashBytes := hash.Sum(nil)
      hashString := hex.EncodeToString(hashBytes)
      return hashString


      }
      func dproject(filename string) {
      client := &http.Client{} //创建一个http的client实例,用于发送http请求
      bf := &bytes.Buffer{} //创建Buffer实例,用于在内存中存储发送的nultipart数据
      writer := multipart.NewWriter(bf) //创建writer实例,写入到buffer中,writer后续用于构建multipart/form-data请求体
      req, err := http.NewRequest("POST", "http://127.0.0.1:8888/upload", nil)
      if err != nil {
      log.Fatal(err)
      }
      //设置请求头,writer.FormDataContentType方法返回一个Content-Type,格式为multipart/form-data;boundary=<boundary>,<boundary> 是一个随机生成的唯一字符串,用于分隔请求体中的不同部分
      req.Header.Set("Content-Type", writer.FormDataContentType())
      md5value := md5Value(filename)
      file, err := os.Open(filename)
      if err != nil {
      log.Fatal(err)
      }
      defer file.Close()
      //创建表单文件字段,字段名为filename,filepath.Base可以截取路径最后的文件名
      part, err := writer.CreateFormFile("filename", filepath.Base(filename))
      if err != nil {
      log.Fatal(err)
      }
      //将文件数据流复制到表单字段part中
      _, err = io.Copy(part, file)
      if err != nil {
      log.Fatal(err)
      }
      err = writer.Close()
      if err != nil {
      log.Fatal(err)
      }
      //writer.WriteField("fieldName","fieldValue") //添加其他字段
      req.Body = io.NopCloser(bf)
      resp, err := client.Do(req)
      if err != nil {
      log.Fatal("cuowu:", err)
      }
      defer resp.Body.Close()
      bodys, err := ioutil.ReadAll(resp.Body)
      if err != nil {
      log.Fatal(err)
      }
      r := jsonToStruct(string(bodys))
      //发送之前的文件和发送到远程机器文件的md5值相同,说明文件传输正常
      if md5value == r {
      data := fmt.Sprintf("fname=%s",filepath.Base(filename))
      requestBody := bytes.NewBuffer([]byte(data))
      reqq,err := http.NewRequest("POST","http://127.0.0.1:8888/fparam",requestBody)
      if err != nil {
      log.Fatal(err)
      }
      reqq.Header.Set("Content-Type","application/x-www-form-urlencoded")
      resps,err := client.Do(reqq)
      if err != nil {
      log.Fatal(err)
      }
      defer reqq.Body.Close()
      bodyss,err := ioutil.ReadAll(resps.Body)
      if err != nil {
      log.Fatal(err)
      }
      fmt.Println(string(bodyss))
      }
      }
      func main() {
      s := os.Args[1:]
      for _, v := range s {
      dproject(v)
      }
      }

      上面的接口地址,根据需要替换为实际ip或者域名即可

      用法

      deployserver.go编译为二进制后在服务端启动,如下:

        go build deployserver.go
        nohup ./deployserver &

        deployagent.go编译为二进制后启动(可与构建后的包在一台机器或者自定义机器),如下:

          go build deployagent.go
          ./deployagent b.jar //后面要跟上要发送到远程机器的包


          文章转载自运维DevOps,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论