目標
本篇文章我們將參照官方的測試實例來一步步添加一個Admission Webhook,從而修改POD的信息
配置
webhook證書生成
由上節的配置我們可以看出,我們的webhook必須使用https,所以我們需要生成一個自簽名的https證書,
ca可以使用自定義的也可以共用apiserver的。
腳本可以參照istio的證書生成腳本: webhook-create-signed-cert.sh
tmpdir=$(mktemp -d) #生成臨時目錄 service=sidecar-injector-webhook-svc #指定webhook對應的svc name namespace=default #指定 運行的命名空間 secret=sidecar-injector-webhook-certs # 指定secret name csrName=${service}.${namespace} #指定CertificateSigningRequest 的名稱 cat <> ${tmpdir}/csr.conf [req] req_extensions = v3_req distinguished_name = req_distinguished_name [req_distinguished_name] [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] DNS.1 = ${service} DNS.2 = ${service}.${namespace} DNS.3 = ${service}.${namespace}.svc EOF openssl genrsa -out server-key.pem 2048 #生成私鑰 openssl req -new -key ${tmpdir}/server-key.pem -subj "/CN=${service}.${namespace}.svc" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf # 生成證書籤名請求 通過k8s CSR生成證書 cat <認證apiserver配置
如果我們的webhook需要對請求進行身份認證,那麼我們需要對apiserver進行以下配置:
--admission-control-config-file=/etc/kubernetes/admission-control.conf/etc/kubernetes/admission-control.conf 文件內容如下:
apiVersion: apiserver.k8s.io/v1alpha1 kind: AdmissionConfiguration plugins: - name: MutatingAdmissionWebhook configuration: apiVersion: apiserver.config.k8s.io/v1alpha1 kind: WebhookAdmission kubeConfigFile: /etc/kubeconfig/mutating-admission-webhook.kubeconfig/etc/kubeconfig/mutating-admission-webhook.kubeconfig 配置如下:
apiVersion: v1 kind: Config users: - name: 'sidecar-injector-webhook-svc.default.svc' user: token: "testtoken"MutatingWebhookConfiguration 配置
apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: sidecar-injector-webhook-cfg labels: app: sidecar-injector webhooks: - name: sidecar-injector.morven.me clientConfig: service: name: sidecar-injector-webhook-svc namespace: default path: "/mutating-pods" rules: - operations: [ "CREATE" ] apiGroups: [""] apiVersions: ["v1"] resources: ["pods"] namespaceSelector: matchLabels: sidecar-injector: enabled代碼實現
接下來我們參照官方的測試實例實現一個 sidecar inject MutatingAdmissionWebhook。
package main import ( "crypto/tls" "encoding/json" "flag" "fmt" "io/ioutil" "net/http" "k8s.io/api/admission/v1beta1" admissionv1beta1 "k8s.io/api/admission/v1beta1" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/klog" // TODO: try this library to see if it generates correct json patch ) var scheme = runtime.NewScheme() //用於序列化 var codecs = serializer.NewCodecFactory(scheme) //提供檢索序列化方式的函數 const ( //返回給apiserver的patcher podsInjectContainerPatch string = `{"op":"add","path":"/spec/containers/-","value":[{"image":"envoy","name":"envoy","resources":{}}]}` addInjectAnnotationPatch string = `{"op":"add","path":"/metadata/annotations","value": {"sidecar-injector-webhook/status": "true"}}` updateInjectAnnotationPatch string = `{"op":"replace","path":"/metadata/annotations/sidecar-injector-webhook/status","value":"true"}` injectAnnotation string = "sidecar-injector-webhook/status" ) func init() { // 將數據類型註冊到scheme utilruntime.Must(corev1.AddToScheme(scheme)) utilruntime.Must(admissionv1beta1.AddToScheme(scheme)) utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme)) } // 創建AdmissionResponse func toAdmissionResponse(err error) *v1beta1.AdmissionResponse { return &v1beta1.AdmissionResponse{ Result: &metav1.Status{ Message: err.Error(), }, } } type admitFunc func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse // 使用對應http.handlefunc 處理請求 func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { var body []byte if r.Body != nil { if data, err := ioutil.ReadAll(r.Body); err == nil { body = data } } // 校驗token token:=r.Header.Get("Authorization") if token!=fmt.Sprintf("Bearer %s",bearToken) { klog.Errorf("unexpect bear token: %s", token) return } // 校驗content type 只接收json contentType := r.Header.Get("Content-Type") if contentType != "application/json" { klog.Errorf("contentType=%s, expect application/json", contentType) return } klog.V(2).Info(fmt.Sprintf("handling request: %s", body)) // 接收到的AdmissionReview requestedAdmissionReview := v1beta1.AdmissionReview{} // 用於返回的AdmissionReview responseAdmissionReview := v1beta1.AdmissionReview{} deserializer := codecs.UniversalDeserializer() //返回runtime.Decoder 用於轉換成為k8s的runtime.Object if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil { //將body轉換為AdmissionReview klog.Error(err) responseAdmissionReview.Response = toAdmissionResponse(err) //返回包含錯誤的AdmissionResponse } else { klog.Info("s1") responseAdmissionReview.Response = admit(requestedAdmissionReview) //通過對應handlefunc處理 } responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID //響應 UUID需要同請求UUID相同 klog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response)) respBytes, err := json.Marshal(responseAdmissionReview) //序列化為JSON if err != nil { klog.Error(err) } if _, err := w.Write(respBytes); err != nil { klog.Error(err) } } func serveMutatePods(w http.ResponseWriter, r *http.Request) { serve(w, r, mutatePods) } func main() { klog.InitFlags(nil) flag.Parse() http.HandleFunc("/mutating-pods", serveMutatePods) sCert, err := tls.LoadX509KeyPair("/etc/certs/cert.pem", "/etc/certs/key.pem") if err != nil { klog.Fatal(err) } server := &http.Server{ Addr: ":443", TLSConfig: &tls.Config{ Certificates: []tls.Certificate{sCert}, }, } server.ListenAndServeTLS("", "") } func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { klog.V(2).Info("mutating pods") podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} if ar.Request.Resource != podResource { //判斷類型是否相同 klog.Errorf("expect resource to be %s %s", podResource,ar.Request.Resource) return nil } raw := ar.Request.Object.Raw pod := corev1.Pod{} deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil { //反序列化為pod klog.Error(err) return toAdmissionResponse(err) } reviewResponse := v1beta1.AdmissionResponse{} reviewResponse.Allowed = true klog.Info(reviewResponse) annotations:=pod.Annotations if annotations==nil { annotations = map[string]string{} } if v,ok:=annotations[injectAnnotation];ok { // 沒有或不是true 進行注入 klog.Info(v) if v!="true" { reviewResponse.Patch = []byte(fmt.Sprintf("{%s,%s}",podsInjectContainerPatch,updateInjectAnnotationPatch)) //設置patcher pt := v1beta1.PatchTypeJSONPatch reviewResponse.PatchType = &pt } }else { reviewResponse.Patch = []byte(fmt.Sprintf("[%s,%s]",podsInjectContainerPatch,addInjectAnnotationPatch)) //設置patcher pt := v1beta1.PatchTypeJSONPatch reviewResponse.PatchType = &pt } klog.Info(string(reviewResponse.Patch)) return &reviewResponse }