|
| 1 | +#!/usr/bin/python |
| 2 | +# Copyright (c) YugaByte, Inc. |
| 3 | + |
| 4 | +# This script would generate a kubeconfig for the given servie account |
| 5 | +# by fetching the cluster information and also add the service account |
| 6 | +# token for the authentication purpose. |
| 7 | + |
| 8 | +import argparse |
| 9 | +from subprocess import check_output |
| 10 | +from sys import exit |
| 11 | +import json |
| 12 | +import base64 |
| 13 | +import tempfile |
| 14 | +import time |
| 15 | +import os.path |
| 16 | + |
| 17 | + |
| 18 | +def run_command(command_args, namespace=None, as_json=True, log_command=True): |
| 19 | + command = ["kubectl"] |
| 20 | + if namespace: |
| 21 | + command.extend(["--namespace", namespace]) |
| 22 | + command.extend(command_args) |
| 23 | + if as_json: |
| 24 | + command.extend(["-o", "json"]) |
| 25 | + if log_command: |
| 26 | + print("Running command: {}".format(" ".join(command))) |
| 27 | + output = check_output(command) |
| 28 | + if as_json: |
| 29 | + return json.loads(output) |
| 30 | + else: |
| 31 | + return output.decode("utf8") |
| 32 | + |
| 33 | + |
| 34 | +def create_sa_token_secret(directory, sa_name, namespace): |
| 35 | + """Creates a service account token secret for sa_name in |
| 36 | + namespace. Returns the name of the secret created. |
| 37 | +
|
| 38 | + Ref: |
| 39 | + https://k8s.io/docs/concepts/configuration/secret/#service-account-token-secrets |
| 40 | +
|
| 41 | + """ |
| 42 | + token_secret = { |
| 43 | + "apiVersion": "v1", |
| 44 | + "data": { |
| 45 | + "do-not-delete-used-for-yugabyte-anywhere": "MQ==", |
| 46 | + }, |
| 47 | + "kind": "Secret", |
| 48 | + "metadata": { |
| 49 | + "annotations": { |
| 50 | + "kubernetes.io/service-account.name": sa_name, |
| 51 | + }, |
| 52 | + "name": sa_name, |
| 53 | + }, |
| 54 | + "type": "kubernetes.io/service-account-token", |
| 55 | + } |
| 56 | + token_secret_file_name = os.path.join(directory, "token_secret.yaml") |
| 57 | + with open(token_secret_file_name, "w") as token_secret_file: |
| 58 | + json.dump(token_secret, token_secret_file) |
| 59 | + run_command(["apply", "-f", token_secret_file_name], namespace) |
| 60 | + return sa_name |
| 61 | + |
| 62 | + |
| 63 | +def get_secret_data(secret, namespace): |
| 64 | + """Returns the secret in JSON format if it has ca.crt and token in |
| 65 | + it, else returns None. It retries 3 times with 1 second timeout |
| 66 | + for the secret to be populated with this data. |
| 67 | +
|
| 68 | + """ |
| 69 | + secret_data = None |
| 70 | + num_retries = 5 |
| 71 | + timeout = 2 |
| 72 | + while True: |
| 73 | + secret_json = run_command(["get", "secret", secret], namespace) |
| 74 | + if "ca.crt" in secret_json["data"] and "token" in secret_json["data"]: |
| 75 | + secret_data = secret_json |
| 76 | + break |
| 77 | + |
| 78 | + num_retries -= 1 |
| 79 | + if num_retries == 0: |
| 80 | + break |
| 81 | + print( |
| 82 | + "Secret '{}' is not populated. Sleep {}s, ({} retries left)".format( |
| 83 | + secret, timeout, num_retries |
| 84 | + ) |
| 85 | + ) |
| 86 | + time.sleep(timeout) |
| 87 | + return secret_data |
| 88 | + |
| 89 | + |
| 90 | +def get_secrets_for_sa(sa_name, namespace): |
| 91 | + """Returns a list of all service account token secrets associated |
| 92 | + with the given sa_name in the namespace. |
| 93 | +
|
| 94 | + """ |
| 95 | + secrets = run_command( |
| 96 | + [ |
| 97 | + "get", |
| 98 | + "secret", |
| 99 | + "--field-selector", |
| 100 | + "type=kubernetes.io/service-account-token", |
| 101 | + "-o", |
| 102 | + 'jsonpath="{.items[?(@.metadata.annotations.kubernetes\.io/service-account\.name == "' |
| 103 | + + sa_name |
| 104 | + + '")].metadata.name}"', |
| 105 | + ], |
| 106 | + namespace, |
| 107 | + as_json=False, |
| 108 | + ) |
| 109 | + return secrets.strip('"').split() |
| 110 | + |
| 111 | + |
| 112 | +parser = argparse.ArgumentParser(description="Generate KubeConfig with Token") |
| 113 | +parser.add_argument("-s", "--service_account", help="Service Account name", required=True) |
| 114 | +parser.add_argument("-n", "--namespace", help="Kubernetes namespace", default="kube-system") |
| 115 | +parser.add_argument("-c", "--context", help="kubectl context") |
| 116 | +parser.add_argument("-o", "--output_file", help="output file path") |
| 117 | +args = vars(parser.parse_args()) |
| 118 | + |
| 119 | +# if the context is not provided we use the current-context |
| 120 | +context = args["context"] |
| 121 | +if context is None: |
| 122 | + context = run_command(["config", "current-context"], args["namespace"], as_json=False) |
| 123 | + |
| 124 | +cluster_attrs = run_command( |
| 125 | + ["config", "get-contexts", context.strip(), "--no-headers"], args["namespace"], as_json=False |
| 126 | +) |
| 127 | + |
| 128 | +cluster_name = cluster_attrs.strip().split()[2] |
| 129 | +endpoint = run_command( |
| 130 | + [ |
| 131 | + "config", |
| 132 | + "view", |
| 133 | + "-o", |
| 134 | + 'jsonpath="{.clusters[?(@.name =="' + cluster_name + '")].cluster.server}"', |
| 135 | + ], |
| 136 | + args["namespace"], |
| 137 | + as_json=False, |
| 138 | +) |
| 139 | +service_account_info = run_command(["get", "sa", args["service_account"]], args["namespace"]) |
| 140 | + |
| 141 | +tmpdir = tempfile.TemporaryDirectory() |
| 142 | + |
| 143 | +# Get the token and ca.crt from service account secret. |
| 144 | +sa_secrets = list() |
| 145 | + |
| 146 | +# Get secrets specified in the service account, there can be multiple |
| 147 | +# of them, and not all are service account token secrets. |
| 148 | +if "secrets" in service_account_info: |
| 149 | + sa_secrets = [secret["name"] for secret in service_account_info["secrets"]] |
| 150 | + |
| 151 | +# Find the existing additional service account token secrets |
| 152 | +sa_secrets.extend(get_secrets_for_sa(args["service_account"], args["namespace"])) |
| 153 | + |
| 154 | +secret_data = None |
| 155 | +for secret in sa_secrets: |
| 156 | + secret_data = get_secret_data(secret, args["namespace"]) |
| 157 | + if secret_data is not None: |
| 158 | + break |
| 159 | + |
| 160 | +# Kubernetes 1.22+ doesn't create the service account token secret by |
| 161 | +# default, we have to create one. |
| 162 | +if secret_data is None: |
| 163 | + print("No usable secret found for '{}', creating one.".format(args["service_account"])) |
| 164 | + token_secret = create_sa_token_secret(tmpdir.name, args["service_account"], args["namespace"]) |
| 165 | + secret_data = get_secret_data(token_secret, args["namespace"]) |
| 166 | + if secret_data is None: |
| 167 | + exit( |
| 168 | + "Failed to generate kubeconfig: No usable credentials found for '{}'.".format( |
| 169 | + args["service_account"] |
| 170 | + ) |
| 171 | + ) |
| 172 | + |
| 173 | + |
| 174 | +context_name = "{}-{}".format(args["service_account"], cluster_name) |
| 175 | +kube_config = args["output_file"] |
| 176 | +if not kube_config: |
| 177 | + kube_config = "/tmp/{}.conf".format(args["service_account"]) |
| 178 | + |
| 179 | + |
| 180 | +ca_crt_file_name = os.path.join(tmpdir.name, "ca.crt") |
| 181 | +ca_crt_file = open(ca_crt_file_name, "wb") |
| 182 | +ca_crt_file.write(base64.b64decode(secret_data["data"]["ca.crt"])) |
| 183 | +ca_crt_file.close() |
| 184 | + |
| 185 | +# create kubeconfig entry |
| 186 | +set_cluster_cmd = [ |
| 187 | + "config", |
| 188 | + "set-cluster", |
| 189 | + cluster_name, |
| 190 | + "--kubeconfig={}".format(kube_config), |
| 191 | + "--server={}".format(endpoint.strip('"')), |
| 192 | + "--embed-certs=true", |
| 193 | + "--certificate-authority={}".format(ca_crt_file_name), |
| 194 | +] |
| 195 | +run_command(set_cluster_cmd, as_json=False) |
| 196 | + |
| 197 | +user_token = base64.b64decode(secret_data["data"]["token"]).decode("utf-8") |
| 198 | +set_credentials_cmd = [ |
| 199 | + "config", |
| 200 | + "set-credentials", |
| 201 | + context_name, |
| 202 | + "--token={}".format(user_token), |
| 203 | + "--kubeconfig={}".format(kube_config), |
| 204 | +] |
| 205 | +run_command(set_credentials_cmd, as_json=False, log_command=False) |
| 206 | + |
| 207 | +set_context_cmd = [ |
| 208 | + "config", |
| 209 | + "set-context", |
| 210 | + context_name, |
| 211 | + "--cluster={}".format(cluster_name), |
| 212 | + "--user={}".format(context_name), |
| 213 | + "--kubeconfig={}".format(kube_config), |
| 214 | +] |
| 215 | +run_command(set_context_cmd, as_json=False) |
| 216 | + |
| 217 | +use_context_cmd = ["config", "use-context", context_name, "--kubeconfig={}".format(kube_config)] |
| 218 | +run_command(use_context_cmd, as_json=False) |
| 219 | + |
| 220 | +print("Generated the kubeconfig file: {}".format(kube_config)) |
0 commit comments