使用 GitLab API 學習 Go 語言

logo

前言

常常會有人問我如何學習 Go 語言,我通常會建議他們從實際專案開始,這樣可以更快的學習到語言的特性。個人也是透過先寫小專案,再慢慢擴大專案的範圍,從貢獻文件到開源專案,在進一步學習如何修改原始碼,最後再自己寫一個專案。這樣的學習方式可以讓你更快的熟悉 Go 語言的特性。

GitLab API

這篇文章將會介紹如何使用 GitLab API 來學習 Go 語言。為什麼會選擇這個主題呢?相信大家都知道 GitLab 是一套免費開源的 Git 版本控制系統,而且提供了一個 RESTful API 供開發者使用。透過 GitLab API,我們可以取得專案的資訊、建立專案、刪除專案、取得專案的檔案內容等等,或者是可以透過 GitLab API 來自動化一些工作,例如建立專案後自動建立 CI/CD 流程,或者是自動化部署專案到伺服器上。可是今天遇到一個問題,如果團隊 Source Code 放在 GitHub 或者是 Gitea 上,讓這樣如何透過 GitHub 或 Gitea Action 來觸發 GitLab CI/CD 流程呢?這樣就可以跨團隊互相觸發各自的 CI 流程。

實作流程

完整的程式碼可以在 GitHub 上找到。

這邊我們將會使用 Go 語言 來實作一個簡單的程式,透過 GitLab API 來取得專案的資訊。這邊我們使用的是 go-gitlab 套件,這是一個 GitLab API 的 Go 語言套件,可以讓你更容易的使用 GitLab API。

要觸發 GitLab CI Piepline 只需要一個 API 就可以完成,這邊我們使用的是 CreatePipeline 方法,這個方法可以透過專案 ID 來觸發 CI Pipeline。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Create Gitlab object
g, err := NewGitlab(p.Host, p.Token, p.Insecure, p.Debug)
if err != nil {
  return err
}

// Create pipeline
pipeline, err := g.CreatePipeline(p.ProjectID, p.Ref, p.Variables)
if err != nil {
  return err
}

但是這執行完成後,我們需要等待 CI Pipeline 執行完成,這邊我們可以透過 GetPipelineStatus 方法來取得 Pipeline 的狀態。由於需要等待 CI Pipeline 執行完成,所以我們需要一個定時器 Ticker 迴圈來等待 CI Pipeline 執行完成。並且設定一個 Timeout 時間,如果超過 Timeout 時間,則會回傳錯誤訊息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Wait for pipeline to complete
ticker := time.NewTicker(p.Interval)
defer ticker.Stop()

l.Info("waiting for pipeline to complete", "timeout", p.Timeout)
for {
  select {
  case <-time.After(p.Timeout):
    return errors.New("timeout waiting for pipeline to complete after " + p.Timeout.String())
  case <-ticker.C:
    // Check pipeline status
    status, err := g.GetPipelineStatus(p.ProjectID, pipeline.ID)
    if err != nil {
      return err
    }

    l.Info("pipeline status",
      "status", status,
      "triggered_by", pipeline.User.Name,
    )

    // https://docs.gitlab.com/ee/api/pipelines.html
    // created, waiting_for_resource, preparing, pending,
    // running, success, failed, canceled, skipped, manual, scheduled
    if status == "success" ||
      status == "failed" ||
      status == "canceled" ||
      status == "skipped" {
      l.Info("pipeline completed", "status", status)
      if p.IsGitHub {
        // update status
        if err := gh.SetOutput(map[string]string{"status": status}); err != nil {
          return err
        }
      }
      return nil
    }
  }
}

大家看一下上面的程式碼,是不是有哪邊可以優化的地方呢?主要是在使用 time.After 時候搭配 for 迴圈,會變成每次 ticker.C 時間到後,會重新再計算整體執行時間,這樣就不是我們的初衷。

這邊可以透過 context 來優化程式碼,這樣就可以更容易的控制程式的執行時間。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Create a new context with a timeout
ctx, cancel := context.WithTimeout(context.Background(), p.Timeout)
defer cancel()
for {
  select {
  case <-ctxTimeout.Done():
    return errors.New("timeout waiting for pipeline to complete after " + p.Timeout.String())
  case <-ticker.C:
    if ctxTimeout.Err() != nil {
      if p.IsGitHub {
        // update status
        if err := gh.SetOutput(map[string]string{"status": status}); err != nil {
          return err
        }
      }
      return ctxTimeout.Err()
    }
  }
}

畫面

透過 GitHub Action 執行成果,大家可以參考 GitHub Repo

gitlab-ci-action

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
time=2024-10-21T15:17:42.079Z level=INFO msg="pipeline created" project_id=***
pipeline_id=1505619557 pipeline_sha=a36503d3ba12e7832752e17c213efd09000fac03
pipeline_ref=main pipeline_status=created
pipeline_web_url=https://gitlab.com/appleboy/test/-/pipelines/1505619557
pipeline_created_at=2024-10-21T15:17:41.767Z
time=2024-10-21T15:17:42.079Z level=INFO msg="waiting for pipeline to complete" project_id=*** timeout=1h0m0s
time=2024-10-21T15:17:47.237Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:17:52.212Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:17:57.209Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:02.219Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:07.217Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:12.222Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:17.210Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:22.235Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:27.219Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:32.241Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:37.395Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:42.219Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:47.229Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:52.211Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:18:57.225Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:02.219Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:07.247Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:12.283Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:17.254Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:22.200Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:27.208Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:32.213Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:37.244Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:42.256Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:47.219Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:52.217Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:19:57.234Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:20:02.226Z level=INFO msg="pipeline status" project_id=*** status=running triggered_by="Bo-Yi Wu"
time=2024-10-21T15:20:07.283Z level=INFO msg="pipeline status" project_id=*** status=success triggered_by="Bo-Yi Wu"
time=2024-10-21T15:20:07.283Z level=INFO msg="pipeline completed" project_id=*** status=success

簡單案例

要如何複製上面的問題呢?可以參考底下代碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
  "time"
)

func main() {
  output := make(chan int, 10)

  go func() {
    for i := 0; i < 30; i++ {
      output <- i
      time.Sleep(100 * time.Millisecond)
    }
  }()

  // how to fix the timeout issue?
  for {
    select {
    case val := <-output:
      // simulate slow consumer
      time.Sleep(500 * time.Millisecond)
      println("output:", val)
    // how to fix the timeout issue?
    case <-time.After(1 * time.Second):
      println("timeout")
      return
    }
  }
}

這樣就可以複製上面的問題,透過 time.After 來設定 Timeout 時間,這樣就可以模擬上面的問題。

解決方案 01

將 time.After 移到 for 迴圈外面,這樣就可以避免每次 ticker.C 時間到後,會重新再計算整體執行時間。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
  "time"
)

func main() {
  output := make(chan int, 30)

  go func() {
    for i := 0; i < 30; i++ {
      output <- i
      time.Sleep(100 * time.Millisecond)
    }
  }()

  timeout := time.After(1 * time.Second)
  // how to fix the timeout issue?
  for {
    select {
    case val := <-output:
      select {
      case <-timeout:
        println("reached timeout, but still have data to process")
        return
      default:
      }
      // simulate slow consumer
      time.Sleep(500 * time.Millisecond)
      println("output:", val)
    // how to fix the timeout issue?
    case <-timeout:
      println("timeout")
      return
    }
  }
}

解決方案 02

透過 context 來優化程式碼,這樣就可以更容易的控制程式的執行時間。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
  "context"
  "time"
)

func main() {
  output := make(chan int, 30)

  go func() {
    for i := 0; i < 30; i++ {
      output <- i
      time.Sleep(100 * time.Millisecond)
    }
  }()

  ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
  defer cancel()
  // how to fix the timeout issue?
  for {
    select {
    case val := <-output:
      if ctx.Err() != nil {
        println("reached timeout, but still have data to process")
        return
      }
      // simulate slow consumer
      time.Sleep(500 * time.Millisecond)
      println("output:", val)
    // how to fix the timeout issue?
    case <-ctx.Done():
      println("timeout")
      return
    }
  }
}

結論

透過 GitLab API 來自動化 CI/CD 流程是一個很好的學習方式,這樣可以讓你更了解 CI/CD 流程的運作方式,並且可以透過程式碼來觸發 CI/CD 流程,這樣就可以更容易的整合到你的專案中。而 Go 語言是一個很好的學習語言,透過 Go 語言來實作一個小專案,這樣可以讓你更快的熟悉 Go 語言的特性。本篇重點是讓想學習 Go 語言的朋友了解 selectcontext 的整合,並且如何使用 context 來優化程式碼。