Skip to content

Instantly share code, notes, and snippets.

@t83714
Forked from Arabus/helm_deployment_migration.sh
Last active April 27, 2020 03:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save t83714/9317f8933223d3c4bd626128ee306208 to your computer and use it in GitHub Desktop.
Save t83714/9317f8933223d3c4bd626128ee306208 to your computer and use it in GitHub Desktop.
Make script use gsed on MacOS and compatible with linux
#!/usr/bin/env bash
#
# This script ports deployed Helm v2 Packages from k8s API v1.15 to 1.16
#
# When upgrading K8s clusters to a new minor API version it is possible that
# already deprecated API Endpoints are removed e.g.,
# extensions/v1beta1/DaemonSet
# K8s handles this transparently for already deployed ressources from the
# deprecated paths, making them available via their new API. They are no longer
# available through the removed paths though.
# Unfortunately already deployed Helm v2 Packages can no longer be upgraded when
# this happens. This is due to the nature of the stored deployment information.
#
# Helm v2 packages in deployed state are stored as k8s configmaps in the tillers
# namespace. The ConfigMap contains a base64 encoded, gzipped, protobuf message
# with the package metadata e.g., NOTES and template files, the contents of the
# chart.yaml file and all default and additionally provided values.
#
# Because the templates of a package are stored in its deployment information,
# helm v2 needs to recalculate the actual ressources generated by it (called a
# manifest) whenever status information is requested about it, e.g., when
# running helm get manifest. Because the ressources constructed use the k8s
# API this will fail if the helm chart template contains deprecated API paths.
# The resulting error will look similar to this:
#
# UPGRADE FAILED
# Error: failed decoding reader into objects: [unable to recognize "": no matches for kind "Deployment" in version "extensions/v1beta1", unable to recognize "": no matches for kind "StatefulSet" in version "apps/v1beta2"]
# Error: UPGRADE FAILED: failed decoding reader into objects: [unable to recognize "": no matches for kind "Deployment" in version "extensions/v1beta1", unable to recognize "": no matches for kind "StatefulSet" in version "apps/v1beta2"]
#
# Which is helm/k8s way of telling you that there is no object under the
# requested API path, because that path no longer exists.
#
# This can be fixed either by upgrading the helm chart before upgrading k8s
# with a breaking API change or by downgrading k8s, upgrading the chart and
# then upgrading k8s again.
#
##############################################################################
#
# Or you could just change the existing deployment information so they produce
# a valid manifest again and then upgrade the helm chart.
#
# To do this, you need to download the configmaps that stores your helm
# deployment. Then you need to extract the deployment data, decode the base64
# blob, unzip it and last but not least decode the contained protbuf.
#
# Now you can edit the data and replace changed api paths or add missing
# attrbutes.
#
# Afterwards your need to reencode, pack, base64encode and integrate
# it into the configmap again.
#
# Finally you can kubectl apply the changed configmap and helm will now use
# the new APIs. You can check via helm get manifest $YOURDEPLOYMENTNAMEHERE
###############################################################################
# Directory we do all the hacky file manipulation in. Change it to your liking.
HM_WORKDIR="${HOME}/.helmmigration"
readonly HM_WORKDIR
# You need the specific protobuf schema of helm to decode the message content
# it is part of the github helm repo, so you need to clone that somewhere local.
# git clone https://github.com/helm/helm.git
# NOTE: You need to checkout the corresponding helm v2 branch of your tiller
# deployment, e.g. v2.16.1 Otherwise the headers won't be there (they were
# removed with helm V3)
HELM_PROTOBUF_SCHEMA="${HOME}/repos/helm/_proto/"
readonly HELM_PROTOBUF_SCHEMA
# You also need the protbuf base schema, so clone that as well
# git clone https://github.com/google/protobuf
PROTOBUF_SCHEMA="${HOME}/repos/protobuf/src"
readonly PROTOBUF_SCHEMA
# Make script compatible with MacOS
if [[ "$OSTYPE" == "darwin"* ]]; then
MKDIR_CMD="mkdir -p"
# base64 on macos runs with default -b 0 and do not support -w or --wrap
BASE64_ENCODE_CMD="base64"
# we need to use gnu sed on MacOS --- can be installed via `brew install gnu-sed`
SED_BIN="gsed"
# We need these to do our work. Mac needs extra `gsed`
HM_DEPENDENCIES=(git kubectl helm gunzip base64 protoc gsed)
readonly HM_DEPENDENCIES
else
MKDIR_CMD="mkdir --parents"
BASE64_ENCODE_CMD="base64 --wrap 0"
SED_BIN="sed"
# We need these to do our work
HM_DEPENDENCIES=(git kubectl helm gunzip base64 protoc)
readonly HM_DEPENDENCIES
fi
shopt -s expand_aliases
shopt -s extglob
# shellcheck disable=SC2139
alias helmprotod="protoc --proto_path ${HELM_PROTOBUF_SCHEMA} --proto_path ${PROTOBUF_SCHEMA} --decode hapi.release.Release ${HELM_PROTOBUF_SCHEMA}/hapi/**/*"
# shellcheck disable=SC2139
alias helmprotoe="protoc --proto_path ${HELM_PROTOBUF_SCHEMA} --proto_path ${PROTOBUF_SCHEMA} --encode hapi.release.Release ${HELM_PROTOBUF_SCHEMA}/hapi/**/*"
_test_dependencies() {
printf "Testing if all dependencies are installed ...\n"
for dep in "${HM_DEPENDENCIES[@]}"; do
command -v "${dep}" || { printf "Missing dependency %s" "${dep} " && exit 1; }
done
}
_usage() {
cat << EOF
Usage: ${0} <helm_deployment_name> [tiller-namespace]\n
EOF
}
_get_configmap() {
# We make a backup of the original config, in case anything goes wrong
# You can restore it by running kubectl apply -f
kubectl get configmaps \
--namespace "${tiller_namespace}" \
--selector "NAME=${helm_deployment_name},STATUS=DEPLOYED" \
--output yaml \
> "${HM_WORKDIR}/${helm_deployment_name}_latest_helm_configmap.yaml"
test -s "${HM_WORKDIR}/${helm_deployment_name}_latest_helm_configmap.yaml" || exit 1
_git_store 'Original Configmap Backup'
}
_get_configmap_data() {
# extract the deployment data to plaintext
kubectl get configmaps \
--namespace "${tiller_namespace}" \
--selector "NAME=${helm_deployment_name},STATUS=DEPLOYED" \
--output jsonpath='{ .items[0].data.release }' \
| base64 --decode \
| gunzip \
| helmprotod \
> "${HM_WORKDIR}/${helm_deployment_name}_data.yaml"
test -s "${HM_WORKDIR}/${helm_deployment_name}_data.yaml" || exit 1
_git_store 'Original Configmap Data'
}
_initialize_workdir() {
test -d "${HM_WORKDIR}" || $MKDIR_CMD "${HM_WORKDIR}"
cd "${HM_WORKDIR}" || exit 1
git init
}
_git_store() {
cd "${HM_WORKDIR}" || exit 1
git add .
git commit --message "${1}"
}
_replace_removed_k8s_1_15_APIs() {
"${SED_BIN}" -i \
-e 's|apiVersion: extensions/v1beta1\\nkind: Deployment|apiVersion: apps/v1\\nkind: Deployment|g' \
-e 's|apiVersion: extensions/v1beta1\\nkind: DaemonSet|apiVersion: apps/v1\\nkind: DaemonSet|g' \
-e 's|apiVersion: extensions/v1beta1\\nkind: StatefulSet|apiVersion: apps/v1\\nkind: StatefulSet|g' \
-e 's|apiVersion: extensions/v1beta1\\nkind: ReplicaSet|apiVersion: apps/v1\\nkind: ReplicaSet|g' \
-e 's|apiVersion: apps/v1beta2\\nkind: Deployment|apiVersion: apps/v1\\nkind: Deployment|g' \
-e 's|apiVersion: apps/v1beta2\\nkind: DaemonSet|apiVersion: apps/v1\\nkind: DaemonSet|g' \
-e 's|apiVersion: apps/v1beta2\\nkind: StatefulSet|apiVersion: apps/v1\\nkind: StatefulSet|g' \
-e 's|apiVersion: apps/v1beta2\\nkind: ReplicaSet|apiVersion: apps/v1\\nkind: ReplicaSet|g' \
-e 's|apiVersion: extensions/v1beta1\\nkind: NetworkPolicy|apiVersion: networking.k8s.io/v1\\nkind: NetworkPolicy|g' \
-e 's|apiVersion: extensions/v1beta1\\nkind: PodSecurityPolicy|apiVersion: policy/v1beta1\\nkind: PodSecurityPolicy|g' \
-e 's|apiVersion: extensions/v1beta1\\nkind: Ingress|apiVersion: networking.k8s.io/v1beta1\\nkind: Ingress|g' \
"${HM_WORKDIR}/${helm_deployment_name}_data.yaml"
_git_store 'Patched Configmap Data' || { printf "Nothing to do! Exiting ...\n" && exit 1; }
}
_show_changes() {
git diff HEAD^
read -rp 'Press enter to continue or ctrl-c to abort'
}
_set_configmap_data() {
patched_data="$(helmprotoe < "${helm_deployment_name}_data.yaml" | gzip | $BASE64_ENCODE_CMD)"
readonly patched_data
"${SED_BIN}" -Ei "s|^([ ]+)release:([ ]+).*$|\1release:\2${patched_data}|" "${helm_deployment_name}_latest_helm_configmap.yaml"
_git_store 'Patched Configmap'
}
_apply_patched_config() {
read -rp 'Press enter to apply changes or ctrl-c to abort'
kubectl apply --dry-run \
--namespace kube-system \
--filename "${helm_deployment_name}_latest_helm_configmap.yaml" \
&& kubectl apply \
--namespace kube-system \
--filename "${helm_deployment_name}_latest_helm_configmap.yaml"
}
_show_patched_manifest() {
helm get manifest "${helm_deployment_name}"
}
main() {
declare helm_deployment_name
declare tiller_namespace
helm_deployment_name="${1?"No Helm deployment name provided to ${FUNCNAME[0]}"}"
tiller_namespace="${2:-kube-system}"
printf "Welcome to the helm deployment k8s API migration. We will now start looking for your helm deployment. Enjoy the ride.\n"
_test_dependencies
_initialize_workdir
_get_configmap
_get_configmap_data
_replace_removed_k8s_1_15_APIs
_show_changes
_set_configmap_data
_apply_patched_config
_show_patched_manifest
}
trap usage err
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && main "${@}"
@t83714
Copy link
Author

t83714 commented Apr 24, 2020

The v1.22 release will stop serving the following deprecated API versions in favor of newer and more stable API versions:

Ingress in the extensions/v1beta1 API version will no longer be served
Migrate to use the networking.k8s.io/v1beta1 API version, available since v1.14. Existing persisted data can be retrieved/updated via the new version.

Support for above has been added

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment