使用Patch方式更新K8S的 API Objects 一共有三種方式:strategic merge patch
, json-patch
,json merge patch
。關於這三種方式的文字描述區別可看官方文檔update-api-object-kubectl-patch。
我在本文中主要會介紹使用client-go的Patch方式,主要包括strategic merge patch
和json-patch
。不介紹json merge patch
的原因,是該方式使用場景比較少,因此不做介紹,如果有同學有興趣,可做補充。
StrategicMergePatch
新增Object值
本次示例以給一個node新增一個labels爲例,直接上代碼:
//根據Pod Sn 更新 pod
func UpdatePodByPodSn(coreV1 v1.CoreV1Interface, podSn string, patchData map[string]interface{}) (*apiv1.Pod, error) {
v1Pod, err := coreV1.Pods("").Get(podSn, metav1.GetOptions{})
if err != nil {
logs.Error("[UpdatePodByPodSn] get pod %v fail %v", podSn, err)
return nil, fmt.Errorf("[UpdatePodByPodSn] get pod %v fail %v", podSn, err)
}
namespace := v1Pod.Namespace
podName := v1Pod.Name
playLoadBytes, _ := json.Marshal(patchData)
newV1Pod, err := coreV1.Pods(namespace).Patch(podName, types.StrategicMergePatchType, playLoadBytes)
if err != nil {
logs.Error("[UpdatePodByPodSn] %v pod Patch fail %v", podName, err)
return nil, fmt.Errorf("[UpdatePodByPodSn] %v pod Patch fail %v", podName, err)
}
return newV1Pod, nil
}
注意:上面的PatchData 必須是以
{"metadata":...}
的go struct, 如:`map[string]interface{}{"metadata": map[string]map[string]string{"labels": {"test2": "test2", }}}`
對應單元測試用例
func pod(podName string, nodeName string, labels map[string]string, annotations map[string]string) *v1.Pod {
return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: podName, Labels: labels, Annotations: annotations}, Spec: v1.PodSpec{NodeName: nodeName}, Status: v1.PodStatus{}}
}
func TestUpdatePodByPodSn(t *testing.T) {
var tests = []struct {
expectedError interface{}
expectedAnnotation string
expectedLabel string
podSn string
patchData map[string]interface{}
v1Pod []runtime.Object
}{
{nil, "test2", "", "1.1.1.1", map[string]interface{}{"metadata": map[string]map[string]string{"annotations": {
"test2": "test2",
}}},
[]runtime.Object{pod("1.1.1.1", "1.1.1.1", map[string]string{"test1": "test1"}, map[string]string{"test1": "test1"})},
},
{nil, "", "", "1.1.1.2", map[string]interface{}{"metadata": map[string]map[string]string{"labels": {
"test2": "",
}}},
[]runtime.Object{pod("1.1.1.2", "1.1.1.1", map[string]string{"test1": "test1"}, map[string]string{"test1": "test1"})},
},
{nil, "", "test2", "1.1.1.3", map[string]interface{}{"metadata": map[string]map[string]string{"labels": {
"test2": "test2",
}}},
[]runtime.Object{pod("1.1.1.3", "1.1.1.1", map[string]string{"test1": "test1"}, map[string]string{"test1": "test1"})},
},
}
for _, test := range tests {
client := fake.NewSimpleClientset(test.v1Pod...)
v1Pod, err := UpdatePodByPodSn(client.CoreV1(), test.podSn, test.patchData)
if err != nil {
t.Errorf("expected error %s, got %s", test.expectedError, err)
}
assert.Equal(t, v1Pod.Annotations["test2"], test.expectedAnnotation)
assert.Equal(t, v1Pod.Labels["test2"], test.expectedLabel)
}
}
修改Object的值
修改Obejct的值使用方式如下,當使用strategic merge patch
的時候,如果提交的數據中鍵已經存在,那就會使用新提交的值替換原先的數據。依舊以修改labels的值爲例。
如新提交的數據爲:
{
"metadata":{
"labels":{
"test2":"test3",
},
}
}
Node中已經存在的labels爲:
{
"metadata":{
"labels":{
"test2":"test1",
},
}
}
最終Node中labels的key爲test2
的值會被替換成 test3
。
刪除Object值
當需要把某個Object的值刪除的時候,當使用strategic merge patch
的時候,依舊是刪除labels爲例提交方式是:
golang裏面的表現形式是:
{
"metadata":{
"labels":{
"test2":nil
},
}
}
對應從瀏覽器提交的數據是:
{
"metadata":{
"labels":{
"test2":null
},
}
}
PS:如果不喜歡使用上面struct的方式組成數據,可以使用如下的方式 labelsPatch := fmt.Sprintf(
{"metadata":{"labels":{"%s":"%s"}}}
, labelkey, labelvalue) 直接代替上面示例中的patchData
JSONPatch
JSONPatch的詳細說明請參考文檔:http://jsonpatch.com/。
JSONPatch 主要有三種操作方式:add
,replace
,remove
。以下會以代碼示例說明這三種操作在Client-go對應的代碼示例來說明怎樣操作K8s 的資源。
使用JSONPatch,如果Patch中帶有斜槓“/”和 (~)這兩個字符,不能直接傳入這兩個字符,需要你輸入的時候就人工轉換下,
/
轉換成~1
,~
轉換成~0
。以新增labels爲例,如我要新增一個"test1/test2":"test3"
的labels,可以把要傳入的數據修改爲"test1~1test2":"test3"
即可。
Op:add
使用JSONPatch
的方式新增一個標籤,其提交的數據格式必須是[{ "op": "replace", "path": "/baz", "value": "boo" }]
這樣的。代碼如下:
//patchStringValue specifies a patch operation for a string.
type PatchStringValue struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value"`
}
type PatchNodeParam struct {
coreV1 v1.CoreV1Interface
NodeSn string `json:"nodeSn"`
OperatorType string `json:"operator_type"`
OperatorPath string `json:"operator_path"`
OperatorData map[string]interface{} `json:"operator_data"`
}
//patch node info, example label, annotation
func patchNode(param PatchNodeParam) (*apiv1.Node, error) {
coreV1 := param.coreV1
nodeSn := param.NodeSn
node, err := coreV1.Nodes().Get(nodeSn, metav1.GetOptions{})
if err != nil {
return nil, err
}
operatorData := param.OperatorData
operatorType := param.OperatorType
operatorPath := param.OperatorPath
var payloads []interface{}
for key, value := range operatorData {
payload := PatchStringValue{
Op: operatorType,
Path: operatorPath + key,
Value: value,
}
payloads = append(payloads, payload)
}
payloadBytes, _ := json.Marshal(payloads)
newNode, err := coreV1.Nodes().Patch(nodeSn, types.JSONPatchType, payloadBytes)
if err != nil {
return nil, err
}
return newNode, err
}
單元測試:
func TestPatchNode(t *testing.T) {
Convey("test patchNode", t, func() {
Convey("Patch Node fail", func() {
var tests = []struct {
nodeSn string
operatorType string
operatorPath string
operatorData map[string]interface{}
expectedError interface{}
expectedValue *v1.Node
objs []runtime.Object
}{
{"1.1.1.1", "add", "/metadata/labels/",
map[string]interface{}{
"test1": "test1",
"test2": "test2"},
"nodes \"1.1.1.1\" not found", nil, nil},
{"1.1.1.1", "aaa", "/metadata/labels/",
map[string]interface{}{
"test1": "test1",
"test2": "test2"},
"Unexpected kind: aaa", nil, []runtime.Object{node("1.1.1.1", nil, nil)}},
}
for _, test := range tests {
client := fake.NewSimpleClientset(test.objs...)
param := PatchNodeParam{
coreV1: client.CoreV1(),
NodeSn: test.nodeSn,
OperatorType: test.operatorType,
OperatorPath: test.operatorPath,
OperatorData: test.operatorData,
EmpId: test.empId,
}
output, err := patchNode(param)
So(output, ShouldEqual, test.expectedValue)
So(err.Error(), ShouldEqual, test.expectedError)
}
})
Convey("Patch Node success", func() {
var tests = []struct {
nodeSn string
operatorType string
operatorPath string
operatorData map[string]interface{}
expectedError interface{}
expectedValue string
objs []runtime.Object
}{
{"1.1.1.1", "add", "/metadata/labels/",
map[string]interface{}{
"test1": "test1",
"test2": "test2"},
nil, "1.1.1.1", []runtime.Object{node("1.1.1.1", map[string]string{"test3": "test3"}, map[string]string{"test3": "test3"})}},
{"1.1.1.1", "add", "/metadata/labels/",
map[string]interface{}{
"test1": "test1",
"test2": "test2"},
nil, "1.1.1.1", []runtime.Object{node("1.1.1.1", map[string]string{"test1": "modifytest"}, map[string]string{"test1": "modifytest"})}},
}
for _, test := range tests {
client := fake.NewSimpleClientset(test.objs...)
param := PatchNodeParam{
coreV1: client.CoreV1(),
NodeSn: test.nodeSn,
OperatorType: test.operatorType,
OperatorPath: test.operatorPath,
OperatorData: test.operatorData,
}
output, err := patchNode(param)
So(output, ShouldNotBeNil)
So(err, ShouldBeNil)
So(output.Name, ShouldEqual, test.expectedValue)
}
})
})
}
使用add有個需要注意的地方就是,當你的Path是使用的
/metadata/labels
而不是/metadata/labels/labelkey
的時候,那你這個add
操作實際是對整個labels
進行替換,而不是新增,一定要注意避免踩坑。
PS:如果不喜歡使用上面struct的方式組成數據,可以使用如下的方式 labelsPatch := fmt.Sprintf([{"op":"add","path":"/metadata/labels/%s","value":"%s" }]
, labelkey, labelvalue) 直接代替上面示例中的patchData
Op:remove
要刪除一個標籤的話,代碼和增加區別不大,唯一的區別就是提交的數據要由鍵值對修改爲提交一個string slice類型[]string
,代碼如下:
type PatchNodeParam struct {
coreV1 v1.CoreV1Interface
NodeSn string `json:"nodeSn"`
OperatorType string `json:"operator_type"`
OperatorPath string `json:"operator_path"`
OperatorData map[string]interface{} `json:"operator_data"`
}
//patchStringValue specifies a remove operation for a string.
type RemoveStringValue struct {
Op string `json:"op"`
Path string `json:"path"`
}
//remove node info, example label, annotation
func removeNodeInfo(param RemoveNodeInfoParam) (*apiv1.Node, error) {
coreV1 := param.coreV1
nodeSn := param.NodeSn
node, err := coreV1.Nodes().Get(nodeSn, metav1.GetOptions{})
if err != nil {
return nil, err
}
operatorKey := param.OperatorKey
operatorType := param.OperatorType
operatorPath := param.OperatorPath
var payloads []interface{}
for key := range operatorKey {
payload := RemoveStringValue{
Op: operatorType,
Path: operatorPath + operatorKey[key],
}
payloads = append(payloads, payload)
}
payloadBytes, _ := json.Marshal(payloads)
newNode, err := coreV1.Nodes().Patch(nodeSn, types.JSONPatchType, payloadBytes)
if err != nil {
return nil, err
}
return newNode, err
}
Op:replace
replace
操作,會對整個的Object進行替換。所以使用replace
記住要把原始的數據取出來和你要新增的數據合併後再提交,如:
type ReplaceNodeInfoParam struct {
coreV1 v1.CoreV1Interface
NodeSn string `json:"nodeSn"`
OperatorType string `json:"operator_type"`
OperatorPath string `json:"operator_path"`
OperatorData map[string]interface{} `json:"operator_data"`
DataType string `json:"data_type"`
}
//patchStringValue specifies a patch operation for a string.
type PatchStringValue struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value"`
}
func replaceNodeInfo(param ReplaceNodeInfoParam) (*apiv1.Node, error) {
coreV1 := param.coreV1
nodeSn := param.NodeSn
node, err := coreV1.Nodes().Get(nodeSn, metav1.GetOptions{})
if err != nil {
return nil, err
}
var originOperatorData map[string]string
dataType := param.DataType
operatorData := param.OperatorData
operatorType := param.OperatorType
operatorPath := param.OperatorPath
switch dataType {
case "labels":
originOperatorData = node.Labels
case "annotations":
originOperatorData = node.Annotations
default:
originOperatorData = nil
}
if originOperatorData == nil {
return nil, fmt.Errorf("[replaceNodeInfo] fail, %v originOperatorData is nil", nodeSn)
}
for key, value := range originOperatorData {
operatorData[key] = value
}
var payloads []interface{}
payload := PatchStringValue{
Op: operatorType,
Path: operatorPath,
Value: operatorData,
}
payloads = append(payloads, payload)
payloadBytes, _ := json.Marshal(payloads)
newNode, err := coreV1.Nodes().Patch(nodeSn, types.JSONPatchType, payloadBytes)
if err != nil {
return nil, err
}
return newNode, err
}
單元測試
func TestReplaceNodeInfo(t *testing.T) {
Convey("test ReplaceNodeInfo", t, func() {
Convey("Patch ReplaceNodeInfo fail", func() {
var tests = []struct {
nodeSn string
operatorType string
operatorPath string
dataType string
operatorData map[string]interface{}
expectedError interface{}
expectedValue *v1.Node
objs []runtime.Object
}{
{"1.1.1.1", "add", "/metadata/labels", "labels",
map[string]interface{}{
"test1": "test1",
"test2": "test2"},
"nodes \"1.1.1.1\" not found", nil, nil},
{"1.1.1.1", "aaa", "/metadata/annotations", "annotations",
map[string]interface{}{
"test1": "test1",
"test2": "test2"},
"[replaceNodeInfo] fail, 1.1.1.1 originOperatorData is nil", nil, []runtime.Object{node("1.1.1.1", nil, nil)}},
}
for _, test := range tests {
client := fake.NewSimpleClientset(test.objs...)
param := ReplaceNodeInfoParam{
coreV1: client.CoreV1(),
NodeSn: test.nodeSn,
OperatorType: test.operatorType,
OperatorPath: test.operatorPath,
OperatorData: test.operatorData,
DataType: test.dataType,
}
output, err := replaceNodeInfo(param)
So(output, ShouldEqual, test.expectedValue)
So(err.Error(), ShouldEqual, test.expectedError)
}
})
Convey("Patch Node success", func() {
var tests = []struct {
nodeSn string
operatorType string
operatorPath string
dataType string
operatorData map[string]interface{}
expectedError interface{}
expectedLabel string
expectedAnnotation string
objs []runtime.Object
}{
{"1.1.1.1", "replace", "/metadata/labels", "labels",
map[string]interface{}{
"test1": "test1",
"test2": "test2"},
nil, "test3", "", []runtime.Object{node("1.1.1.1", map[string]string{"test3": "test3"}, map[string]string{"test3": "test3"})}},
{"1.1.1.1", "replace", "/metadata/annotations", "annotations",
map[string]interface{}{
"test1": "test1",
"test2": "test2"},
nil, "", "modifytest", []runtime.Object{node("1.1.1.1", map[string]string{"test1": "modifytest"}, map[string]string{"test1": "modifytest"})}},
}
for _, test := range tests {
client := fake.NewSimpleClientset(test.objs...)
param := ReplaceNodeInfoParam{
coreV1: client.CoreV1(),
NodeSn: test.nodeSn,
OperatorType: test.operatorType,
OperatorPath: test.operatorPath,
OperatorData: test.operatorData,
DataType: test.dataType,
}
output, err := replaceNodeInfo(param)
So(output, ShouldNotBeNil)
So(err, ShouldBeNil)
So(output.Labels["test3"], ShouldEqual, test.expectedLabel)
So(output.Annotations["test1"], ShouldEqual, test.expectedAnnotation)
}
})
})
}
PS:如各位還有其他更好的方式,歡迎交流補充。