Kube Admission
External Admission Webhooks
作用及使用场景
当需要对某些api请求或者所有请求进行校验或者修改object的时候,可以考虑使用ValidatingAdmissionWebhook或者MutatingAdmissionWebhook,两者的区别:
- ValidatingAdmissionWebhook不允许在webhook中对Object进行修改,只是返回结果是true或false
- MutatingAdmissionWebhook运行在webhook中对Object进行修改
启用
在1.10之前的版本,需要使用 --admission-control
启用相关配置,并且是按配置的顺序来决定运行顺序,因此需要用户对于Admission Controllers 完全了解。
在1.10之后的版本,上述配置已经废弃,建议使用--enable-admission-plugins=MutatingAdmissionWebhook,ValidatingAdmissionWebhook
,并且用户指定的顺序并不会影响实际运行顺序,更加友好。
官方的Using Admission Controllers - Kubernetes有关于这方面的详细说明。
流程
kube-apiserver –> 认证鉴权 –> Admission Controller –> webhook
使用
创建admission服务,以供kube-apiserver调用
package main
import (
"crypto/tls"
"encoding/json"
"flag"
"io/ioutil"
"net/http"
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"
)
type patchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
func init() {
addToScheme(scheme)
}
func addToScheme(scheme *runtime.Scheme) {
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(admissionv1beta1.AddToScheme(scheme))
utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme))
}
func toAdmissionResponse(err error) *admissionv1beta1.AdmissionResponse {
return &admissionv1beta1.AdmissionResponse{
Result: &metav1.Status{
Message: err.Error(),
},
}
}
func updateAnnotation(target map[string]string, added map[string]string) (patch []patchOperation) {
if target == nil {
klog.Info(target)
target = map[string]string{}
}
for key, value := range added {
if target[key] == "" {
patch = append(patch, patchOperation{
Op: "add",
Path: "/metadata/annotations/" + key,
Value: value,
})
} else {
patch = append(patch, patchOperation{
Op: "replace",
Path: "/metadata/annotations/" + key,
Value: value,
})
}
}
return patch
}
func mutatePods(ar admissionv1beta1.AdmissionReview) *admissionv1beta1.AdmissionResponse {
klog.V(2).Info("Inject pods.")
podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
if ar.Request.Resource != podResource {
klog.Error("expect resource to be %s", podResource)
return nil
}
raw := ar.Request.Object.Raw
pod := corev1.Pod{}
if _, _, err := codecs.UniversalDeserializer().Decode(raw, nil, &pod); err != nil {
klog.Error(err)
return toAdmissionResponse(err)
}
reviewResponse := admissionv1beta1.AdmissionResponse{}
reviewResponse.Allowed = true
klog.V(3).Infof("Pod %s annotations: %v", pod.Name, pod.Annotations)
if pod.Annotations["injector.test.io/inject"] != "true" {
return &reviewResponse
}
var patch []patchOperation
// patch = append(patch, patchOperation{Op: "add", Path: "/metadata/annotations", Value: map[string]string{"injector.test.io/status": "success"}})
added := map[string]string{"injector.test.io.status": "success"}
patch = append(patch, updateAnnotation(pod.Annotations, added)...)
if b, err := json.Marshal(patch); err == nil {
pt := admissionv1beta1.PatchTypeJSONPatch
reviewResponse.PatchType = &pt
reviewResponse.Patch = b
}
return &reviewResponse
}
func injectHandler(w http.ResponseWriter, r *http.Request) {
var body []byte
if r.Body != nil {
if data, err := ioutil.ReadAll(r.Body); err == nil {
body = data
}
}
// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
klog.Errorf("contentType=%s, expect application/json", contentType)
return
}
requestedAdmissionReview := admissionv1beta1.AdmissionReview{}
responseAdmissionReview := admissionv1beta1.AdmissionReview{}
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil {
klog.Error(err)
responseAdmissionReview.Response = toAdmissionResponse(err)
} else {
responseAdmissionReview.Response = mutatePods(requestedAdmissionReview)
}
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
klog.V(1).Infof("sending response: %v", responseAdmissionReview.Response)
respBytes, err := json.Marshal(responseAdmissionReview)
if err != nil {
klog.Error(err)
}
if _, err := w.Write(respBytes); err != nil {
klog.Error(err)
}
}
func configTLS(certFile, keyFile string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: true,
}
return tlsConfig, nil
}
var (
certFile string
keyFile string
)
func main() {
klog.InitFlags(nil)
flag.Set("logtostderr", "true")
flag.StringVar(&certFile, "tls-cert-file", "", "File containing the default x509 Certificate for HTTPS.")
flag.StringVar(&keyFile, "tls-key-file", "", "File containing the default x509 private key matching --tls-cert-file.")
flag.Parse()
defer klog.Flush()
// cfg, err := configTLS(certFile, keyFile)
// if err != nil {
// klog.Fatalf("failed to tls config with %v", err)
// }
http.HandleFunc("/pods/inject", injectHandler)
srv := &http.Server{
Addr: ":9443",
TLSConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
klog.Info("Admission server starting...")
srv.ListenAndServeTLS(certFile, keyFile)
}
创建上述服务所使用的证书
- CA
[root@k8s-1 ~]$ cat ca-config.json
{
"signing": {
"default": {
"expiry": "43800h"
},
"profiles": {
"server": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth"
]
},
"client": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
}
}
}
}
[root@k8s-1 ~]$ cat ca-csr.json
{
"CN": "cloud.kubernetes",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "ZheJiang",
"L": "HangZhou",
"O": "k8s",
"OU": "cloud"
}
]
}
cfssl gencert -initca ca-config.json | cfssljson -bare ca
- Server
[root@k8s-1 ~]$ cat server.json
{
"CN": "cloud.server",
"hosts": [
"127.0.0.1",
"${IP}",
"*.cloud"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "China",
"L": "HangZhou",
"ST": "ZheJiang"
}
]
}
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server.json | cfssljson -bare server
- Client
[root@k8s-1 ~]$ cat client.json
{
"CN": "cloud.client",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "China",
"L": "HangZhou",
"ST": "ZheJiang"
}
]
}
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client cient.json | cfssljson -bare client
创建admission文件
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: test-webhook
namespace: kube-system
labels:
app: admission-webhook-injector
webhooks:
- name: sidecar-injector.test.io
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
clientConfig:
url: "https://${IP}:9443/pods/inject"
caBundle: <pem encoded ca cert that signs the server cert used by the webhook>
命令: cat ca.pem | base64 | tr -d '\n'
Testing
启动admission服务
./admission -tls-cert-file certs/server.pem -tls-key-file certs/server-key.pem -v 3
创建测试pod的yaml文件
apiVersion: v1
kind: Pod
metadata:
annotations:
injector.test.io/inject: "true"
labels:
app: testpod
name: testpod
spec:
containers:
- image: k8s.gcr.io/etcd
imagePullPolicy: IfNotPresent
name: etcd
kubectl apply -f pod.yaml
查看新生成的pod,发现annotations有"injector.test.io.status": "success"
,说明admission生效
Kubernetes 准入控制 Admission Controller 介绍 | Coder·码农网
深度剖析Kubernetes动态准入控制之Admission Webhooks - WaltonWang’s Blog - 开源中国