"""Building blocks shared by the CKA task banks written for the per-task hosts (114 onwards): bash for setups and checks. The generic ones - NET, PODS, PY, CAN - are sim/bank.py's. The two rules of the CKS and CKAD banks hold here as well: a task scores nothing before anyone has worked on it, and what can be tested for real - a file on a node, a name resolved from a Pod, a request, a volume that gets provisioned - is tested for real rather than read off the object. """ from sim.bank import C, CAN, NET, NS, PODS, PY, ns_pick, name_pick # noqa: F401 (the banks import these from here) # one FILE: the file's content with blanks and line ends removed - for a one-word answer ONE = r""" one() { tr -d ' \t\n\r' < "$1" 2>/dev/null; } """ # Volumes and claims. STOR = ONE + r""" # pvnode PV: the node a PersistentVolume is tied to (its node affinity) pvnode() { j pv "$1" '{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}'; } # pvpath PV: its directory on that node (hostPath or local) pvpath() { local p; p=$(j pv "$1" '{.spec.hostPath.path}'); [ -n "$p" ] || p=$(j pv "$1" '{.spec.local.path}'); echo "$p"; } # bound NS CLAIM [SECONDS]: the claim is Bound, given a moment bound() { local i for i in $(seq 1 $(( ${3:-45} / 3 ))); do [ "$(j -n "$1" pvc "$2" '{.status.phase}')" = Bound ] && return 0; sleep 3; done return 1 } # running NS POD [SECONDS]: the Pod is Running, given a moment running() { local i for i in $(seq 1 $(( ${3:-60} / 3 ))); do [ "$(j -n "$1" pod "$2" '{.status.phase}')" = Running ] && return 0; sleep 3; done return 1 } # gone KIND NAME [-n NS]: the object does not exist (and the API server said so) gone() { local out; out=$($K get "$@" 2>&1); case "$out" in *NotFound*) return 0 ;; esac; return 1; } # newest NS SELECTOR: the name of the newest Running Pod the selector matches newest() { $K -n "$1" get pod -l "$2" --field-selector status.phase=Running --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].metadata.name}' 2>/dev/null; } """ # Name lookups from a Pod. DNS = r""" # look NS POD NAME: the address the name resolves to from that Pod ("" when it does not) look() { $K -n "$1" exec "$2" -- nslookup "$3" 2>/dev/null | awk '/^Name:/ {n=1} n && /^Address/ {print $NF; exit}'; } # resolves NS POD NAME ADDRESS: it resolves to that address, given a moment (CoreDNS reloads its # configuration by itself, but not at once) resolves() { local i; for i in $(seq 1 10); do [ "$(look "$1" "$2" "$3")" = "$4" ] && return 0; sleep 3; done; return 1; } """ # A small operator for Backup objects (group data.example.com): a controller that gives each # Backup a ConfigMap and marks it Scheduled. It is kubectl in a loop - the node's own kubectl, # mounted into a busybox container - which is enough to show the pattern: a CRD, RBAC for its # controller, and a Deployment that acts on it. With status_rbac=False the ClusterRole lacks # `backups/status`, and the controller can make the ConfigMaps but not set the phase. _BACKUP_CRD = r"""apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: backups.data.example.com spec: group: data.example.com scope: Namespaced names: {kind: Backup, plural: backups, singular: backup, shortNames: [bk]} versions: - name: v1 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object required: [schedule] properties: schedule: {type: string} retention: {type: integer, minimum: 1, default: 7} status: type: object properties: phase: {type: string} --- """ def backup_operator(crd=False, status_rbac=True): """The operator's manifests, as one YAML document stream (the CRD first, when asked for).""" status = ", backups/status" if status_rbac else "" return (_BACKUP_CRD if crd else "") + r"""apiVersion: v1 kind: Namespace metadata: name: backup-operator --- apiVersion: v1 kind: ServiceAccount metadata: name: controller namespace: backup-operator --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: backup-operator rules: - apiGroups: [data.example.com] resources: [backups""" + status + r"""] verbs: [get, list, watch, update, patch] - apiGroups: [""] resources: [configmaps] verbs: [get, list, create, update, patch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: backup-operator roleRef: {apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: backup-operator} subjects: - {kind: ServiceAccount, name: controller, namespace: backup-operator} --- apiVersion: apps/v1 kind: Deployment metadata: name: controller namespace: backup-operator spec: replicas: 1 selector: matchLabels: {app: backup-operator} template: metadata: labels: {app: backup-operator} spec: serviceAccountName: controller containers: - name: controller image: busybox:1.36 command: - sh - -c - | set -f while true; do for b in $(kubectl get backups -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}'); do ns=${b%/*}; name=${b#*/} sched=$(kubectl -n "$ns" get backup "$name" -o jsonpath='{.spec.schedule}') kubectl -n "$ns" create configmap "backup-$name" --from-literal=schedule="$sched" --dry-run=client -o yaml | kubectl apply -f - >/dev/null kubectl -n "$ns" patch backup "$name" --subresource=status --type=merge -p '{"status":{"phase":"Scheduled"}}' >/dev/null done sleep 5 done volumeMounts: - {name: kubectl, mountPath: /bin/kubectl, readOnly: true} volumes: - name: kubectl hostPath: {path: /usr/bin/kubectl, type: File} """