diff --git a/controllerconfig/controllerconfig/scripts/migrate_helm_release.py b/controllerconfig/controllerconfig/scripts/migrate_helm_release.py index 434e9618d6..501f15f9c5 100644 --- a/controllerconfig/controllerconfig/scripts/migrate_helm_release.py +++ b/controllerconfig/controllerconfig/scripts/migrate_helm_release.py @@ -1,5 +1,13 @@ -#!/usr/bin/env python - +#!/usr/bin/python +# +# Copyright (c) 2022 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# This script will perform helm v2 to helm v3 resource migration. This includes +# using the helm 2to3 plugin along with labeling and annotating cluster +# resources associated with the helm release so that it is managable by helm v3 +# import keyring import psycopg2 @@ -7,14 +15,19 @@ import subprocess import sys import os +from controllerconfig.common import log from psycopg2.extras import RealDictCursor +LOG = log.get_logger(__name__) def main(): if len(sys.argv) != 2: raise Exception("Release name should be specified") + + log.configure() + release = sys.argv[1] - print("Starting to migrate release {}".format(release)) + LOG.info("Starting to migrate release {}".format(release)) conn = init_connection() migrate_release(conn, release) @@ -35,6 +48,7 @@ def migrate_release(conn, release): release_name = release_info["name"] create_configmap(release_info) helm2to3_migrate(release_name) + update_release_resources(release_name) cleanup_release(conn, release_name) @@ -78,29 +92,144 @@ data: cmd = "kubectl --kubeconfig=/etc/kubernetes/admin.conf apply -f {}" \ .format(configmap_path) execute_command(cmd) - print("Configmap {} created".format(configmap_label_name)) + LOG.info("Configmap {} created".format(configmap_label_name)) os.remove(configmap_path) def helm2to3_migrate(release_name): - cmd = "helm 2to3 convert --kubeconfig=/etc/kubernetes/admin.conf \ - --tiller-out-cluster -s configmaps {}" \ - .format(release_name) + cmd = ("helm 2to3 convert --kubeconfig=/etc/kubernetes/admin.conf " + "--tiller-out-cluster -s configmaps {}".format(release_name)) execute_command(cmd) - print("Migrated {} to helm3".format(release_name)) + LOG.info("Migrated {} helm2 release to helm3".format(release_name)) + + +def get_api_resources(namespaced=True): + if namespaced: + namespace_arg = ' --namespaced=true' + else: + namespace_arg = ' --namespaced=false' + + # Get all API resources + try: + api_resources = [] + api_resources_query = subprocess.check_output( + ("kubectl --kubeconfig=/etc/kubernetes/admin.conf api-resources " + " --verbs=list {} -o name".format(namespace_arg)), + shell=True, stderr=subprocess.STDOUT).decode('utf-8') + if api_resources_query: + api_resources = [a for a in api_resources_query.split("\n") if a] + except Exception as e: + LOG.info("Exception {} occured when trying to get kubernetes API " + "resources".format(e)) + raise + return api_resources + + +def update_release_resources(release_name): + """ Properly label resources to support Helm v3 + + Per https://github.com/helm/helm-2to3/issues/147, existing cluster + resources deployed by helm v2 are not labeled properly for helm v3. + Search for deployed resources based on release name and adjust the + labeling. + """ + + LOG.info("Gathering namespaced kubernetes API resources...") + namespaced_api_resources = get_api_resources(namespaced=True) + + LOG.info("Gathering non-namespaced kubernetes API resources...") + nonamespaced_api_resources = get_api_resources(namespaced=False) + + # Get all helm release resources + release_resources = [] + for r in namespaced_api_resources + nonamespaced_api_resources: + if r in nonamespaced_api_resources: + awk_print = '{print $1}' + else: + awk_print = '{print "-n "$1" "$2}' + + LOG.info("Searching for {} resource related to {}...".format( + r, release_name)) + try: + cmd = ("kubectl --kubeconfig=/etc/kubernetes/admin.conf get -A " + "-l app.kubernetes.io/instance={} --show-kind " + "--ignore-not-found --no-headers {}".format( + release_name, r)) + release_query = subprocess.Popen(cmd, shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + output_filter = subprocess.Popen(['awk', awk_print], + stdin=release_query.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) + release_query.stdout.close() + resource_list, err = output_filter.communicate() + if output_filter.returncode != 0: + LOG.info("Command failed:\n {}\n{}\n{}".format( + cmd, resource_list, err)) + raise Exception("Failed to execute command: %s" % cmd) + + if resource_list: + resources = [r for r in resource_list.split("\n") if r] + release_resources += resources + + except Exception as e: + LOG.info("Exception {} occured when trying to check for {} use of " + "API resource {}".format(e, release_name, r)) + continue + + # Dump the resources need to be labeled/annotated + for r in release_resources: + LOG.info("Found {} resource: {}".format(release_name, r)) + + # Label the resources appropriately to support the release upgrade + for tiller_managed_resource in release_resources: + try: + labeling_out = subprocess.check_output( + ('kubectl --kubeconfig=/etc/kubernetes/admin.conf label ' + '--overwrite {} ' + '"app.kubernetes.io/managed-by=Helm"'.format( + tiller_managed_resource)), + shell=True, stderr=subprocess.STDOUT).decode('utf-8') + LOG.info(labeling_out) + except Exception as e: + LOG.info("Exception {} occured when trying to label '{}'".format( + e, tiller_managed_resource)) + continue + + if "-n " in tiller_managed_resource: + # Extract and annotate the namespaced resource + # Ex: '-n metrics-server deployment.apps/ms-metrics-server' + components = [c for c in tiller_managed_resource.split(" ") if c] + namespace = components[1] + + try: + annotate_out = subprocess.check_output( + ('kubectl --kubeconfig=/etc/kubernetes/admin.conf annotate' + ' --overwrite {} "meta.helm.sh/release-name={}" ' + '"meta.helm.sh/release-namespace={}"'.format( + tiller_managed_resource, release_name, namespace)), + shell=True, stderr=subprocess.STDOUT).decode('utf-8') + LOG.info(annotate_out) + except Exception as e: + LOG.info("Exception {} occured when trying to annotate " + "'{}'".format(e, tiller_managed_resource)) + continue def cleanup_release(conn, release_name): - cmd = "helm 2to3 cleanup --kubeconfig=/etc/kubernetes/admin.conf --release-cleanup \ - --tiller-out-cluster -s configmaps --skip-confirmation --name {}" \ - .format(release_name) + cmd = ("helm 2to3 cleanup --kubeconfig=/etc/kubernetes/admin.conf " + "--release-cleanup --tiller-out-cluster -s configmaps " + "--skip-confirmation --name {}".format(release_name)) execute_command(cmd) with conn: with conn.cursor() as cur: cur.execute("delete from releases where name = %s", (release_name,)) - print("Cleaned up helm2 data for {}".format(release_name)) + LOG.info("Cleaned up helm2 data for {}".format(release_name)) def execute_command(cmd): @@ -109,7 +238,7 @@ def execute_command(cmd): stdout, stderr = sub.communicate() if sub.returncode != 0: - print("Command failed:\n %s\n%s\n%s" % (cmd, stdout, stderr)) + LOG.info("Command failed:\n %s\n%s\n%s" % (cmd, stdout, stderr)) raise Exception("Failed to execute command: %s" % cmd) return stdout