iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 28
0
DevOps

不一樣的 CI/CD 工具:Concourse 初探系列 第 28

28 - 自訂資源 (8)

雖然已經讓自訂資源可以實際動起來了,但是後續的維護也還是有困難的。為了改善這些問題,我們可以再多做一些處理來對應這些問題。

Makefile

因為製作 Docker Image 常常需要非常多指令,所以大多數時候都會設置一個 Makefile 來輔助。像是這類型簡單的操作,利用 Makefile 都是很容易撰寫的。

Docker 類

一般筆者會把 buildpush 設定到 Makefile 裡面來輔助。

REPO_NAME=elct9620/custom-resource

all: build

build:
  docker build -t $(REPO_NAME) .
  
push: build
  docker push $(REPO_NAME)

run: build
  docker run -it --rm $(REPO_NAME) /bin/sh

這樣一來要測試生成 Docker Image 是否有問題、上傳 Docker Image 就只需要用 makemake push 即可。

有時候也會用 make run 來進入容器中實際測試指令是否正常運作。

Concourse 類

經過幾次的實作,其實會發現 Concourse 的指令其實是又臭又長的,為了方便測試自訂資源把一些常用的指令寫進去其實是比較好處理的。

所幸一般情況 Pipeline 都只要設定個一兩次就不用重新設定,但是測試的時候為了讓新版的 Docker Image 能馬上被下載,就有必要這樣做。

PIPELINE=custom-resource-example

sp:
  fly -t lite sp -p $(PIPELINE) -c concourse.yml -n
  
up:
  fly -t lite up -p $(PIPELINE)
  
dp:
  fly -t lite dp -p $(PIPELINE) -n

reset: push dp sp up

在部分 Concourse 指令可以用 -n 來取消互動式的操作,像是 dpsp 會詢問使用者是否要刪除或者更新設定的類型,就可以用 -n 直接確認。
在我們做完 docker push 之後,往往會希望能上測試最新版的自訂資源,就可以利用 make reset 連續把 docker push destroy-pipeline set-pipeline unpause-pipeline 幾個動作完成,讓 Pipeline 馬上運作起來。

原始碼

至於原始碼方面,其實有非常多重複的部分,所以是可以抽出來變成 context.rbapi.rb 兩個檔案負責不同的部分。

以這次的範例來說 context.rb 內容會像這樣。

require 'json'

class Context
  attr_reader :major, :minior

  def initialize
    input = JSON.parse(STDIN.gets)
    @source = input["source"] || {}
    @version = input["version"] || {}
    @params = input["params"] || {}
    @version = @version["version"] || @params["version"] || "0.0"
    @major, @minior = @version.to_s.split(".")
    @dest = ARGV[0]
  end

  def uri
    @source["uri"]
  end

  def version
    "#{@major || 0}.#{@minior || 0}"
  end

  def write(filename, data)
    path = File.join(@dest, filename)
    File.write(path, data)
  end

  def read(filename)
    path = File.join(@dest, filename)
    File.read(path)
  end
end

我們把從 STDIN 存取資料的部分抽離出來,並且個別存取版本、參數等等,並且轉為一個物件。另一方面也將讀取跟寫入檔案的功能事先寫在 Context 物件內,讓我們可以隨時存取。

api.rb 部分則是專門處理 HTTP 請求的。

require 'net/http'
require 'json'
require 'date'

module API

  API_VERSION = '/v1'
  ENDPOINT_CHECK = '/versions'
  ENDPOINT_INPUT = '/monsters'
  ENDPOINT_OUTPUT = '/version'

  module_function
  def init(context)
    @uri ||= URI(context.uri)
    @context = context
  end

  def http
    return if @uri.nil?
    @http = Net::HTTP.new(@uri.host, @uri.port)
  end

  def build_query_from(context = @context)
    URI.encode_www_form(major: context.major, minior: context.minior)
  end

  def endpoint(path, context = @context)
    API_VERSION + path + "?" + build_query_from(context)
  end

  def check(context = @context, &block)
    response = http.send_request('GET', endpoint(ENDPOINT_CHECK, context))
    versions = if response.is_a?(Net::HTTPSuccess)
                 JSON.parse(response.body)
               else
                 []
               end
    yield versions if block_given?
  end

  def in(context = @context, &block)
    response = http.send_request('GET', endpoint(ENDPOINT_INPUT, context))
    monsters = if response.is_a?(Net::HTTPSuccess)
                 JSON.parse(response.body)
               else
                 []
               end
    yield monsters if block_given?
  end

  def out(context = @context, &block)
    response = http.send_request('PUT', endpoint(ENDPOINT_OUTPUT, context))
    result = if response.is_a?(Net::HTTPSuccess)
                   JSON.parse(response.body)
                 else
                   {}
                 end
    yield DateTime.parse(result["updated_at"] || Time.now.to_s) if block_given?
  end

end

預社會從剛剛建立的 Context 物件取得資料,並且透過 source 指定的 uri 參數設定好 HTTP 請求要詢問的主機名稱和埠號。

之後將 check in out 三個功能抽象化處理,並且對資料做好預先的處理。

之後 check 檔案就能簡化成這樣。

#!/usr/bin/env ruby

$LOAD_PATH.unshift File.expand_path(ENV['SOURCE_DIR'], __FILE__)

require 'context'
require 'api'

context = Context.new
output = []

API.init(context)
API.check do |versions|
  output = versions.map { |version| { "version" => version } }
end

STDOUT.puts output.to_json

這邊會利用 $LOAD_PATH 去讀取製作 Docker Image 所指定的 SOURCE_DIR 環境變數對應的目錄,因為我們並沒有把 context.rbapi.rb 複製過去,用一般的 require './api 是會失敗的。

inout 分別會長這樣。

#!/usr/bin/env ruby
$LOAD_PATH.unshift File.expand_path(ENV['SOURCE_DIR'], __FILE__)

require 'context'
require 'api'

context = Context.new
output = {
  "version" => { "version" => context.version },
  "metadata" => []
}

API.init(context)
API.in do |monsters|
  output["metadata"] << { "name" => "count", "value" => monsters.size.to_s }
  context.write("monsters.csv", monsters.map { |m| m.join(",") }.join("\r\n"))
end

STDOUT.puts output.to_json
#!/usr/bin/env ruby
$LOAD_PATH.unshift File.expand_path(ENV['SOURCE_DIR'], __FILE__)

require 'context'
require 'api'

context = Context.new
output = {
  "version" => {"version" => context.version },
  "metadata" => []
}

API.init(context)
API.out do |updated_at|
  output["metadata"] << { "name" => "updated_at", "value" => updated_at.strftime("%Y-%m-%d") }
end

STDOUT.puts output.to_json

相對原本的程式碼簡單很多,往後即使碰到要更改版本編號的規則,或者存取的網址等等,都只需要更新 api.rbcontext.rb 就可以了。

下一篇討論實際使用自訂資源來產生靜態頁面的方式。


上一篇
27 - 自訂資源 (7)
下一篇
29 - 自訂資源(9)
系列文
不一樣的 CI/CD 工具:Concourse 初探30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
小克
iT邦新手 4 級 ‧ 2023-02-02 12:25:07

針對 Makefile 的部分
我也寫了一篇分享文
請蒼時弦也大大指教
https://blog.goodjack.tw/2023/01/use-makefile-to-manage-workflows-for-web-projects.html

我覺得寫得蠻好的,很不錯的應用方式

我要留言

立即登入留言