Dynamic Admission Controllers in OpenShift 4

Why This Tutorial ?

Who is it for ?

Where to Begin ?

What is a dynamic admission controller?

Dynamic Admission Controllers Type.

# oc api-resources -o name | grep -i webhook
mutatingwebhookconfigurations.admissionregistration.k8s.io
validatingwebhookconfigurations.admissionregistration.k8s.io

Building the Webhook

Initializing GO

$ mkdir imagepullpolicy 
$ export GOPATH="$(pwd)/imagepullpolicy/"
$ mkdir imagepullpolicy/{src,bin,pkg}$ mkdir imagepullpolicy/src/ippac/ $ cd imagepullpolicy/src/ippac/

GO Coding

package mainimport (
"fmt"
"crypto/tls"
"flag"
"net/http"
"os"
"os/signal"
"syscall"
// "github.com/golang/glog"
"context"
)
type myServerHandler struct {}var (
tlscert , tlskey string
)
func getEnv(key , fallback string) string {
value , exists := os.LookupEnv(key)
if !exists {
value = fallback
}
return value
}
func main() {

// chaeck the Environment variable for User Define Placements
certpem := getEnv("CERT_FILE", "/opt/app-root/tls/tls.crt")
keypem := getEnv("KEY_FILE", "/opt/app-root/tls/tls.key")
port := getEnv("PORT", "8443")
// Setting the variables for the TLS
flag.StringVar(&tlscert, "tlsCertFile", certpem , "The File Contains the X509 Certificate for HTTPS")
flag.StringVar(&tlskey, "tlsKeyFile", keypem , "The File Contains the X509 Private key")
flag.Parse()certs , err := tls.LoadX509KeyPair(tlscert, tlskey)if err != nil {
// glog.Errorf("Failed to load Certificate/Key Pair: %v", err)
fmt.Fprintf(os.Stderr, "Failed to load Certificate/Key Pair: %v", err);
}
// Setting the HTTP Server with TLS (HTTPS)
server := &http.Server {
Addr: fmt.Sprintf(":%v", port),
TLSConfig: &tls.Config{Certificates: []tls.Certificate{certs}},
}
// Setting 2 variable which are defined by an empty struct for each of the function depending on the URL path
// the http request is calling
// in our example we have 2 paths , one for the mutate and one for validate
mr := myServerHandler{}
gs := myServerHandler{}
mux := http.NewServeMux()
// Setting a function reference for the /mutate URL
mux.HandleFunc("/mutate", mr.mutserve)
// Setting a function reference for the /validate URL
mux.HandleFunc("/validate", gs.valserve)
server.Handler = mux
// Starting a new channel to start the Server with TLS configuration we provided when we defined the server
// variable
go func() {
if err := server.ListenAndServeTLS("",""); err != nil {
// Failed to Listen and Serve Web Hook Server
fmt.Fprintf(os.Stderr, "Failed to Listen and Serve Web Hook Server: %v", err);
}
}()
// The Server Is running on Port : 8080 by default
fmt.Fprintf(os.Stdout, "The Server Is running on Port : %s \n" , port)
// Next we are going to setup the single handling for our HTTP server by sending the right signals to the channelsignalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
<-signalChan
// Get Shutdown signal , sutting down the webhook Server gracefully...
fmt.Fprintf(os.Stdout, "Get Shutdown signal , sutting down the webhook Server gracefully...\n")
server.Shutdown(context.Background())
}
package mainimport (
"net/http"
"os"
"io/ioutil"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/api/core/v1"
"encoding/json"
"fmt"
)func (gs *myServerHandler) valserve(w http.ResponseWriter, r *http.Request) {if r.URL.Path != "/validate" {
fmt.Fprintf(os.Stderr, "Not a Valid URL Path\n")
http.Error(w, "Not A Valid URL Path", http.StatusBadRequest)
}
var Body []byte
if r.Body != nil {
if data , err := ioutil.ReadAll(r.Body); err == nil {
Body = data
} else {
fmt.Fprintf(os.Stderr, "Unable to Copy the Body\n")
}
}
if len(Body) == 0 {
fmt.Fprintf(os.Stderr, "Unable to retrieve Body from the WebHook\n")
http.Error(w, "Unable to retrieve Body from the API" , http.StatusBadRequest )
return
} else {
fmt.Fprintf(os.Stdout, "Body retrieved\n")
}
arRequest := &admissionv1.AdmissionReview{}if err := json.Unmarshal(Body, arRequest); err != nil {
fmt.Fprintf(os.Stderr, "unable to Unmarshal the Body Request\n")
http.Error(w, "unable to Unmarshal the Body Request" , http.StatusBadRequest)
return
}

// Making Sure we are not running on a system Namespace
if isKubeNamespace(arRequest.Request.Namespace) {
fmt.Fprintf(os.Stderr, "Unauthorized Namespace\n")
http.Error(w, "Unauthorized Namespace", http.StatusBadRequest)
}
// initial the POD values from the requestrow := arRequest.Request.Object.Raw
pod := corev1.Pod{}
if err := json.Unmarshal(row, &pod); err != nil {
fmt.Fprintf(os.Stderr, "Unable to Unmarshal the Pod Information\n")
http.Error(w, "Unable to Unmarshal the Pod Information", http.StatusBadRequest)
}
// Now we Are going to Start and build the Response
arResponse := admissionv1.AdmissionReview {
Response: &admissionv1.AdmissionResponse{
Result: &metav1.Status{Status: "Failure", Message: "Not All Images are set to \"Always pull image\" policy", Code: 401},
UID: arRequest.Request.UID,
Allowed: false,
},
}
// Let's take an array of all the containers
containers := pod.Spec.Containers
var pullPolicyFlag bool
pullPolicyFlag = true
for i , container := range containers {
fmt.Fprintf(os.Stdout, "container[%d]= %s imagePullPolicy=%s", i, container.Name , container.ImagePullPolicy)
if container.Name != "recycler-container" && containers[i].ImagePullPolicy != "Always" {
pullPolicyFlag = false
}
}
arResponse.APIVersion = "admission.k8s.io/v1"
arResponse.Kind = arRequest.Kind
if pullPolicyFlag == true {
arResponse.Response.Allowed = true
arResponse.Response.Result = &metav1.Status{Status: "Success",
Message: "All Images are set to \"Always pull image\" policy",
Code: 201}
}
resp , resp_err := json.Marshal(arResponse)if resp_err != nil {
fmt.Fprintf(os.Stderr, "Unable to Marshal the Request\n")
http.Error(w, "Unable to Marshal the Request", http.StatusBadRequest)
}
if _ , werr := w.Write(resp); werr != nil {
fmt.Fprintf(os.Stderr, "Unable to Write the Response\n")
http.Error(w, "Unable to Write Response", http.StatusBadRequest)
}
}
package mainimport (
"net/http"
"fmt"
"io/ioutil"
"encoding/json"
"os"
"strconv"
"k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
admissionv1 "k8s.io/api/admission/v1"
)
func isKubeNamespace(ns string) bool {
return ns == metav1.NamespacePublic || ns == metav1.NamespaceSystem
}// the struct the patch needs for each image
type patchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
func (mr *myServerHandler) mutserve(w http.ResponseWriter, r *http.Request) {var Body []byte
if r.Body != nil {
if data , err := ioutil.ReadAll(r.Body); err == nil {
Body = data
}
}
if len(Body) == 0 {
fmt.Fprintf(os.Stderr, "Unable to retrieve Body from API")
http.Error(w,"Empty Body", http.StatusBadRequest)
}
fmt.Fprintf(os.Stdout,"Received Request\n")if r.URL.Path != "/mutate" {
fmt.Fprintf(os.Stderr, "Not a Validate URL Path")
http.Error(w, "Not A Validate URL Path", http.StatusBadRequest)
}
// Read the Response from the Kubernetes API and place it in the RequestarRequest := &v1.AdmissionReview{}
if err := json.Unmarshal(Body, arRequest); err != nil {
fmt.Fprintf(os.Stderr, "Error Unmarsheling the Body request")
http.Error(w, "Error Unmarsheling the Body request", http.StatusBadRequest)
return
}
raw := arRequest.Request.Object.Raw
obj := corev1.Pod{}
if !isKubeNamespace(arRequest.Request.Namespace) {

if err := json.Unmarshal(raw, &obj); err != nil {
fmt.Fprintf(os.Stderr, "Error , unable to Deserializing Pod")
http.Error(w,"Error , unable to Deserializing Pod", http.StatusBadRequest)
return
}
} else {
fmt.Fprintf(os.Stderr, "Error , unauthorized Namespace")
http.Error(w,"Error , unauthorized Namespace", http.StatusBadRequest)
return
}
containers := obj.Spec.ContainersarResponse := v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: arRequest.Request.UID,
},
}
var patches []patchOperationfmt.Fprintf(os.Stdout, "Starting the Loop for containers\n")

// running a for loop on all the images in order to create the patch
for i , container := range containers {
fmt.Fprintf(os.Stdout, "container[%d] = %s imagePullPolicy = %s\n", i, container.Name , container.ImagePullPolicy)
if containers[i].ImagePullPolicy == "Never" || containers[i].ImagePullPolicy == "IfNotPresent" {

patches = append(patches, patchOperation{
Op: "replace",
Path: "/spec/containers/"+ strconv.Itoa(i) +"/imagePullPolicy",
Value: "Always",
})
}
}
fmt.Fprintf(os.Stdout, "the Json Is : \"%s\"\n", patches)patchBytes, err := json.Marshal(patches)if err != nil {
fmt.Fprintf(os.Stderr, "Can't encode Patches: %v", err)
http.Error(w, fmt.Sprintf("couldn't encode Patches: %v", err), http.StatusInternalServerError)
return
}
v1JSONPatch := admissionv1.PatchTypeJSONPatch
arResponse.APIVersion = "admission.k8s.io/v1"
arResponse.Kind = arRequest.Kind
arResponse.Response.Allowed = true
arResponse.Response.Patch = patchBytes
arResponse.Response.PatchType = &v1JSONPatch

resp, rErr := json.Marshal(arResponse)
if rErr != nil {
fmt.Fprintf(os.Stderr, "Can't encode response: %v", rErr)
http.Error(w, fmt.Sprintf("couldn't encode response: %v", rErr), http.StatusInternalServerError)
}
if _ , wErr := w.Write(resp); wErr != nil {
fmt.Fprintf(os.Stderr, "Can't write response: %v", wErr)
http.Error(w, fmt.Sprintf("cloud not write response: %v", wErr), http.StatusInternalServerError)
}
}
# go mod init ippac
# go mod vendor
# cd $GOPATH
FROM ubi8/go-toolset AS builderRUN mkdir -p /opt/app-root/src/ippac
WORKDIR /opt/app-root/src/ippac
ENV GOPATH=/opt/app-root/
ENV PATH="${PATH}:/opt/app-root/src/go/bin/"
COPY src/ippac .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ippacFROM ubi8/ubi-minimal
COPY --from=builder /opt/app-root/src/ippac/ippac /usr/bin/
USER 1001
EXPOSE 8080 8443CMD ["/usr/bin/ippac"]
ENTRYPOINT ["/usr/bin/ippac"]
# buildah bud -f Containerfile -t registry.example.com/library/ippac
# buildah push registry.example.com/library/ippac

Admission Controller Deployment

Service and Certificate

OpenShift Internal CA.

# cat << EOF | oc apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: kube-ippac
labels:
openshift.io/cluster-monitoring: "true"
EOF
# oc project kube-ippac

Service Deployment

# cat << EOF | oc apply -f -
apiVersion: v1
kind: Service
metadata:
name: ippac
namespace: kube-ippac
spec:
selector:
app: ippac
ports:
- protocol: TCP
port: 8443
targetPort: 8443
EOF
# oc annotate service ippac service.beta.openshift.io/serving-cert-secret-name=ippac-tls 

Pod Deployment

# cat << EOF | oc apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: ippac
namespace: kube-ippac
spec:
selector:
matchLabels:
app: ippac
replicas: 2
template:
metadata:
labels:
app: ippac
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- ippac
topologyKey: kubernetes.io/hostname
containers:
- name: ippac
image: registry.example.com/library/ippac:latest
imagePullPolicy: Always
ports:
- containerPort: 8443
env:
- name: CERT_FILE
value: '/opt/app-root/tls/tls.crt'
- name: KEY_FILE
value: '/opt/app-root/tls/tls.key'
- name: PORT
value: '8443'
volumeMounts:
- name: ippac-certs
mountPath: /opt/app-root/tls/
readOnly: true
volumes:
- name: ippac-certs
secret:
secretName: ippac-tls
EOF
# oc get pods -n kube-ippac -o wide

Validation / Mutation Resource

# cat << EOF | oc apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: "imagepullpolicy.il.redhat.io"
annotations:
service.beta.openshift.io/inject-cabundle: true
webhooks:
- name: "imagepullpolicy.il.redhat.io"
namespaceSelector:
matchExpressions:
- key: admission.il.redhat.io/imagePullPolicy
operator: In
values: ["True"]
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE","UPDATE"]
resources: ["pods"]
scope: "Namespaced"
clientConfig:
service:
namespace: "kube-ippac"
name: "ippac"
path: /validate
port: 8443
caBundle:
admissionReviewVersions: ["v1"]
sideEffects: None
timeoutSeconds: 5
EOF
# cat << EOF | oc apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: "imagepullpolicy.il.redhat.io"
annotations:
service.beta.openshift.io/inject-cabundle: true
webhooks:
- name: "imagepullpolicy.il.redhat.io"
reinvocationPolicy: IfNeeded
namespaceSelector:
matchExpressions:
- key: admission.il.redhat.io/imagePullPolicy
operator: In
values: ["True"]
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE","UPDATE"]
resources: ["pods"]
scope: "Namespaced"
clientConfig:
service:
namespace: "kube-ippac"
name: "ippac"
path: /mutate
port: 8443
caBundle:
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
EOF

Testing

# cat << EOF | oc apply -f - 
apiVersion: v1
kind: Namespace
metadata:
name: imagepullpolicy-test
labels:
admission.il.redhat.io/imagePullPolicy: "True"
EOF
# cat << EOF | oc apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: ippac-example
namespace: imagepullpolicy-test
spec:
selector:
matchLabels:
app: ippac-example
replicas: 1
template:
metadata:
labels:
app: ippac-example
spec:
containers:
- name: ippac-example
image: quay.io/ooichman/daemonize:latest
imagePullPolicy: Never
EOF

--

--

Open Source contributer for the past 15 years

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store