#!/bin/bash # Copyright 2015 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # !!!EXPERIMENTAL !!! Upgrade script for GCE. Expect this to get # rewritten in Go in relatively short order, but it allows us to start # testing the concepts. set -o errexit set -o nounset set -o pipefail if [[ "${KUBERNETES_PROVIDER:-gce}" != "gce" ]] { echo "!!! ${1} only works on GCE" >&2 exit 1 } setvar KUBE_ROOT = "$(dirname "${BASH_SOURCE}")/../.." source "${KUBE_ROOT}/hack/lib/util.sh" source "${KUBE_ROOT}/cluster/kube-util.sh" proc usage { echo "!!! EXPERIMENTAL !!!" echo "" echo "${0} [-M | -N | -P] [-o] (-l | )" echo " Upgrades master and nodes by default" echo " -M: Upgrade master only" echo " -N: Upgrade nodes only" echo " -P: Node upgrade prerequisites only (create a new instance template)" echo " -c: Upgrade NODE_UPGRADE_PARALLELISM nodes in parallel (default=1) within a single instance group. The MIGs themselves are dealt serially." echo " -o: Use os distro sepcified in KUBE_NODE_OS_DISTRIBUTION for new nodes. Options include 'debian' or 'gci'" echo " -l: Use local(dev) binaries. This is only supported for master upgrades." echo "" echo ' Version number or publication is either a proper version number' echo ' (e.g. "v1.0.6", "v1.2.0-alpha.1.881+376438b69c7612") or a version' echo ' publication of the form / (e.g. "release/stable",' echo ' "ci/latest-1"). Some common ones are:' echo ' - "release/stable"' echo ' - "release/latest"' echo ' - "ci/latest"' echo ' See the docs on getting builds for more information about version publication.' echo "" echo "(... Fetching current release versions ...)" echo "" # NOTE: IF YOU CHANGE THE FOLLOWING LIST, ALSO UPDATE test/e2e/cluster_upgrade.go local release_stable local release_latest local ci_latest setvar release_stable = $(gsutil cat gs://kubernetes-release/release/stable.txt) setvar release_latest = $(gsutil cat gs://kubernetes-release/release/latest.txt) setvar ci_latest = $(gsutil cat gs://kubernetes-release-dev/ci/latest.txt) echo "Right now, versions are as follows:" echo " release/stable: ${0} ${release_stable}" echo " release/latest: ${0} ${release_latest}" echo " ci/latest: ${0} ${ci_latest}" } proc print-node-version-info { echo "== $1 Node OS and Kubelet Versions ==" "${KUBE_ROOT}/cluster/kubectl.sh" get nodes -o=jsonpath='{range .items[*]}name: "{.metadata.name}", osImage: "{.status.nodeInfo.osImage}", kubeletVersion: "{.status.nodeInfo.kubeletVersion}"{"\n"}{end}' } proc upgrade-master { local num_masters setvar num_masters = $(get-master-replicas-count) if [[ "${num_masters}" -gt 1 ]] { echo "Upgrade of master not supported if more than one master replica present. The current number of master replicas: ${num_masters}" exit 1 } echo "== Upgrading master to '${SERVER_BINARY_TAR_URL}'. Do not interrupt, deleting master instance. ==" # Tries to figure out KUBE_USER/KUBE_PASSWORD by first looking under # kubeconfig:username, and then under kubeconfig:username-basic-auth. # TODO: KUBE_USER is used in generating ABAC policy which the # apiserver may not have enabled. If it's enabled, we must have a user # to generate a valid ABAC policy. If the username changes, should # the script fail? Should we generate a default username and password # if the section is missing in kubeconfig? Handle this better in 1.5. get-kubeconfig-basicauth get-kubeconfig-bearertoken detect-master parse-master-env upgrade-master-env backfile-kubeletauth-certs # Delete the master instance. Note that the master-pd is created # with auto-delete=no, so it should not be deleted. gcloud compute instances delete \ --project ${PROJECT} \ --quiet \ --zone ${ZONE} \ ${MASTER_NAME} create-master-instance "${MASTER_NAME}-ip" wait-for-master } proc upgrade-master-env { echo "== Upgrading master environment variables. ==" # Generate the node problem detector token if it isn't present on the original # master. if [[ "${ENABLE_NODE_PROBLEM_DETECTOR:-}" == "standalone" && "${NODE_PROBLEM_DETECTOR_TOKEN:-}" == "" ]] { setvar NODE_PROBLEM_DETECTOR_TOKEN = $(dd if=/dev/urandom bs=128 count=1 2>/dev/null | base64 | tr -d "=+/" | dd bs=32 count=1 2>/dev/null) } } # TODO(mikedanese): delete when we don't support < 1.6 proc backfile-kubeletauth-certs { if [[ ! -z "${KUBEAPISERVER_CERT_BASE64:-}" && ! -z "${KUBEAPISERVER_CERT_BASE64:-}" ]] { return 0 } mkdir -p "${KUBE_TEMP}/pki" echo ${CA_KEY_BASE64} | base64 -d > "${KUBE_TEMP}/pki/ca.key" echo ${CA_CERT_BASE64} | base64 -d > "${KUBE_TEMP}/pki/ca.crt" shell {cd "${KUBE_TEMP}/pki" kube::util::ensure-cfssl "${KUBE_TEMP}/cfssl" cat <<< """ > ca-config.json { "signing": { "client": { "expiry": "43800h", "usages": [ "signing", "key encipherment", "client auth" ] } } } """ > ca-config.json { "signing": { "client": { "expiry": "43800h", "usages": [ "signing", "key encipherment", "client auth" ] } } } EOF # the name kube-apiserver is bound to the node proxy # subpaths required for the apiserver to hit proxy # endpoints on the kubelet's handler. cat <<< """ \ | "${CFSSL_BIN}" gencert \ -ca=ca.crt \ -ca-key=ca.key \ -config=ca-config.json \ -profile=client \ - \ | "${CFSSLJSON_BIN}" -bare kube-apiserver { "CN": "kube-apiserver" } """ ${CFSSL_BIN} gencert \ -ca=ca.crt \ -ca-key=ca.key \ -config=ca-config.json \ -profile=client \ - \ | ${CFSSLJSON_BIN} -bare kube-apiserver { "CN": "kube-apiserver" } EOF } setvar KUBEAPISERVER_CERT_BASE64 = $(cat "${KUBE_TEMP}/pki/kube-apiserver.pem" | base64 | tr -d '\r\n') setvar KUBEAPISERVER_KEY_BASE64 = $(cat "${KUBE_TEMP}/pki/kube-apiserver-key.pem" | base64 | tr -d '\r\n') } proc wait-for-master { echo "== Waiting for new master to respond to API requests ==" local curl_auth_arg if [[ -n ${KUBE_BEARER_TOKEN:-} ]] { setvar curl_auth_arg = ''(-H "Authorization: Bearer ${KUBE_BEARER_TOKEN}") } elif [[ -n ${KUBE_PASSWORD:-} ]] { setvar curl_auth_arg = ''(--user "${KUBE_USER}:${KUBE_PASSWORD}") } else { echo "can't get auth credentials for the current master" exit 1 } while ! curl --insecure ${curl_auth_arg[@]} --max-time 5 \ --fail --output /dev/null --silent "https://${KUBE_MASTER_IP}/healthz { printf "." sleep 2 } echo "== Done ==" } # Perform common upgrade setup tasks # # Assumed vars # KUBE_VERSION proc prepare-upgrade { kube::util::ensure-temp-dir detect-project detect-subnetworks detect-node-names # sets INSTANCE_GROUPS write-cluster-name tars_from_version } # Reads kube-env metadata from first node in NODE_NAMES. # # Assumed vars: # NODE_NAMES # PROJECT # ZONE proc get-node-env { # TODO(zmerlynn): Make this more reliable with retries. gcloud compute --project ${PROJECT} ssh --zone ${ZONE} ${NODE_NAMES[0]} --command \ "curl --fail --silent -H 'Metadata-Flavor: Google' \ 'http://metadata/computeMetadata/v1/instance/attributes/kube-env'" 2>/dev/null } # Read os distro information from /os/release on node. # $1: The name of node # # Assumed vars: # PROJECT # ZONE proc get-node-os { gcloud compute ssh $1 \ --project ${PROJECT} \ --zone ${ZONE} \ --command \ "cat /etc/os-release | grep \"^ID=.*\" | cut -c 4-" } # Assumed vars: # KUBE_VERSION # NODE_SCOPES # NODE_INSTANCE_PREFIX # PROJECT # ZONE # # Vars set: # KUBELET_TOKEN # KUBE_PROXY_TOKEN # NODE_PROBLEM_DETECTOR_TOKEN # CA_CERT_BASE64 # EXTRA_DOCKER_OPTS # KUBELET_CERT_BASE64 # KUBELET_KEY_BASE64 proc upgrade-nodes { prepare-node-upgrade do-node-upgrade } proc setup-base-image { if [[ "${env_os_distro}" == "false" ]] { echo "== Ensuring that new Node base OS image matched the existing Node base OS image" setvar NODE_OS_DISTRIBUTION = $(get-node-os "${NODE_NAMES[0]}") if [[ "${NODE_OS_DISTRIBUTION}" == "cos" ]] { setvar NODE_OS_DISTRIBUTION = ""gci"" } source "${KUBE_ROOT}/cluster/gce/${NODE_OS_DISTRIBUTION}/node-helper.sh" # Reset the node image based on current os distro set-node-image } } # prepare-node-upgrade creates a new instance template suitable for upgrading # to KUBE_VERSION and echos a single line with the name of the new template. # # Assumed vars: # KUBE_VERSION # NODE_SCOPES # NODE_INSTANCE_PREFIX # PROJECT # ZONE # # Vars set: # SANITIZED_VERSION # INSTANCE_GROUPS # KUBELET_TOKEN # KUBE_PROXY_TOKEN # NODE_PROBLEM_DETECTOR_TOKEN # CA_CERT_BASE64 # EXTRA_DOCKER_OPTS # KUBELET_CERT_BASE64 # KUBELET_KEY_BASE64 proc prepare-node-upgrade { echo "== Preparing node upgrade (to ${KUBE_VERSION}). ==" >&2 setup-base-image setvar SANITIZED_VERSION = $(echo ${KUBE_VERSION} | sed 's/[\.\+]/-/g') # TODO(zmerlynn): Refactor setting scope flags. local scope_flags= if test -n ${NODE_SCOPES} { setvar scope_flags = ""--scopes ${NODE_SCOPES}"" } else { setvar scope_flags = ""--no-scopes"" } # Get required node env vars from exiting template. local node_env=$(get-node-env) setvar KUBELET_TOKEN = $(get-env-val "${node_env}" "KUBELET_TOKEN") setvar KUBE_PROXY_TOKEN = $(get-env-val "${node_env}" "KUBE_PROXY_TOKEN") setvar NODE_PROBLEM_DETECTOR_TOKEN = $(get-env-val "${node_env}" "NODE_PROBLEM_DETECTOR_TOKEN") setvar CA_CERT_BASE64 = $(get-env-val "${node_env}" "CA_CERT") setvar EXTRA_DOCKER_OPTS = $(get-env-val "${node_env}" "EXTRA_DOCKER_OPTS") setvar KUBELET_CERT_BASE64 = $(get-env-val "${node_env}" "KUBELET_CERT") setvar KUBELET_KEY_BASE64 = $(get-env-val "${node_env}" "KUBELET_KEY") upgrade-node-env # TODO(zmerlynn): How do we ensure kube-env is written in a ${version}- # compatible way? write-node-env # TODO(zmerlynn): Get configure-vm script from ${version}. (Must plumb this # through all create-node-instance-template implementations). local template_name=$(get-template-name-from-version ${SANITIZED_VERSION}) create-node-instance-template ${template_name} # The following is echo'd so that callers can get the template name. echo "Instance template name: ${template_name}" echo "== Finished preparing node upgrade (to ${KUBE_VERSION}). ==" >&2 } proc upgrade-node-env { echo "== Upgrading node environment variables. ==" # Get the node problem detector token from master if it isn't present on # the original node. if [[ "${ENABLE_NODE_PROBLEM_DETECTOR:-}" == "standalone" && "${NODE_PROBLEM_DETECTOR_TOKEN:-}" == "" ]] { detect-master local master_env=$(get-master-env) setvar NODE_PROBLEM_DETECTOR_TOKEN = $(get-env-val "${master_env}" "NODE_PROBLEM_DETECTOR_TOKEN") } } # Upgrades a single node. # $1: The name of the node # # Note: This is called multiple times from do-node-upgrade() in parallel, so should be thread-safe. proc do-single-node-upgrade { local -r instance="$1" setvar instance_id = $(gcloud compute instances describe "${instance}" \ --format='get(id)' \ --project="${PROJECT}" \ --zone="${ZONE}" 2>&1) && setvar describe_rc = ""$? || setvar describe_rc = ""$? if [[ "${describe_rc}" != 0 ]] { echo "== FAILED to describe ${instance} ==" echo ${instance_id} return ${describe_rc} } # Drain node echo "== Draining ${instance}. == " >&2 "${KUBE_ROOT}/cluster/kubectl.sh" drain --delete-local-data --force --ignore-daemonsets ${instance} \ && setvar drain_rc = ""$? || setvar drain_rc = ""$? if [[ "${drain_rc}" != 0 ]] { echo "== FAILED to drain ${instance} ==" return ${drain_rc} } # Recreate instance echo "== Recreating instance ${instance}. ==" >&2 setvar recreate = $(gcloud compute instance-groups managed recreate-instances "${group}" \ --project="${PROJECT}" \ --zone="${ZONE}" \ --instances="${instance}" 2>&1) && setvar recreate_rc = ""$? || setvar recreate_rc = ""$? if [[ "${recreate_rc}" != 0 ]] { echo "== FAILED to recreate ${instance} ==" echo ${recreate} return ${recreate_rc} } # Wait for instance to be recreated echo "== Waiting for instance ${instance} to be recreated. ==" >&2 while true { setvar new_instance_id = $(gcloud compute instances describe "${instance}" \ --format='get(id)' \ --project="${PROJECT}" \ --zone="${ZONE}" 2>&1) && setvar describe_rc = ""$? || setvar describe_rc = ""$? if [[ "${describe_rc}" != 0 ]] { echo "== FAILED to describe ${instance} ==" echo ${new_instance_id} echo " (Will retry.)" } elif [[ "${new_instance_id}" == "${instance_id}" ]] { echo -n . } else { echo "Instance ${instance} recreated." break } sleep 1 } # Wait for k8s node object to reflect new instance id echo "== Waiting for new node to be added to k8s. ==" >&2 while true { setvar external_id = $("${KUBE_ROOT}/cluster/kubectl.sh" get node "${instance}" --output=jsonpath='{.spec.externalID}' 2>&1) && setvar kubectl_rc = ""$? || setvar kubectl_rc = ""$? if [[ "${kubectl_rc}" != 0 ]] { echo "== FAILED to get node ${instance} ==" echo ${external_id} echo " (Will retry.)" } elif [[ "${external_id}" == "${new_instance_id}" ]] { echo "Node ${instance} recreated." break } elif [[ "${external_id}" == "${instance_id}" ]] { echo -n . } else { echo "Unexpected external_id '${external_id}' matches neither old ('${instance_id}') nor new ('${new_instance_id}')." echo " (Will retry.)" } sleep 1 } # Wait for the node to not have SchedulingDisabled=True and also to have # Ready=True. echo "== Waiting for ${instance} to become ready. ==" >&2 while true { setvar cordoned = $("${KUBE_ROOT}/cluster/kubectl.sh" get node "${instance}" --output='jsonpath={.status.conditions[?(@.type == "SchedulingDisabled")].status}') setvar ready = $("${KUBE_ROOT}/cluster/kubectl.sh" get node "${instance}" --output='jsonpath={.status.conditions[?(@.type == "Ready")].status}') if [[ "${cordoned}" == 'True' ]] { echo "Node ${instance} is still not ready: SchedulingDisabled=${ready}" } elif [[ "${ready}" != 'True' ]] { echo "Node ${instance} is still not ready: Ready=${ready}" } else { echo "Node ${instance} Ready=${ready}" break } sleep 1 } } # Prereqs: # - prepare-node-upgrade should have been called successfully proc do-node-upgrade { echo "== Upgrading nodes to ${KUBE_VERSION} with max parallelism of ${node_upgrade_parallelism}. ==" >&2 # Do the actual upgrade. # NOTE(zmerlynn): If you are changing this gcloud command, update # test/e2e/cluster_upgrade.go to match this EXACTLY. local template_name=$(get-template-name-from-version ${SANITIZED_VERSION}) local old_templates=() local updates=() for group in ${INSTANCE_GROUPS[@]} { setvar old_templates = ''($(gcloud compute instance-groups managed list \ --project="${PROJECT}" \ --filter="name ~ '${group}' AND zone:(${ZONE})" \ --format='value(instanceTemplate)' || true)) setvar set_instance_template_out = $(gcloud compute instance-groups managed set-instance-template "${group}" \ --template="${template_name}" \ --project="${PROJECT}" \ --zone="${ZONE}" 2>&1) && setvar set_instance_template_rc = ""$? || setvar set_instance_template_rc = ""$? if [[ "${set_instance_template_rc}" != 0 ]] { echo "== FAILED to set-instance-template for ${group} to ${template_name} ==" echo ${set_instance_template_out} return ${set_instance_template_rc} } setvar instances = ''() setvar instances = ''($(gcloud compute instance-groups managed list-instances "${group}" \ --format='value(instance)' \ --project="${PROJECT}" \ --zone="${ZONE}" 2>&1)) && setvar list_instances_rc = ""$? || setvar list_instances_rc = ""$? if [[ "${list_instances_rc}" != 0 ]] { echo "== FAILED to list instances in group ${group} ==" echo ${instances} return ${list_instances_rc} } setvar process_count_left = ${node_upgrade_parallelism} setvar pids = ''() setvar ret_code_sum = '0' # Should stay 0 in the loop iff all parallel node upgrades succeed. for instance in ${instances[@]} { do-single-node-upgrade ${instance} & setvar pids = ''("$!") # We don't want to run more than ${node_upgrade_parallelism} upgrades at a time, # so wait once we hit that many nodes. This isn't ideal, since one might take much # longer than the others, but it should help. setvar process_count_left = $((process_count_left - 1)) if [[ process_count_left -eq 0 || "${instance}" == "${instances[-1]}" ]] { # Wait for each of the parallel node upgrades to finish. for pid in "${pids[@]}" { wait $pid setvar ret_code_sum = $(( ret_code_sum + $? )) } # Return even if at least one of the node upgrades failed. if [[ ${ret_code_sum} != 0 ]] { echo "== Some of the ${node_upgrade_parallelism} parallel node upgrades failed. ==" return ${ret_code_sum} } setvar process_count_left = ${node_upgrade_parallelism} } } } # Remove the old templates. echo "== Deleting old templates in ${PROJECT}. ==" >&2 for tmpl in ${old_templates[@]} { gcloud compute instance-templates delete \ --quiet \ --project="${PROJECT}" \ ${tmpl} || true } echo "== Finished upgrading nodes to ${KUBE_VERSION}. ==" >&2 } setvar master_upgrade = 'true' setvar node_upgrade = 'true' setvar node_prereqs = 'false' setvar local_binaries = 'false' setvar env_os_distro = 'false' setvar node_upgrade_parallelism = '1' while getopts ":MNPlcho" opt { case{ M { setvar node_upgrade = 'false' } N { setvar master_upgrade = 'false' } P { setvar node_prereqs = 'true' } l { setvar local_binaries = 'true' } c { setvar node_upgrade_parallelism = ${NODE_UPGRADE_PARALLELISM:-1} } o { setvar env_os_distro = 'true' } h { usage exit 0 } \? { echo "Invalid option: -$OPTARG" >&2 usage exit 1 } } } shift $((OPTIND-1)) if [[ $# -gt 1 ]] { echo "Error: Only one parameter () may be passed after the set of flags!" >&2 usage exit 1 } if [[ $# -lt 1 ]] && [[ "${local_binaries}" == "false" ]] { usage exit 1 } if [[ "${master_upgrade}" == "false" ]] && [[ "${node_upgrade}" == "false" ]] { echo "Can't specify both -M and -N" >&2 exit 1 } # prompt if etcd storage media type isn't set unless using etcd2 when doing master upgrade if [[ -z "${STORAGE_MEDIA_TYPE:-}" ]] && [[ "${STORAGE_BACKEND:-}" != "etcd2" ]] && [[ "${master_upgrade}" == "true" ]] { echo "The default etcd storage media type in 1.6 has changed from application/json to application/vnd.kubernetes.protobuf." echo "Documentation about the change can be found at https://kubernetes.io/docs/admin/etcd_upgrade." echo "" echo "ETCD2 DOES NOT SUPPORT PROTOBUF: If you wish to have to ability to downgrade to etcd2 later application/json must be used." echo "" echo "It's HIGHLY recommended that etcd be backed up before this step!!" echo "" echo "To enable using json, before running this script set:" echo "export STORAGE_MEDIA_TYPE=application/json" echo "" if test -t 0 && test -t 1 { read -p "Would you like to continue with the new default, and lose the ability to downgrade to etcd2? [y/N] " confirm if [[ "${confirm}" != "y" ]] { exit 1 } } else { echo "To enable using protobuf, before running this script set:" echo "export STORAGE_MEDIA_TYPE=application/vnd.kubernetes.protobuf" echo "" echo "STORAGE_MEDIA_TYPE must be specified when run non-interactively." >&2 exit 1 } } print-node-version-info "Pre-Upgrade" if [[ "${local_binaries}" == "false" ]] { set_binary_version ${1} } prepare-upgrade if [[ "${node_prereqs}" == "true" ]] { prepare-node-upgrade exit 0 } if [[ "${master_upgrade}" == "true" ]] { upgrade-master } if [[ "${node_upgrade}" == "true" ]] { if [[ "${local_binaries}" == "true" ]] { echo "Upgrading nodes to local binaries is not yet supported." >&2 exit 1 } else { upgrade-nodes } } echo "== Validating cluster post-upgrade ==" "${KUBE_ROOT}/cluster/validate-cluster.sh" print-node-version-info "Post-Upgrade"