diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1caa81d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_STORE +tmp diff --git a/Dockerfile b/Dockerfile index 86eb979..bf1bbbc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM alpine:3.4 -RUN apk --no-cache add curl ca-certificates bash -RUN curl -Lo /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl -RUN chmod +x /usr/local/bin/kubectl +FROM alpine:3.7 +RUN apk --no-cache add curl ca-certificates bash && \ + curl -Lo /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \ + chmod +x /usr/local/bin/kubectl COPY update.sh /bin/ ENTRYPOINT ["/bin/bash"] CMD ["/bin/update.sh"] diff --git a/README.md b/README.md index 8ec9b97..fd872e5 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,80 @@ -# Kubernetes plugin for drone.io [![Docker Repository on Quay](https://quay.io/repository/honestbee/drone-kubernetes/status "Docker Repository on Quay")](https://quay.io/repository/honestbee/drone-kubernetes) +# Kubernetes plugin for drone.io +#### [Docker Repository on Docker Cloud](https://cloud.docker.com/app/razorpay/repository/docker/razorpay/drone-kubernetes) +## Borrowed and distilled from [honestbee/drone-kubernetes](https://github.com/honestbee/drone-kubernetes) This plugin allows to update a Kubernetes deployment. + - Cert based auth for tls + - token based auth + - Insecure auth without tls -## Usage +## Usage This pipeline will update the `my-deployment` deployment with the image tagged `DRONE_COMMIT_SHA:0:8` ```yaml - pipeline: - deploy: - image: quay.io/honestbee/drone-kubernetes - deployment: my-deployment - repo: myorg/myrepo - container: my-container - tag: - - mytag - - latest +pipeline: + deploy: + image: razorpay/drone-kubernetes + pull: true + secrets: + - docker_username + - docker_password + - server_url_ + - server_cert_ + - client_cert_ / - server_token_ + - client_key_ / - server_token_ + - ... + user: + cluster: + auth_mode: [ token | client-cert ] // provide only if providing server_cert_ + deployment: [] + repo: + container: [ ] + namespace: + tag: + - ${DRONE_REPO_BRANCH}-${DRONE_COMMIT_SHA} + - ... + when: + environment: + branch: [ ,... ] + event: + exclude: [push, pull_request, tag] + include: [deployment] ``` -Deploying containers across several deployments, eg in a scheduler-worker setup. Make sure your container `name` in your manifest is the same for each pod. - -```yaml - pipeline: - deploy: - image: quay.io/honestbee/drone-kubernetes - deployment: [server-deploy, worker-deploy] - repo: myorg/myrepo - container: my-container - tag: - - mytag - - latest -``` - -Deploying multiple containers within the same deployment. - -```yaml - pipeline: - deploy: - image: quay.io/honestbee/drone-kubernetes - deployment: my-deployment - repo: myorg/myrepo - container: [container1, container2] - tag: - - mytag - - latest -``` - -**NOTE**: Combining multi container deployments across multiple deployments is not recommended - -This more complex example demonstrates how to deploy to several environments based on the branch, in a `app` namespace - -```yaml - pipeline: - deploy-staging: - image: quay.io/honestbee/drone-kubernetes - kubernetes_server: ${KUBERNETES_SERVER_STAGING} - kubernetes_cert: ${KUBERNETES_CERT_STAGING} - kubernetes_token: ${KUBERNETES_TOKEN_STAGING} - deployment: my-deployment - repo: myorg/myrepo - container: my-container - namespace: app - tag: - - mytag - - latest - when: - branch: [ staging ] - - deploy-prod: - image: quay.io/honestbee/drone-kubernetes - kubernetes_server: ${KUBERNETES_SERVER_PROD} - kubernetes_token: ${KUBERNETES_TOKEN_PROD} - # notice: no tls verification will be done, warning will is printed - deployment: my-deployment - repo: myorg/myrepo - container: my-container - namespace: app - tag: - - mytag - - latest - when: - branch: [ master ] -``` ## Required secrets -```bash - drone secret add --image=honestbee/drone-kubernetes \ - your-user/your-repo KUBERNETES_SERVER https://mykubernetesapiserver + - server_url + - token: + - server_token + - `kubectl get secret [ your default secret name ] -o yaml | egrep 'token:' > server.token` + - tls: + - server_cert + - `kubectl get secret [ your default secret name ] -o yaml | egrep 'ca.crt:' > ca.crt` + - `kubectl get secret [ your default secret name ] -o yaml | egrep 'ca.key:' > ca.key` + - client_cert + - client_key + - ``` + openssl genrsa -out client.key + openssl req -new -key client.key -out client.csr -subj "/CN=drone/O=org" + openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 500 + ``` + - ``` + cat ca.crt | base64 > car.crt.enc + cat client.crt | base64 > client.crt.enc + cat client.key | base64 > client.key.enc + ``` + - ``` + drone secret add -repository razorpay/gimli -image razorpay/drone-kubernetes -event deployment -name server_url_ -value https://k8s.org.com.:443 + drone secret add -repository razorpay/gimli -image razorpay/drone-kubernetes -event deployment -name server_cert_ -value @./ca.crt.enc + drone secret add -repository razorpay/gimli -image razorpay/drone-kubernetes -event deployment -name client_cert_ -value @./client.crt.enc + drone secret add -repository razorpay/gimli -image razorpay/drone-kubernetes -event deployment -name client_key_ -value @./client.key.enc + ``` - drone secret add --image=honestbee/drone-kubernetes \ - your-user/your-repo KUBERNETES_CERT - - drone secret add --image=honestbee/drone-kubernetes \ - your-user/your-repo KUBERNETES_TOKEN eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJ... -``` - -When using TLS Verification, ensure Server Certificate used by kubernetes API server +When using TLS Verification, ensure Server Certificate used by kubernetes API server is signed for SERVER url ( could be a reason for failures if using aliases of kubernetes cluster ) -## How to get token -1. After deployment inspect you pod for name of (k8s) secret with **token** and **ca.crt** -```bash -kubectl describe po/[ your pod name ] | grep SecretName | grep token -``` -(When you use **default service account**) - -2. Get data from you (k8s) secret -```bash -kubectl get secret [ your default secret name ] -o yaml | egrep 'ca.crt:|token:' -``` -3. Copy-paste contents of ca.crt into your drone's **KUBERNETES_CERT** secret -4. Decode base64 encoded token -```bash -echo [ your k8s base64 encoded token ] | base64 -d && echo'' -``` -5. Copy-paste decoded token into your drone's **KUBERNETES_TOKEN** secret - ### RBAC When using a version of kubernetes with RBAC (role-based access control) @@ -171,7 +129,7 @@ kubectl -n web get secrets kubectl -n web get secret/drone-deploy-token-XXXXX -o yaml | egrep 'ca.crt:|token:' ``` -## To do +## To do Replace the current kubectl bash script with a go implementation. diff --git a/update.sh b/update.sh index 98df6a9..523a87c 100755 --- a/update.sh +++ b/update.sh @@ -1,40 +1,218 @@ #!/bin/bash +set -euo pipefail -if [ -z ${PLUGIN_NAMESPACE} ]; then - PLUGIN_NAMESPACE="default" -fi +# globals +USER="" +NAMESPACE="" +CLUSTER="" +DEPLOYMENTS="" +SERVER_URL="" -if [ ! -z ${PLUGIN_KUBERNETES_TOKEN} ]; then - KUBERNETES_TOKEN=$PLUGIN_KUBERNETES_TOKEN -fi +# set globals +setUser(){ + USER=${PLUGIN_USER:-default} +} -if [ ! -z ${PLUGIN_KUBERNETES_SERVER} ]; then - KUBERNETES_SERVER=$PLUGIN_KUBERNETES_SERVER -fi +setNamespace(){ + NAMESPACE=${PLUGIN_NAMESPACE:-default} +} -if [ ! -z ${PLUGIN_KUBERNETES_CERT} ]; then - KUBERNETES_CERT=${PLUGIN_KUBERNETES_CERT} -fi +setCluster(){ + if [ ! -z ${PLUGIN_CLUSTER} ]; then + # convert cluster name to ucase and assign + CLUSTER=${PLUGIN_CLUSTER^^} + else + echo "[ERROR] Required pipeline parameter: cluster not provided" + exit 1 + fi +} -kubectl config set-credentials default --token=${KUBERNETES_TOKEN} -if [ ! -z ${KUBERNETES_CERT} ]; then - echo ${KUBERNETES_CERT} | base64 -d > ca.crt - kubectl config set-cluster default --server=${KUBERNETES_SERVER} --certificate-authority=ca.crt -else - echo "WARNING: Using insecure connection to cluster" - kubectl config set-cluster default --server=${KUBERNETES_SERVER} --insecure-skip-tls-verify=true -fi +setServerUrl(){ + # create dynamic cert var names + local SERVER_URL_VAR=SERVER_URL_${CLUSTER} + SERVER_URL=${!SERVER_URL_VAR} + if [[ -z "${SERVER_URL}" ]]; then + echo "[ERROR] Required drone secret: ${SERVER_URL_VAR} not added!" + exit 1 + fi +} -kubectl config set-context default --cluster=default --user=default -kubectl config use-context default +setGlobals(){ + setUser + setNamespace + setCluster + setServerUrl +} -# kubectl version -IFS=',' read -r -a DEPLOYMENTS <<< "${PLUGIN_DEPLOYMENT}" -IFS=',' read -r -a CONTAINERS <<< "${PLUGIN_CONTAINER}" -for DEPLOY in ${DEPLOYMENTS[@]}; do - echo Deploying to $KUBERNETES_SERVER - for CONTAINER in ${CONTAINERS[@]}; do - kubectl -n ${PLUGIN_NAMESPACE} set image deployment/${DEPLOY} \ - ${CONTAINER}=${PLUGIN_REPO}:${PLUGIN_TAG} --record +setSecureCluster(){ + local CLUSTER=$1; shift + local SERVER_URL=$1; shift + local SERVER_CERT=$1 + + echo "[INFO] Using secure connection with tls-certificate." + echo ${SERVER_CERT} | base64 -d > ca.crt + kubectl config set-cluster ${CLUSTER} --server=${SERVER_URL} --certificate-authority=ca.crt +} + +setInsecureCluster(){ + local CLUSTER=$1; shift + local SERVER_URL=$1 + + echo "[WARNING] Using insecure connection to cluster" + kubectl config set-cluster ${CLUSTER} --server=${SERVER_URL} --insecure-skip-tls-verify=true +} + +setClientToken(){ + local USER=$1; shift + local SERVER_TOKEN=$1 + + echo "[INFO] Setting client credentials with token" + kubectl config set-credentials ${USER} --token=${SERVER_TOKEN} +} + +setClientCertAndKey(){ + local USER=$1; shift + local CLIENT_CERT=$1; shift + local CLIENT_KEY=$1 + + echo "[INFO] Setting client credentials with signed-certificate and key." + echo ${CLIENT_CERT} | base64 -d > client.crt + echo ${CLIENT_KEY} | base64 -d > client.key + kubectl config set-credentials ${USER} --client-certificate=client.crt --client-key=client.key +} + +setContext(){ + local CLUSTER=$1; shift + local USER=$1 + + kubectl config set-context ${CLUSTER} --cluster=${CLUSTER} --user=${USER} + kubectl config use-context ${CLUSTER} +} + +pollDeploymentRollout(){ + local NAMESPACE=$1; shift + local DEPLOY=$1 + local TIMEOUT=600 + + # wait on deployment rollout status + echo "[INFO] Watching ${DEPLOY} rollout status..." + while true; do + result=`kubectl -n ${NAMESPACE} rollout status --watch=false --revision=0 deployment/${DEPLOY}` + echo ${result} + if [[ "${result}" == "deployment \"${DEPLOY}\" successfully rolled out" ]]; then + return 0 + else + # TODO: more conditions for error handling based on result text + sleep 10 + TIMEOUT=$((TIMEOUT-10)) + if [ "${TIMEOUT}" -eq 0 ]; then + return 1 + fi + fi done -done +} + +startDeployments(){ + local CLUSTER=$1; shift + local NAMESPACE=$1 + + IFS=',' read -r -a DEPLOYMENTS <<< "${PLUGIN_DEPLOYMENT}" + + for DEPLOY in ${DEPLOYMENTS[@]}; do + echo "[INFO] Deploying ${DEPLOY} to ${CLUSTER} ${NAMESPACE}" + kubectl -n ${NAMESPACE} set image deployment/${DEPLOY} \ + *="${PLUGIN_REPO}:${PLUGIN_TAG}" --record + pollDeploymentRollout ${NAMESPACE} ${DEPLOY} + + if [ "$?" -eq 0 ]; then + continue + else + exit 0 + fi + done +} + +clientAuthToken(){ + local CLUSTER=$1; shift + local USER=$1 + + echo "[INFO] Using Server token to authorize" + + CLIENT_TOKEN_VAR=CLIENT_TOKEN_${CLUSTER} + CLIENT_TOKEN=${!CLIENT_TOKEN_VAR} + + if [[ ! -z "${CLIENT_TOKEN}" ]]; then + setClientToken ${USER} ${CLIENT_TOKEN} + else + echo "[ERROR] Required plugin secrets:" + echo " - ${CLIENT_TOKEN_VAR}" + echo "not provided." + exit 1 + fi +} + +clientAuthCert(){ + local CLUSTER=$1; shift + local USER=$1 + + echo "[INFO] Using Client cert and Key to authorize" + CLIENT_CERT_VAR=CLIENT_CERT_${CLUSTER} + CLIENT_KEY_VAR=CLIENT_KEY_${CLUSTER} + # expand + CLIENT_CERT=${!CLIENT_CERT_VAR} + CLIENT_KEY=${!CLIENT_KEY_VAR} + + if [[ ! -z "${CLIENT_CERT}" ]] && [[ ! -z "${CLIENT_KEY}" ]]; then + setClientCertAndKey ${USER} ${CLIENT_CERT} ${CLIENT_KEY} + else + echo "[ERROR] Required plugin secrets:" + echo " - ${CLIENT_CERT_VAR}" + echo " - ${CLIENT_KEY_VAR}" + echo "not provided" + exit 1 + fi +} + +clientAuth(){ + local AUTH_MODE=$1; shift + local CLUSTER=$1; shift + local USER=$1 + + if [ ! -z ${AUTH_MODE} ]; then + if [[ "${AUTH_MODE}" == "token" ]]; then + clientAuthToken ${CLUSTER} ${USER} + elif [[ "${AUTH_MODE}" == "client-cert" ]]; then + clientAuthCert ${CLUSTER} ${USER} + else + echo "[ERROR] Required plugin param - auth_mode - Should be either:" + echo "[ token | client-cert ]" + exit 1 + fi + else + echo "[ERROR] Required plugin param - auth_mode - not provided" + exit 1 + fi +} + +clusterAuth(){ + local SERVER_URL=$1; shift + local CLUSTER=$1; shift + local USER=$1 + + SERVER_CERT_VAR=SERVER_CERT_${CLUSTER} + SERVER_CERT=${!SERVER_CERT_VAR} + + if [[ ! -z "${SERVER_CERT}" ]]; then + setSecureCluster ${CLUSTER} ${SERVER_URL} ${SERVER_CERT} + AUTH_MODE=${PLUGIN_AUTH_MODE} + clientAuth ${AUTH_MODE} ${CLUSTER} ${USER} + else + echo "[WARNING] Required plugin parameter: ${SERVER_CERT_VAR} not added!" + setInsecureCluster ${CLUSTER} ${SERVER_URL} + fi +} + +setGlobals +clusterAuth ${SERVER_URL} ${CLUSTER} ${USER} +setContext ${CLUSTER} ${USER} +startDeployments ${CLUSTER} ${NAMESPACE}