雖然已經讓自訂資源可以實際動起來了,但是後續的維護也還是有困難的。為了改善這些問題,我們可以再多做一些處理來對應這些問題。
因為製作 Docker Image 常常需要非常多指令,所以大多數時候都會設置一個 Makefile 來輔助。像是這類型簡單的操作,利用 Makefile 都是很容易撰寫的。
一般筆者會把 build
和 push
設定到 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 就只需要用 make
跟 make push
即可。
有時候也會用
make run
來進入容器中實際測試指令是否正常運作。
經過幾次的實作,其實會發現 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
來取消互動式的操作,像是 dp
跟 sp
會詢問使用者是否要刪除或者更新設定的類型,就可以用 -n
直接確認。
在我們做完 docker push
之後,往往會希望能上測試最新版的自訂資源,就可以利用 make reset
連續把 docker push
destroy-pipeline
set-pipeline
unpause-pipeline
幾個動作完成,讓 Pipeline 馬上運作起來。
至於原始碼方面,其實有非常多重複的部分,所以是可以抽出來變成 context.rb
和 api.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.rb
和 api.rb
複製過去,用一般的 require './api
是會失敗的。
而 in
和 out
分別會長這樣。
#!/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.rb
和 context.rb
就可以了。
下一篇討論實際使用自訂資源來產生靜態頁面的方式。
針對 Makefile 的部分
我也寫了一篇分享文
請蒼時弦也大大指教
https://blog.goodjack.tw/2023/01/use-makefile-to-manage-workflows-for-web-projects.html
我覺得寫得蠻好的,很不錯的應用方式