diff --git a/3-security-context/Readme.md b/3-security-context/Readme.md index 1bfa43f..ec970e2 100644 --- a/3-security-context/Readme.md +++ b/3-security-context/Readme.md @@ -5,11 +5,14 @@ You can choose between the interactive demo: ```bash +cd demo ./interactive-demo.sh ``` +Note that the demo requires [bat](https://github.com/sharkdp/bat) for pretty printing the files. -And a manual demo. To only print the commands just run: +Alternatively, you could print a transcript of the demo for doing the demo manually ```bash +cd demo PRINT_ONLY=true ./interactive-demo.sh ``` \ No newline at end of file diff --git a/3-security-context/create-clusters.sh b/3-security-context/create-clusters.sh index 36a4351..1d832f3 100755 --- a/3-security-context/create-clusters.sh +++ b/3-security-context/create-clusters.sh @@ -3,6 +3,7 @@ set -o errexit -o nounset -o pipefail BASEDIR=$(dirname $0) ABSOLUTE_BASEDIR="$( cd ${BASEDIR} && pwd )" +PSPDIR=${ABSOLUTE_BASEDIR}/../4-pod-security-policies/demo source ${ABSOLUTE_BASEDIR}/../config.sh source ${ABSOLUTE_BASEDIR}/../utils.sh @@ -10,17 +11,17 @@ source ${ABSOLUTE_BASEDIR}/../utils.sh function main() { - createCluster "${CLUSTER3}" "2" "--enable-pod-security-policy --enable-network-policy" + createCluster "${CLUSTER3}" "2" "--enable-pod-security-policy --enable-network-policy " # Become cluster admin, so we are authorized to create role for PSP becomeClusterAdmin # Make sure we're in a namespace that does not have any netpols - kubectl create namespace wild-west + kubectlIdempotent create namespace wild-west kubectl config set-context $(kubectl config current-context) --namespace=wild-west # Start with a privileged PSP. Makes sure deployments are allowed to create pods - kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/psp-privileged.yaml + kubectl apply -f ${PSPDIR}/psp-privileged.yaml kubectl create role psp:privileged \ --verb=use \ --resource=podsecuritypolicy \ @@ -30,11 +31,18 @@ function main() { --serviceaccount=wild-west:default kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/02-deployment-run-as-non-root-unprivileged.yaml - kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/04-deployment-allow-no-privilege-escalation.yaml - kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/05-deployment-read-only-fs.yaml - kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/06-deployment-nginx-read-only-fs.yaml - kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/07-deployment-nginx-read-only-fs-empty-dirs.yaml - kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/11-statefulset.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/03-deployment-run-as-user-unprivileged.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/05-deployment-allow-no-privilege-escalation.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/06-deployment-seccomp.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/07-deployment-run-without-caps.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/08-deployment-run-with-certain-caps.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/09-deployment-run-without-caps-unprivileged.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/10-deployment-read-only-fs.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/11-deployment-nginx-read-only-fs.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/12-deployment-nginx-read-only-fs-empty-dirs.yaml + kubectl apply -f ${ABSOLUTE_BASEDIR}/demo/13-deployment-all-at-once.yaml + + kubectl apply -f ${PSPDIR}/11-statefulset.yaml } main "$@" diff --git a/3-security-context/demo/00-deployment-nginx.sh b/3-security-context/demo/00-deployment-nginx.sh deleted file mode 100644 index c80398b..0000000 --- a/3-security-context/demo/00-deployment-nginx.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -kubectl create deployment nginx --image nginx:1.17.2 \ No newline at end of file diff --git a/3-security-context/demo/03-deployment-docker-sudo.sh b/3-security-context/demo/03-deployment-docker-sudo.sh deleted file mode 100644 index 6f006e8..0000000 --- a/3-security-context/demo/03-deployment-docker-sudo.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -kubectl run docker-sudo --image schnatterer/docker-sudo:0.1 \ No newline at end of file diff --git a/3-security-context/demo/03-deployment-run-as-user-unprivileged.yaml b/3-security-context/demo/03-deployment-run-as-user-unprivileged.yaml new file mode 100644 index 0000000..1ecefff --- /dev/null +++ b/3-security-context/demo/03-deployment-run-as-user-unprivileged.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: run-as-user-unprivileged + name: run-as-user-unprivileged +spec: + selector: + matchLabels: + run: run-as-user-unprivileged + strategy: + type: Recreate + template: + metadata: + labels: + run: run-as-user-unprivileged + spec: + containers: + - image: nginxinc/nginx-unprivileged:1.17.2 + name: run-as-user-unprivileged + securityContext: + runAsUser: 100000 + runAsGroup: 100000 \ No newline at end of file diff --git a/3-security-context/demo/04-deployment-run-as-user.yaml b/3-security-context/demo/04-deployment-run-as-user.yaml new file mode 100644 index 0000000..7f75de0 --- /dev/null +++ b/3-security-context/demo/04-deployment-run-as-user.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: run-as-user + name: run-as-user +spec: + selector: + matchLabels: + run: run-as-user + template: + metadata: + labels: + run: run-as-user + spec: + containers: + - image: nginx:1.17.2 + name: run-as-user + securityContext: + runAsUser: 100000 + runAsGroup: 100000 \ No newline at end of file diff --git a/3-security-context/demo/04-deployment-allow-no-privilege-escalation.yaml b/3-security-context/demo/05-deployment-allow-no-privilege-escalation.yaml similarity index 100% rename from 3-security-context/demo/04-deployment-allow-no-privilege-escalation.yaml rename to 3-security-context/demo/05-deployment-allow-no-privilege-escalation.yaml diff --git a/3-security-context/demo/06-deployment-seccomp.yaml b/3-security-context/demo/06-deployment-seccomp.yaml new file mode 100644 index 0000000..f83c6e6 --- /dev/null +++ b/3-security-context/demo/06-deployment-seccomp.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: run-with-seccomp + name: run-with-seccomp +spec: + selector: + matchLabels: + run: run-with-seccomp + template: + metadata: + labels: + run: run-with-seccomp + annotations: + seccomp.security.alpha.kubernetes.io/pod: runtime/default + spec: + containers: + - image: nginx:1.17.2 + name: run-with-seccomp \ No newline at end of file diff --git a/3-security-context/demo/07-deployment-run-without-caps.yaml b/3-security-context/demo/07-deployment-run-without-caps.yaml new file mode 100644 index 0000000..b38b2f3 --- /dev/null +++ b/3-security-context/demo/07-deployment-run-without-caps.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: run-without-caps + name: run-without-caps +spec: + selector: + matchLabels: + run: run-without-caps + template: + metadata: + labels: + run: run-without-caps + spec: + containers: + - image: nginx:1.17.2 + name: run-without-caps + securityContext: + capabilities: + drop: + - ALL \ No newline at end of file diff --git a/3-security-context/demo/08-deployment-run-with-certain-caps.yaml b/3-security-context/demo/08-deployment-run-with-certain-caps.yaml new file mode 100644 index 0000000..2900958 --- /dev/null +++ b/3-security-context/demo/08-deployment-run-with-certain-caps.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: run-with-certain-caps + name: run-with-certain-caps +spec: + selector: + matchLabels: + run: run-with-certain-caps + template: + metadata: + labels: + run: run-with-certain-caps + spec: + containers: + - image: nginx:1.17.2 + name: run-with-certain-caps + securityContext: + capabilities: + drop: + - ALL + add: + - CHOWN + - NET_BIND_SERVICE + - SETGID + - SETUID \ No newline at end of file diff --git a/3-security-context/demo/09-deployment-run-without-caps-unprivileged.yaml b/3-security-context/demo/09-deployment-run-without-caps-unprivileged.yaml new file mode 100644 index 0000000..b266944 --- /dev/null +++ b/3-security-context/demo/09-deployment-run-without-caps-unprivileged.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: run-without-caps-unprivileged + name: run-without-caps-unprivileged +spec: + selector: + matchLabels: + run: run-without-caps-unprivileged + template: + metadata: + labels: + run: run-without-caps-unprivileged + spec: + containers: + - image: nginxinc/nginx-unprivileged:1.17.2 + name: run-without-caps-unprivileged + securityContext: + capabilities: + drop: + - ALL \ No newline at end of file diff --git a/3-security-context/demo/05-deployment-read-only-fs.yaml b/3-security-context/demo/10-deployment-read-only-fs.yaml similarity index 100% rename from 3-security-context/demo/05-deployment-read-only-fs.yaml rename to 3-security-context/demo/10-deployment-read-only-fs.yaml diff --git a/3-security-context/demo/05a-netpol-egress-docker-sudo-allow-internal-only.yaml b/3-security-context/demo/10a-netpol-egress-docker-sudo-allow-internal-only.yaml similarity index 100% rename from 3-security-context/demo/05a-netpol-egress-docker-sudo-allow-internal-only.yaml rename to 3-security-context/demo/10a-netpol-egress-docker-sudo-allow-internal-only.yaml diff --git a/3-security-context/demo/06-deployment-nginx-read-only-fs.yaml b/3-security-context/demo/11-deployment-nginx-read-only-fs.yaml similarity index 100% rename from 3-security-context/demo/06-deployment-nginx-read-only-fs.yaml rename to 3-security-context/demo/11-deployment-nginx-read-only-fs.yaml diff --git a/3-security-context/demo/07-deployment-nginx-read-only-fs-empty-dirs.yaml b/3-security-context/demo/12-deployment-nginx-read-only-fs-empty-dirs.yaml similarity index 100% rename from 3-security-context/demo/07-deployment-nginx-read-only-fs-empty-dirs.yaml rename to 3-security-context/demo/12-deployment-nginx-read-only-fs-empty-dirs.yaml diff --git a/3-security-context/demo/13-deployment-all-at-once.yaml b/3-security-context/demo/13-deployment-all-at-once.yaml new file mode 100644 index 0000000..ea3bed7 --- /dev/null +++ b/3-security-context/demo/13-deployment-all-at-once.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: all-at-once + name: all-at-once +spec: + selector: + matchLabels: + run: all-at-once + strategy: + type: Recreate + template: + metadata: + labels: + run: all-at-once + annotations: + seccomp.security.alpha.kubernetes.io/pod: runtime/default + spec: + containers: + # Another suitable example springcommunity/spring-framework-petclinic:5.1.5 + - image: nginxinc/nginx-unprivileged:1.17.2 + name: all-at-once + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 100000 + runAsGroup: 100000 + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} \ No newline at end of file diff --git a/3-security-context/demo/interactive-demo.sh b/3-security-context/demo/interactive-demo.sh index d106ff6..86d8f1b 100755 --- a/3-security-context/demo/interactive-demo.sh +++ b/3-security-context/demo/interactive-demo.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash set -o errexit -o nounset -o pipefail +BASEDIR=$(dirname $0) +ABSOLUTE_BASEDIR="$( cd ${BASEDIR} && pwd )" PRINT_ONLY=${PRINT_ONLY:-false} @@ -11,10 +13,18 @@ function main() { runAsRoot + runAsUser + allowPrivilegeEscalation + activateSeccompProfile + + dropCapabilities + readOnlyRootFilesystem + allAtOnce + echo message "This concludes the demo, thanks for securing your clusters!" } @@ -40,69 +50,154 @@ function runAsRoot() { subHeading "1.2 Same with \"runAsNonRoot: true\"" printFile 01-deployment-run-as-non-root.yaml printAndRun "kubectl apply -f 01-deployment-run-as-non-root.yaml" - sleep 2 + run "sleep 3" printAndRun "kubectl get pod \$(kubectl get pods | awk '/^run-as-non-root/ {print \$1;exit}')" + pressKeyToContinue echo printAndRun "kubectl describe pod \$(kubectl get pods | awk '/^run-as-non-root/ {print \$1;exit}') | grep Error" subHeading "1.3 Image that runs as nginx as non-root ➜ runs as uid != 0" printFile 02-deployment-run-as-non-root-unprivileged.yaml + printAndRun "kubectl get pod \$(kubectl get pods | awk '/^run-as-non-root-unprivileged/ {print \$1;exit}')" + pressKeyToContinue printAndRun "kubectl exec \$(kubectl get pods | awk '/run-as-non-root-unprivileged/ {print \$1;exit}') id" pressKeyToContinue } +function runAsUser() { + heading "2. Run as user/group" + + subHeading "2.1 Runnginx as uid/gid 100000" + printFile 03-deployment-run-as-user-unprivileged.yaml + printAndRun "kubectl get pod \$(kubectl get pods | awk '/run-as-user-unprivileged/ {print \$1;exit}')" + printAndRun "kubectl exec \$(kubectl get pods | awk '/run-as-user-unprivileged/ {print \$1;exit}') id" + + subHeading "2.2 Image must be designed to work with \"runAsUser\" and \"runAsGroup\"" + printFile 04-deployment-run-as-user.yaml + printAndRun "kubectl apply -f 04-deployment-run-as-user.yaml" + run "sleep 3" + + printAndRun "kubectl get pod \$(kubectl get pods | awk '/^run-as-user/ {print \$1;exit}')" + echo + printAndRun "kubectl logs \$(kubectl get pods | awk '/^run-as-user/ {print \$1;exit}')" +} + function allowPrivilegeEscalation() { - heading "2. allowPrivilegeEscalation" + heading "3. allowPrivilegeEscalation" - subHeading "2.1 Escalate privileges" + subHeading "3.1 Escalate privileges" printAndRun "kubectl create deployment docker-sudo --image schnatterer/docker-sudo:0.1" - sleep 1 + run "sleep 1" printAndRun "kubectl exec \$(kubectl get pods | awk '/docker-sudo/ {print \$1;exit}') sudo apt update" - subHeading "2.2 Same with \"allowPrivilegeEscalation: true\" -> escalation fails" - printFile 04-deployment-allow-no-privilege-escalation.yaml + subHeading "3.2 Same with \"allowPrivilegeEscalation: true\" ➜ escalation fails" + printFile 05-deployment-allow-no-privilege-escalation.yaml printAndRun "kubectl exec \$(kubectl get pods | awk '/allow-no-privilege-escalation/ {print \$1;exit}') sudo apt update" pressKeyToContinue } -function readOnlyRootFilesystem() { +function activateSeccompProfile() { + + heading "4. Enable Seccomp default profile" + + subHeading "4.1 No seccomp profile by default 😲" + kubectlSilent create deployment nginx --image nginx:1.17.2 + printAndRun "kubectl exec \$(kubectl get pods | awk '/nginx/ {print \$1;exit}') grep Seccomp /proc/1/status" + + subHeading "4.2 Same with default seccomp profile" + printFile 06-deployment-seccomp.yaml + printAndRun "kubectl get pod \$(kubectl get pods | awk '/run-with-seccomp/ {print \$1;exit}')" + printAndRun "kubectl exec \$(kubectl get pods | awk '/run-with-seccomp/ {print \$1;exit}') grep Seccomp /proc/1/status" + + pressKeyToContinue +} + +function dropCapabilities() { + + heading "5. Drop Capabilities" + + subHeading "5.1 some images require capabilities" + printFile 07-deployment-run-without-caps.yaml + printAndRun "kubectl get pod \$(kubectl get pods | awk '/^run-without-caps/ {print \$1;exit}')" + echo + printAndRun "kubectl logs \$(kubectl get pods | awk '/^run-without-caps/ {print \$1;exit}') " - heading "3. readOnlyRootFilesystem" + message "How to find out which capabilities we need to add?" + printAndRun "docker run --rm --cap-drop ALL nginx:1.17.2" + pressKeyToContinue + echo + printAndRun "docker run --rm --cap-drop ALL --cap-add CAP_CHOWN nginx:1.17.2" + pressKeyToContinue + #printAndRun "docker run --rm --cap-drop ALL --cap-add CAP_CHOWN --cap-add CAP_NET_BIND_SERVICE nginx:1.17.2" + #printAndRun "docker run --rm --cap-drop ALL --cap-add CAP_CHOWN --cap-add CAP_NET_BIND_SERVICE --cap-add SETGID nginx:1.17.2" + #printAndRun "docker run --rm --cap-drop ALL --cap-add CAP_CHOWN --cap-add CAP_NET_BIND_SERVICE --cap-add SETGID --cap-add SETUID nginx:1.17.2" + + message "... and so on and so forth. Then add the necessary caps to kubernetes:" + printFile 08-deployment-run-with-certain-caps.yaml + printAndRun "kubectl get pod \$(kubectl get pods | awk '/run-with-certain-caps/ {print \$1;exit}')" + + subHeading "5.2 Image that runs without caps" + printFile 09-deployment-run-without-caps-unprivileged.yaml + printAndRun "kubectl get pod \$(kubectl get pods | awk '/run-without-caps-unprivileged/ {print \$1;exit}')" + + pressKeyToContinue +} + +function readOnlyRootFilesystem() { + heading "6. readOnlyRootFilesystem" + kubectlSilent create deployment docker-sudo --image schnatterer/docker-sudo:0.1 - subHeading "3.1 Write to container's file system" + subHeading "6.1 Write to container's file system" printAndRun "kubectl exec \$(kubectl get pods | awk '/docker-sudo/ {print \$1;exit}') sudo apt update" - subHeading "3.2 Same with \"readOnlyRootFilesystem: true\" -> fails to write to temp dirs" - printFile 05-deployment-read-only-fs.yaml + subHeading "6.2 Same with \"readOnlyRootFilesystem: true\" ➜ fails to write to temp dirs" + printFile 10-deployment-read-only-fs.yaml printAndRun "kubectl exec \$(kubectl get pods | awk '/^read-only-fs/ {print \$1;exit}') sudo apt update" - subHeading "( 3.2a By the way - this could also be done with a networkPolicy )" - printFile 05a-netpol-egress-docker-sudo-allow-internal-only.yaml - printAndRun "kubectl apply -f 05a-netpol-egress-docker-sudo-allow-internal-only.yaml" + subHeading "( 6.2a By the way - this could also be done with a networkPolicy )" + printFile 10a-netpol-egress-docker-sudo-allow-internal-only.yaml + printAndRun "kubectl apply -f 10a-netpol-egress-docker-sudo-allow-internal-only.yaml" printAndRun "timeout 10s kubectl exec \$(kubectl get pods | awk '/docker-sudo/ {print \$1;exit}') sudo apt update" - subHeading "3.3 readOnlyRootFilesystem causes issues with other images" - printFile 06-deployment-nginx-read-only-fs.yaml + subHeading "6.3 readOnlyRootFilesystem causes issues with other images" + printFile 11-deployment-nginx-read-only-fs.yaml printAndRun "kubectl get pod \$(kubectl get pods | awk '/failing-nginx-read-only-fs/ {print \$1;exit}')" - echo + message "Not running. Let's check the logs" printAndRun "kubectl logs \$(kubectl get pods | awk '/failing-nginx-read-only-fs/ {print \$1;exit}')" message "How to find out which folders we need to mount?" - printAndRun "docker run -d --rm --name nginx nginx:1.17.2" - sleep 1 - printAndRun "docker diff nginx" - run docker rm -f nginx > /dev/null + printAndRun "nginxContainer=\$(docker run -d --rm nginx:1.17.2)" + run "sleep 1" + printAndRun "docker diff \${nginxContainer}" + run "docker rm -f \${nginxContainer} > /dev/null" message "Mount those dirs as as emptyDir!" - printFile 07-deployment-nginx-read-only-fs-empty-dirs.yaml + printFile 12-deployment-nginx-read-only-fs-empty-dirs.yaml printAndRun "kubectl get pod \$(kubectl get pods | awk '/empty-dirs-nginx-read-only-fs/ {print \$1;exit}')" + + pressKeyToContinue +} + +function allAtOnce() { + heading "7. An example that implements all good practices at once" + + printFile 13-deployment-all-at-once.yaml + printAndRun "kubectl get pod \$(kubectl get pods | awk '/all-at-once/ {print \$1;exit}')" + pressKeyToContinue + printAndRun "kubectl port-forward \$(kubectl get pods | awk '/all-at-once/ {print \$1;exit}') 8080 > /dev/null &" + run "sleep 2" + pressKeyToContinue + printAndRun "curl localhost:8080" + run "jobs > /dev/null && kill %1" + + pressKeyToContinue } function heading() { @@ -153,17 +248,18 @@ function printFile() { } function reset() { - if [[ "${PRINT_ONLY}" != "true" ]]; then - # Reset the changes done by this demo - kubectlSilent delete deploy nginx - kubectlSilent delete -f 01-deployment-run-as-non-root.yaml - kubectlSilent delete deploy docker-sudo - kubectlSilent delete netpol egress-nginx-allow-internal-only - fi + # Reset the changes done by this demo + kubectlSilent delete deploy nginx + kubectlSilent delete -f 01-deployment-run-as-non-root.yaml + kubectlSilent delete -f 04-deployment-run-as-user.yaml + kubectlSilent delete deploy docker-sudo + kubectlSilent delete netpol egress-nginx-allow-internal-only } function kubectlSilent() { - kubectl "$@" > /dev/null 2>&1 || true + if [[ "${PRINT_ONLY}" != "true" ]]; then + kubectl "$@" > /dev/null 2>&1 || true + fi } diff --git a/4-pod-security-policies/Readme.md b/4-pod-security-policies/Readme.md index 2dcad63..4fce3f1 100644 --- a/4-pod-security-policies/Readme.md +++ b/4-pod-security-policies/Readme.md @@ -2,6 +2,8 @@ ![Clusters, Namespaces and Pods](http://www.plantuml.com/plantuml/svg/dP2nQWCn38PtFuMuGZE5qWP2nj12nXB8M3gebdgOEqk7hEDIIjwzlZH3iaQJ_Vdt_u6snT5yp7qeNP813JCaSRPlZ0o_0U0LOzUQZa9lsgl1myiALqJpYngnNUZpUhtPK3Y5go8qq-bSSllr9XGr3oeiVeSDOAVY5xOxprmUH8cXEN0SBVbFjOlpqU4HzeTzKpq1OAWYR6lg7JENUcDOJAcdvSJ55mtK5DJva3R9yVF__9LSCAUdQqOQExPb6KbdKksdi6MXkj8_) +Note: This demo uses the same cluster as [Security Context](../3-security-context/Readme.md). + ```bash # Switch to proper kubectl context - alternatively use kubectx source ../config.sh diff --git a/utils.sh b/utils.sh index 9f9c0a4..3903d60 100644 --- a/utils.sh +++ b/utils.sh @@ -76,11 +76,15 @@ function clusterExists() { } function becomeClusterAdmin() { - kubectl create clusterrolebinding myname-cluster-admin-binding \ + kubectlIdempotent create clusterrolebinding myname-cluster-admin-binding \ --clusterrole=cluster-admin \ --user=$(gcloud info | grep Account | sed -r 's/Account\: \[(.*)\]/\1/') } +function kubectlIdempotent() { + kubectl "$@" --dry-run -o yaml | kubectl apply -f - +} + function podReady() { local POD_NAME="$1" local NAMESPACE="$2"