[TOC]
# Meme M109
## A CICD pipeline to deploy this artillery to GKE
### 我們希望每次 push code 的時候 CICD 幫我們做的事
1. 把專案包成 docker image 並上傳到 dockerhub
1. 生成 k8s deployment yaml,並在裡面選用 (1) 包好的 image
1. 用 kubectl apply (2) 生成的 deployment 文件,完成 deploy
### 專案準備 ( 可以用滑鼠點的部分 )
- 開好 Gitlab repo
- 開好 GCP project ( 並記下 project_id )
- Enable GKE API ( 點進 project 的 Kubernetes Engine 標籤就會自動 enable 了 )
### 事前作業
1. 安裝 gcloud CLI 在自己電腦上
[Google Cloud SDK 安裝方法](https://cloud.google.com/sdk/docs?hl=zh-tw#install_the_latest_cloud_tools_version_cloudsdk_current_version)
install 完之後如果```gcloud```指令不能用要 reload shell 設定 ( .bashrc, .zshrc, etc. )
```
source ~/.bashrc //也不一定是 .bashrc,總之就是 terminal rc file
```
1. 這個指令可以讓你用互動式的方式,初始化你的 gcloud sdk
```
gcloud init
```
:::info
等同於以下三個指令
```
gcloud auth login
gcloud config set project [project-id]
gcloud config set compute/zone [compute-zone]
```
:::
1. 開一個cluster
```
gcloud container clusters create [cluster-名稱] --num-nodes=[node 數量] ```
1. 讓 gcloud 自動幫你做 kubectl 設定,連接到 cluster
```
gcloud container clusters get-credentials [cluster-名稱]
```
1. 在 GCP 開一個服務帳戶 (sa-name: 6~30個字,小寫)
```
gcloud iam service-accounts create [sa-name] --description="[sa-description]" --display-name="[sa-display-name]"
```
1. 給他 deploy 到 k8s 的權限 (這個指令會讓剛才設好的服務帳戶成為 kubernetes developer)
```
gcloud projects add-iam-policy-binding [project-id] --member="serviceAccount:[sa-name]@[project-id].iam.gserviceaccount.com" --role='roles/container.developer'
```
1. 生成一組剛才的 service account 的 key,存成 service-account.json
```
gcloud iam service-accounts keys create --iam-account "[sa-name]@[project-id].iam.gserviceaccount.com" service-account.json
```
1. CICD 環境變數設定
### 環境變數設定
進入 gitlab repo > settings > CICD > Variables
type|key|value
--|--|--
Variable|DOCKER_HUB_USERNAME|dockerhub 帳號
Variable|DOCKER_HUB_PASSWORD|dockerhub 密碼
Variable|DOCKER_HUB_REGISTRY|https://index.docker.io/v1/
Variable|DOCKER_HUB_REPO|dockerhub repo
File|serviceAccountKey|前面生成的 json key

### 正式開始
#### 先在自己電腦上把 service 跟 ingress 架設好(一次性的不需要用到 CICD)
```
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml
```
#### 生成 kubeconfig 供 CICD 使用
1. 把取得 cluster 的資訊用的 command 存成變數,以便接下來的工作
``` GET_CMD="gcloud container clusters describe [cluster 名稱] --zone=[cluster 地區]" ```
2. 以下指令會以現在用的 cluster 資訊生成一個 kubeconfig.yaml 的設定檔
``` bash=
cat > kubeconfig.yaml <<EOF
apiVersion: v1
kind: Config
current-context: my-cluster
contexts: [{name: my-cluster, context: {cluster: cluster-1, user: user-1}}]
users: [{name: user-1, user: {auth-provider: {name: gcp}}}]
clusters:
- name: cluster-1
cluster:
server: "https://$(eval "$GET_CMD --format='value(endpoint)'")"
certificate-authority-data: "$(eval "$GET_CMD --format='value(masterAuth.clusterCaCertificate)'")"
EOF
```
:::info
這個檔案裡面的資訊只是用來把 kubectl 指向 cluster,光是這樣還不能夠用 kubectl 來操作你的 GKE cluster,所以可以放心把它放在 repo 裡。
我有過這個疑慮,所以把他 decode 出來,確定是 public key。

:::
### gitlab-ci.yaml
#### 這個檔案分兩個階段
1. 包成 docker image push 到 dockerhub
2. 把 docker image deploy 到 GKE
### docker-hub
- before script:用設定裡的環境變數登入 dockerhub
- script:build & push
### k8s-deploy
- before script:安裝 kubectl, 把 service account key 跟 kubeconfig 呼叫出來
- script:生成 deploy.yaml 然後 apply
這樣就可以自動 deploy 到 GKE 了。
## Cucumber BDD
### BDD & TDD ?
#### TDD (Test-Driven Development)
先寫測試,當通過所有測試即開發完成。
#### BDD (Behaviour-Driven Development)
先設定用戶行為的場景,以滿足用戶行為撰寫測試,當滿足所有用戶行為即開發完成。
### Cucumber 是什麼?
#### cucumber .feature檔範例 (支援中文)
```
#language: zh-TW
功能: 根據輸入文字查找圖片路徑
場景大綱: 使用者輸入文字
當 使用者輸入 "<文字>"
那麼 查找對應的圖片 "<路徑>"
例子:
| 文字 | 路徑 |
| 王金平 | 王金平.png |
| 賴清德 | 賴清德.jpg |
| 習近平 | 習近平.jpg |
| 韓冰 | 韓冰.jpg |
| 馬英九 | 馬英九.jpg |
場景: 圖庫中 有 對應的迷因
假如 資料庫中有對應的路徑
那麼 根據路徑產生圖片
場景: 圖庫中 沒有 對應的迷因
假如 資料庫中 沒有 對應的路徑
那麼 告訴使用者沒有資料
```
:::success
以淺顯易懂的語言撰寫須滿足的用戶行為場景,有利於與非資訊人員的溝通(PM 或客戶等等)。
並且.feature檔是可執行的測試檔案,當通過所有的用戶行為測試,專案中的功能即開發完成。
:::
#### cucumber 常用中英文關鍵字對照
| 英文關鍵字 | 中文關鍵字 |
| ---------------- | ---------- |
| feature | Text |
| scenario | 場景 |
| scenario_outline | 場景大綱 |
| examples | 例子 |
| given | * , 假如 |
| when | * , 當 |
| then | * , 那麼 |
| and | * , 而且 |
| but | * , 但是 |
:::info
有 * 部份代表使用關鍵字只是為了增加可閱讀性,也可以都寫 * 。
:::
### 使用 Cucumber 實做 BDD
#### 確認 Node.js 已安裝
```
node -v
npm -v
```
#### 進入當前專案目錄
```
mkdir meme
cd meme
```
#### npm 初始設定
```
// --yes 使用 deafult 設定
npm init --yes
```
#### 安裝 cucumber
```
sudo npm install cucumber --save -dev
```
#### 將 package.json 中的 "test" 更改為 "cucumber-js"
```json=
{
"name": "hellocucumber",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "cucumber-js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cucumber": "^5.0.3"
}
}
```
#### 準備文件結構
```
mkdir -p features/step_definitions
```
#### 在專案根目錄中新增 ```cucumber.js```,寫入以下內容
```javascript=
module.exports = {
default: `--format-options '{"snippetInterface": "synchronous"}'`
}
```
#### 新增 ```features/step_definitions/stepdefs.js```文件,寫入以下內容
```javascript=
const assert = require('assert'); // js 用來報錯的模組
const { Given, When, Then } = require('cucumber');
```
#### 目前文件結構

:::info
資料夾名稱根據 cucumber 的預設,檔名自定義。
這樣我們就有了一個最簡單的cucumber 專案。
:::
#### 驗證各元件是否運作正常
```
// 在專案根目錄中執行
npm test
```

> Cucumber 告訴我們它沒有發現能運行的場景。
#### 開始撰寫用戶行為的場景(```.feature```)
以我們的範例來說,當用戶輸入文字,必須產生相對應的圖片。
#### 新增 ```features/meme.feature``` 空文件,預備用來撰寫場景
```gherkin=
#language: zh-TW
功能: 根據輸入文字查找圖片路徑
場景: 使用者輸入文字
當 使用者輸入 "文字"
那麼 查找對應的圖片 "路徑"
```
:::info
功能(Feature)的部份,代表我們目前需要滿足的用戶行為。
場景(Scenario)的部份,代表我們目前假設的操作。
當(When)以及 那麼(Then),則是我們目前要測試的步驟(Step)。
" " 雙引號中的內容為變數,前面要空格。
:::
#### 再執行一次測試 ```npm test```
會得到以下結果

cucumber 告訴我們目前的兩個 Step 皆為 Undefined 的狀態,並提示我們該如何撰寫。
:::success
複製提示的程式碼片段,在```features/step_definitions/stepdefs.js``` 中貼上。
:::

#### 我們再執行一次```npm test``` 看看有什麼不同

前次的 ```UU``` 變為 ```P-```,表示 cucumber 找到我們定義的 step 並執行,第一個 step 是 pending 的狀態,而第二個則是被跳過。
#### 下個步驟為撰寫 step 的內容
```javasript=
const assert = require('assert'); // js 用來報錯的模組
const { Given, When, Then } = require('cucumber');
// 建立圖片路徑資料庫
let imgData = new Map();
imgData.set("王金平", "王金平.png")
imgData.set("賴清德", "賴清德.jpg")
imgData.set("習近平", "習近平.jpg")
imgData.set("韓冰", "韓冰.jpg")
imgData.set("馬英九", "馬英九.jpg")
// 根據輸入文字查找檔案名稱
function findImg(text) {
return imgData.get(text);
}
// scenario #1
Given('使用者輸入 {string}', function (inputText) {
this.text = inputText;
});
Then('查找對應的圖片 {string}', function (expectedPath) {
// 實際上我們的程式找到的檔名
this.actualPath = findImg(this.text);
// 如果與預期不相符則報錯
assert.equal(this.actualPath, expectedPath);
});
```
#### 在執行一次測試 ```npm test```

兩個 step 目前是無法連動的,可以看到 ```undefined == '路徑'```, step2 沒有正確拿到 step1 中的 "文字",而且 cucumber 還不知道什麼應該是預期的 output。
#### 加入 input 與 output 對應的表格
```gherkin=
#language: zh-TW
功能: 根據輸入文字查找圖片路徑
場景大綱: 使用者輸入文字
當 使用者輸入 "<文字>"
那麼 查找對應的圖片 "<路徑>"
例子:
| 文字 | 路徑 |
| 王金平 | 王金平.png |
| 賴清德 | 賴清德.jpg |
| 習近平 | 習近平.jpg |
| 韓冰 | 韓冰.jpg |
| 馬英九 | 馬英九.jpg |
```
:::warning
"場景"改為"場景大綱",兩個 step 才會連動。
變數的部份加上<>,才能對應到表格。
:::
#### 再測試 ```npm test```

一個 scenario,兩個 step,測試五組資料都通過。
## 實做產品 html,加入通過測試的程式碼
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="imgData.js"></script>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src=" ../index.bundle.js "></script>
</head>
<body style="background-color:pink">
<div style="text-align: center; padding-top: 64px;">
<input id="text" type="text" value="" size="15" placeholder="輸入文字" style="font-size: 48px;">
<button onclick="generateImg()">出來吧</button>
<div>
<img id="img" src="https://i.imgur.com/f5cWWxT.png" style="max-height: 600px; max-width: 800px; padding-top: 32px;">
</div>
</div>
<script>
function generateImg() {
// get input text
text = document.getElementById("text").value;
// then find the meme's path in imgData
path = imgData.get(text);
console.log(path);
// generate image in html
document.getElementById("img").src = path;
}
</script>
</body>
</html>
```
### ```git push``` 後自動 Test, Build 以及 Deploy


:::success
大功告成,往後更新時,都將會自動的測試以及發布。
:::