From 43a043a218ff386e2cf18a42bea5c57209e48dae Mon Sep 17 00:00:00 2001 From: Thomas Rawley Date: Wed, 6 Sep 2023 10:49:22 -0400 Subject: [PATCH 01/10] changes for kube exec --- src/pybritive/britive_cli.py | 127 +++++++++++++++--- src/pybritive/choices/mode.py | 4 +- src/pybritive/commands/checkout.py | 1 + .../helpers/aws_credential_process.py | 5 +- src/pybritive/helpers/cache.py | 27 ++-- .../helpers/cloud_credential_printer.py | 59 ++++++++ .../helpers/profile_argument_dectorator.py | 31 ++++- 7 files changed, 213 insertions(+), 41 deletions(-) diff --git a/src/pybritive/britive_cli.py b/src/pybritive/britive_cli.py index 96ee9b4..4de41bb 100644 --- a/src/pybritive/britive_cli.py +++ b/src/pybritive/britive_cli.py @@ -43,6 +43,16 @@ def __init__(self, tenant_name: str = None, token: str = None, silent: bool = Fa self.credential_manager = None self.verbose_checkout = False self.checkout_progress_previous_message = None + self.cachable_modes = { + 'awscredentialprocess': { + 'app_type': 'AWS', + 'expiration_jmespath': 'expirationTime' + }, + 'kube-exec': { + 'app_type': 'Kubernetes', + 'expiration_jmespath': 'user.expiration' + } + } def set_output_format(self, output_format: str): self.output_format = self.config.get_output_format(output_format) @@ -337,7 +347,7 @@ def _get_app_type(self, application_id): raise click.ClickException(f'Application {application_id} not found') def __get_cloud_credential_printer(self, app_type, console, mode, profile, silent, credentials, - aws_credentials_file, gcloud_key_file): + aws_credentials_file, gcloud_key_file, kube_exec_api_version): if app_type in ['AWS', 'AWS Standalone']: return printer.AwsCloudCredentialPrinter( console=console, @@ -367,6 +377,16 @@ def __get_cloud_credential_printer(self, app_type, console, mode, profile, silen cli=self, gcloud_key_file=gcloud_key_file ) + elif app_type in ['Kubernetes']: + return printer.KubernetesCredentialPrinter( + console=console, + mode=mode, + profile=profile, + credentials=credentials, + silent=silent, + cli=self, + api_version=kube_exec_api_version + ) else: return printer.GenericCloudCredentialPrinter( console=console, @@ -468,9 +488,69 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, force_renew, aws_credentials_file, gcloud_key_file, verbose): credentials = None app_type = None - credential_process_creds_found = False + cached_credentials_found = False response = None self.verbose_checkout = verbose + kube_exec_api_version = None + kube_creds = None + + # handle kube-exec since the profile is actually going to be passed in via another method + # and perform some basic validation so we don't waste time performing a checkout when we + # will not be able to return a response back to kubectl via the exec command + if mode == 'kube-exec' or 'KUBERNETES_EXEC_INFO' in os.environ: + mode = 'kube-exec' + exec_data = json.loads(os.getenv('KUBERNETES_EXEC_INFO')) + + if not exec_data: # this env var HAS to exist if we are being invoked by k8s kubeconfig exec command + raise Exception('could not find environment variable KUBERNETES_EXEC_INFO') + + kube_exec_api_version = exec_data.get('apiVersion') + + if not kube_exec_api_version: + raise ValueError('apiVersion not found. Cannot continue.') + + if kube_exec_api_version == 'client.authentication.k8s.io/v1alpha1': + raise ValueError(f'apiVersion {kube_exec_api_version} is not supported.') + + #### REMOVE AFTER TESTING ### + # without group system:masters as GKE doesn't like it + # Error from server (Forbidden): groups "system:masters" is forbidden: + # User "system:serviceaccount:anthos-identity-service:gke-oidc-envoy-sa" cannot impersonate resource + # "groups" in API group "" at the cluster scope: GKE Warden authz [denied by user-impersonation-limitation]: + # impersonating system identities are not allowed + gke_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyZGFiMTJhLTExZmYtNDQyOC05YzRjLTkzZmViZmZjNzAwNyIsInR5cCI6IkpXVCJ9" \ + ".eyJzdWIiOiJrdWJlcm5ldGVzLWFkbWluIiwiZ3JvdXBzIjpbXSwiaXNzIjoiaHR0cHM6Ly90ZXN0MS5kZXYyLmF3cy5icml" \ + "0aXZlLWFwcC5jb20vYXBpL29pZGMvdGVzdCIsImF1ZCI6InRlc3QiLCJleHAiOjE3MjMyMTM2MTUsImlhdCI6MTY5MTU5MTI" \ + "xNiwibmJmIjoxNjkxNTkxMjE2fQ.j3G-J4MsgNBwVkMmEfov-39SmIzuEAOLoq6_QebFwAYPNJpPrmxQd0KxsKu3meIFlj-M" \ + "qmJ4_3nEleklyQ9iOCgLt91Kb9vNt55ooFTymxm9iBpAkBW14sDbOfBACtoFNAxZ116S8cfeLqGIcNRw4t1bB0F97E0yX1Pk" \ + "T-12gDLsFGXOKMhWlcLviTuJn75tEp67E5VNEwQPuw5wAVemggprEmxVj6CqTuZK9YjurKyb2ANLW7lk-OzSEtsqAXNzPpC1" \ + "0T09e3SDuaFID_W972CUDsDlYP5qHNoKx5-k0dwsIQ1dOTVeEun84Jh1Kzv_JQNjx4Pm50bvWDt3uxUfyg" + + # eks token which does allow system:master + eks_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyZGFiMTJhLTExZmYtNDQyOC05YzRjLTkzZmViZmZjNzAwNyIsInR5cCI6IkpXVCJ9" \ + ".eyJzdWIiOiJrdWJlcm5ldGVzLWFkbWluIiwiZ3JvdXBzIjpbInN5c3RlbTptYXN0ZXJzIl0sImlzcyI6Imh0dHBzOi8vdGV" \ + "zdDEuZGV2Mi5hd3MuYnJpdGl2ZS1hcHAuY29tL2FwaS9vaWRjL3Rlc3QiLCJhdWQiOiJ0ZXN0IiwiZXhwIjoxNzIzMjEzNjE" \ + "1LCJpYXQiOjE2OTE1OTEyMTYsIm5iZiI6MTY5MTU5MTIxNn0.bXcAOCHUBxBCwJDnTvxxEuNlQfW5y8d--i_HgtgYL1ptKNx" \ + "GU2aAaEIXhNd81ArVetcHDZmG41rODCAuLmUm5aZN8EKXO4FyqlP9zaFb8JNjSZ_U0KbBUymWrx_KgSyqoue_qEYXa-BCzEq" \ + "nFyXHEdpIJYvQ3KB2dAmXTpPQtdAls7D0g6oKhGJYAwwMSr_bkKRecn1y6ctetiKno2CE_OrGikU7LPgijj4G-71d_cT7jRM" \ + "FrA7VOUdTJ1CmrWTRT22b8RSlqCVePUmOyQ9Zg-2na0gn39WR1hmUpXQQrqPpdyIxuc4lmZT3oR21qDT0CENJxdVmROWb5aH4cFLiXw" + + kube_creds = { + 'user': { + 'token': gke_token if 'gke' in profile.lower() else eks_token, + 'issuer_url': 'https://test1.dev2.aws.britive-app.com/api/oidc/test', + 'client_id': 'test', + 'expiration': '2024-08-09T22:50:09Z', + 'expiration_epoch': 1723243809 + }, + 'environment': { + 'cluster': { + 'server': exec_data['spec']['cluster']['server'], + 'certificate_authority_data': exec_data['spec']['cluster']['certificate-authority-data'] + }, + 'name': 'environ_name' + } + } # these 2 modes implicitly say that console access should be checked out without having to provide # the --console flag @@ -479,24 +559,25 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, self._validate_justification(justification) - if mode == 'awscredentialprocess': - self.silent = True # the aws credential process CANNOT output anything other than the expected JSON - # we need to check the credential process cache for the credentials first - # then check to see if they are expired - # if not simply return those credentials - # if they are expired - app_type = 'AWS' # just hardcode as we know for sure this is for AWS - credentials = Cache(passphrase=passphrase).get_awscredentialprocess(profile_name=alias or profile) + if mode in self.cachable_modes: + self.silent = True # CANNOT output anything other than the expected JSON + # we need to check the cache for the credentials first and then check to see if they are expired + # if not simply return those credentials, if they are expired, continue to do an actual checkout + app_type = self.cachable_modes[mode]['app_type'] + credentials = Cache(passphrase=passphrase).get_credentials(profile_name=alias or profile, mode=mode) if credentials: - expiration_timestamp_str = credentials['expirationTime'].replace('Z', '') + expiration_timestamp_str = jmespath.search( + expression=self.cachable_modes[mode]['expiration_jmespath'], + data=credentials + ).replace('Z', '') expires = datetime.fromisoformat(expiration_timestamp_str) now = datetime.utcnow() if now >= expires: # check to ensure the credentials are still valid, if not, set to None and get new credentials = None else: - credential_process_creds_found = True + cached_credentials_found = True - parts = self._split_profile_into_parts(profile) + parts = self._split_profile_into_parts(profile) if not kube_creds else {'profile': profile, 'env': 'env', 'app': 'app'} # create this params once so we can use it multiple places params = { @@ -509,10 +590,10 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, 'justification': justification } - if not credential_process_creds_found: # nothing found via aws cred process or not aws cred process mode - response = self._checkout(**params) - app_type = self._get_app_type(response['appContainerId']) - credentials = response['credentials'] + if not cached_credentials_found: # nothing found in cache, cache is expired, or not a cachable mode + response = kube_creds or self._checkout(**params) + app_type = kube_creds or self._get_app_type(response['appContainerId']) + credentials = kube_creds or response['credentials'] # this handles the --force-renew flag # lets check to see if we should checkin this profile first and check it out again @@ -524,16 +605,17 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, self.print('checking in the profile to get renewed credentials....standby') self.checkin(profile=profile) response = self._checkout(**params) - credential_process_creds_found = False # need to write new creds to cache + cached_credentials_found = False # need to write new creds to cache credentials = response['credentials'] if alias: # do this down here, so we know that the profile is valid and a checkout was successful self.config.save_profile_alias(alias=alias, profile=profile) - if mode == 'awscredentialprocess' and not credential_process_creds_found: - Cache(passphrase=passphrase).save_awscredentialprocess( + if mode in self.cachable_modes and not cached_credentials_found: + Cache(passphrase=passphrase).save_credentials( profile_name=alias or profile, - credentials=credentials + credentials=credentials, + mode=mode ) self.__get_cloud_credential_printer( @@ -544,7 +626,8 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, self.silent, credentials, aws_credentials_file, - gcloud_key_file + gcloud_key_file, + kube_exec_api_version ).print() def import_existing_npm_config(self): diff --git a/src/pybritive/choices/mode.py b/src/pybritive/choices/mode.py index 06b8d18..cfc62ad 100644 --- a/src/pybritive/choices/mode.py +++ b/src/pybritive/choices/mode.py @@ -23,7 +23,9 @@ 'browser-macosx', 'browser-safari', 'browser-chrome', - 'browser-chromium' + 'browser-chromium', + 'kube-exec', # bake into kubeconfig with oidc exec output and additional caching to make kubectl more performant + 'kube-config', # create a kubeconfig file with user oidc credentials and optionally cluster/context in ~/.britive/kubeconfig/ ], case_sensitive=False ) diff --git a/src/pybritive/commands/checkout.py b/src/pybritive/commands/checkout.py index ae2a864..c167f70 100644 --- a/src/pybritive/commands/checkout.py +++ b/src/pybritive/commands/checkout.py @@ -2,6 +2,7 @@ from ..helpers.build_britive import build_britive from ..options.britive_options import britive_options from ..helpers.profile_argument_dectorator import click_smart_profile_argument +import os @click.command() diff --git a/src/pybritive/helpers/aws_credential_process.py b/src/pybritive/helpers/aws_credential_process.py index 1592c97..10a1ab3 100644 --- a/src/pybritive/helpers/aws_credential_process.py +++ b/src/pybritive/helpers/aws_credential_process.py @@ -59,7 +59,10 @@ def main(): creds = None if not args['force_renew']: # if force renew let's defer to that the full package vs. this helper from .cache import Cache # lazy load - creds = Cache(passphrase=args['passphrase']).get_awscredentialprocess(profile_name=args['profile']) + creds = Cache(passphrase=args['passphrase']).get_credentials( + profile_name=args['profile'], + mode='awscredentialprocess' + ) if creds: from datetime import datetime # lazy load expiration = datetime.fromisoformat(creds['expirationTime'].replace('Z', '')) diff --git a/src/pybritive/helpers/cache.py b/src/pybritive/helpers/cache.py index ec5fa19..410f865 100644 --- a/src/pybritive/helpers/cache.py +++ b/src/pybritive/helpers/cache.py @@ -1,8 +1,6 @@ from pathlib import Path import json import os - - from .encryption import StringEncryption, InvalidPassphraseException @@ -13,6 +11,11 @@ def __init__(self, passphrase: str = None): home = os.getenv('PYBRITIVE_HOME_DIR', str(Path.home())) self.path = str(Path(home) / '.britive' / 'pybritive.cache') # handle os specific separators properly self.cache = {} + self.default_key_values = { + 'profiles': [], + 'awscredentialprocess': {}, + 'kube-exec': {} + } self.load() def load(self): @@ -30,10 +33,7 @@ def load(self): except json.decoder.JSONDecodeError: self.cache = {} - if 'profiles' not in self.cache.keys(): - self.cache['profiles'] = [] - if 'awscredentialprocess' not in self.cache.keys(): - self.cache['awscredentialprocess'] = {} + self.cache = {**self.default_key_values, **self.cache} def write(self): # write the new cache file @@ -50,24 +50,23 @@ def save_profiles(self, profiles: list): self.write() def clear(self): - self.cache['profiles'] = [] - self.cache['awscredentialprocess'] = {} + self.cache = self.default_key_values self.write() - def get_awscredentialprocess(self, profile_name: str): + def get_credentials(self, profile_name: str, mode: str = 'awscredentialprocess'): try: - ciphertext = self.cache['awscredentialprocess'].get(profile_name.lower()) + ciphertext = self.cache[mode].get(profile_name.lower()) if not ciphertext: return None return json.loads(self.string_encryptor.decrypt(ciphertext)) except InvalidPassphraseException: # if we cannot decrypt don't error - just make the API call to get the creds return None - def save_awscredentialprocess(self, profile_name: str, credentials: dict): + def save_credentials(self, profile_name: str, credentials: dict, mode: str = 'awscredentialprocess'): ciphertext = self.string_encryptor.encrypt(json.dumps(credentials, default=str)) - self.cache['awscredentialprocess'][profile_name.lower()] = ciphertext + self.cache[mode][profile_name.lower()] = ciphertext self.write() - def clear_awscredentialprocess(self, profile_name: str): - self.cache['awscredentialprocess'].pop(profile_name.lower(), None) + def clear_credentials(self, profile_name: str, mode: str = 'awscredentialprocess'): + self.cache[mode].pop(profile_name.lower(), None) self.write() diff --git a/src/pybritive/helpers/cloud_credential_printer.py b/src/pybritive/helpers/cloud_credential_printer.py index 681bbc2..dbe1be3 100644 --- a/src/pybritive/helpers/cloud_credential_printer.py +++ b/src/pybritive/helpers/cloud_credential_printer.py @@ -5,6 +5,7 @@ import configparser import webbrowser from pathlib import Path +import os # trailing spaces matter as some options do not have the trailing space @@ -61,6 +62,8 @@ def print(self): self.print_azps() if self.mode == 'gcloudauth': self.print_gcloudauth() + if self.mode == 'kube': + self.print_kube() def print_console(self): url = self.credentials.get('url', self.credentials) @@ -93,6 +96,9 @@ def print_azps(self): def print_gcloudauth(self): self._not_implemented() + def print_kube(self): + self._not_implemented() + def _not_implemented(self): raise click.ClickException(f'Application type {self.app_type} does not support the specified mode.') @@ -251,3 +257,56 @@ def print_gcloudauth(self): f"gcloud auth activate-service-account {self.credentials['client_email']} --key-file {str(path)}", ignore_silent=True ) + + +class KubernetesCredentialPrinter(CloudCredentialPrinter): + def __init__(self, console, mode, profile, silent, credentials, cli, api_version): + self.api_version = api_version + super().__init__('Kubernetes', console, mode, profile, silent, credentials, cli) + + # expected kube credentials format is + # creds = { + # 'user': { + # 'token': '...', + # 'issuer_url': '...', + # 'client_id': '...', + # 'expiration': '...', + # 'expiration_epoch': '...' + # }, + # 'environment': { + # 'cluster': { + # 'server': '...', + # 'certificate_authority_data': '...' + # }, + # 'name': '...' + # } + # } + + def print_json(self): + try: + self.cli.print(json.dumps(self.credentials, indent=2), ignore_silent=True) + except json.JSONDecodeError: + self.cli.print(self.credentials, ignore_silent=True) + + def print_kube(self): + if self.mode_modifier == 'exec': + if self.api_version in ['client.authentication.k8s.io/v1', 'client.authentication.k8s.io/v1beta1']: + response = { + 'kind': 'ExecCredential', + 'apiVersion': self.api_version, + 'spec': {}, + 'status': { + 'expirationTimestamp': self.credentials['user']['expiration'], + 'token': self.credentials['user']['token'] + } + } + self.cli.print(json.dumps(response), ignore_silent=True) + else: + raise Exception(f'apiVersion {self.api_version} not accounted for.') + elif self.mode_modifier == 'config': + # write file to ~/.britive/kubeconfig/... + # clean up any older config file that are no longer required + # print out `export KUBECONFIG=~/.britive/kubeconfig/...` + pass + else: + raise ValueError(f'--mode modifier {self.mode_modifier} for mode {self.mode} not supported') diff --git a/src/pybritive/helpers/profile_argument_dectorator.py b/src/pybritive/helpers/profile_argument_dectorator.py index 3500dca..cafa80a 100644 --- a/src/pybritive/helpers/profile_argument_dectorator.py +++ b/src/pybritive/helpers/profile_argument_dectorator.py @@ -1,15 +1,40 @@ import click import pkg_resources from ..completers.profile import profile_completer +import os +import json + click_major_version = int(pkg_resources.get_distribution('click').version.split('.')[0]) +def validate_profile(ctx, param, value): + if 'KUBERNETES_EXEC_INFO' in os.environ: + try: + return json.loads(os.getenv('KUBERNETES_EXEC_INFO'))['spec']['cluster']['config']['britive-profile'] + except: + raise ValueError('unable to find britive profile via cluster exec-extension with name britive-profile') + return value + + +def is_required(): + return 'KUBERNETES_EXEC_INFO' not in os.environ + + def click_smart_profile_argument(func): + required = is_required() + kwargs = { + 'required': required + } + if not required: + kwargs['callback'] = validate_profile if click_major_version >= 8: - dec = click.argument('profile', shell_complete=profile_completer) - else: - dec = click.argument('profile') + kwargs['shell_complete'] = profile_completer + + dec = click.argument( + 'profile', + **kwargs + ) return dec(func) From 1490c93b7670e1c757e411e6694850d3d761f499 Mon Sep 17 00:00:00 2001 From: twratl Date: Fri, 13 Oct 2023 08:00:39 -0400 Subject: [PATCH 02/10] k8s work --- setup.cfg | 5 +- src/pybritive/britive_cli.py | 76 +++---------- .../helpers/cloud_credential_printer.py | 36 +------ src/pybritive/helpers/k8s_exec.py | 100 ++++++++++++++++++ .../helpers/k8s_exec_credential_builder.py | 55 ++++++++++ 5 files changed, 173 insertions(+), 99 deletions(-) create mode 100644 src/pybritive/helpers/k8s_exec.py create mode 100644 src/pybritive/helpers/k8s_exec_credential_builder.py diff --git a/setup.cfg b/setup.cfg index 2d5777e..baa0d13 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pybritive -version = 1.5.0rc1 +version = 1.5.0rc3 author = Britive Inc. author_email = support@britive.com description = A pure Python CLI for Britive @@ -36,4 +36,5 @@ where = src [options.entry_points] console_scripts = pybritive = pybritive.cli_interface:safe_cli - pybritive-aws-cred-process = pybritive.helpers.aws_credential_process:main \ No newline at end of file + pybritive-aws-cred-process = pybritive.helpers.aws_credential_process:main + pybritive-kube-exec = pybritive.helpers.k8s_exec:main \ No newline at end of file diff --git a/src/pybritive/britive_cli.py b/src/pybritive/britive_cli.py index 4de41bb..254470b 100644 --- a/src/pybritive/britive_cli.py +++ b/src/pybritive/britive_cli.py @@ -50,7 +50,7 @@ def __init__(self, tenant_name: str = None, token: str = None, silent: bool = Fa }, 'kube-exec': { 'app_type': 'Kubernetes', - 'expiration_jmespath': 'user.expiration' + 'expiration_jmespath': 'expirationTime' } } @@ -347,7 +347,7 @@ def _get_app_type(self, application_id): raise click.ClickException(f'Application {application_id} not found') def __get_cloud_credential_printer(self, app_type, console, mode, profile, silent, credentials, - aws_credentials_file, gcloud_key_file, kube_exec_api_version): + aws_credentials_file, gcloud_key_file, k8s_processor): if app_type in ['AWS', 'AWS Standalone']: return printer.AwsCloudCredentialPrinter( console=console, @@ -385,7 +385,7 @@ def __get_cloud_credential_printer(self, app_type, console, mode, profile, silen credentials=credentials, silent=silent, cli=self, - api_version=kube_exec_api_version + k8s_processor=k8s_processor ) else: return printer.GenericCloudCredentialPrinter( @@ -489,68 +489,16 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, credentials = None app_type = None cached_credentials_found = False - response = None + k8s_processor = None self.verbose_checkout = verbose - kube_exec_api_version = None - kube_creds = None # handle kube-exec since the profile is actually going to be passed in via another method # and perform some basic validation so we don't waste time performing a checkout when we # will not be able to return a response back to kubectl via the exec command if mode == 'kube-exec' or 'KUBERNETES_EXEC_INFO' in os.environ: - mode = 'kube-exec' - exec_data = json.loads(os.getenv('KUBERNETES_EXEC_INFO')) - - if not exec_data: # this env var HAS to exist if we are being invoked by k8s kubeconfig exec command - raise Exception('could not find environment variable KUBERNETES_EXEC_INFO') - - kube_exec_api_version = exec_data.get('apiVersion') - - if not kube_exec_api_version: - raise ValueError('apiVersion not found. Cannot continue.') - - if kube_exec_api_version == 'client.authentication.k8s.io/v1alpha1': - raise ValueError(f'apiVersion {kube_exec_api_version} is not supported.') - - #### REMOVE AFTER TESTING ### - # without group system:masters as GKE doesn't like it - # Error from server (Forbidden): groups "system:masters" is forbidden: - # User "system:serviceaccount:anthos-identity-service:gke-oidc-envoy-sa" cannot impersonate resource - # "groups" in API group "" at the cluster scope: GKE Warden authz [denied by user-impersonation-limitation]: - # impersonating system identities are not allowed - gke_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyZGFiMTJhLTExZmYtNDQyOC05YzRjLTkzZmViZmZjNzAwNyIsInR5cCI6IkpXVCJ9" \ - ".eyJzdWIiOiJrdWJlcm5ldGVzLWFkbWluIiwiZ3JvdXBzIjpbXSwiaXNzIjoiaHR0cHM6Ly90ZXN0MS5kZXYyLmF3cy5icml" \ - "0aXZlLWFwcC5jb20vYXBpL29pZGMvdGVzdCIsImF1ZCI6InRlc3QiLCJleHAiOjE3MjMyMTM2MTUsImlhdCI6MTY5MTU5MTI" \ - "xNiwibmJmIjoxNjkxNTkxMjE2fQ.j3G-J4MsgNBwVkMmEfov-39SmIzuEAOLoq6_QebFwAYPNJpPrmxQd0KxsKu3meIFlj-M" \ - "qmJ4_3nEleklyQ9iOCgLt91Kb9vNt55ooFTymxm9iBpAkBW14sDbOfBACtoFNAxZ116S8cfeLqGIcNRw4t1bB0F97E0yX1Pk" \ - "T-12gDLsFGXOKMhWlcLviTuJn75tEp67E5VNEwQPuw5wAVemggprEmxVj6CqTuZK9YjurKyb2ANLW7lk-OzSEtsqAXNzPpC1" \ - "0T09e3SDuaFID_W972CUDsDlYP5qHNoKx5-k0dwsIQ1dOTVeEun84Jh1Kzv_JQNjx4Pm50bvWDt3uxUfyg" - - # eks token which does allow system:master - eks_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyZGFiMTJhLTExZmYtNDQyOC05YzRjLTkzZmViZmZjNzAwNyIsInR5cCI6IkpXVCJ9" \ - ".eyJzdWIiOiJrdWJlcm5ldGVzLWFkbWluIiwiZ3JvdXBzIjpbInN5c3RlbTptYXN0ZXJzIl0sImlzcyI6Imh0dHBzOi8vdGV" \ - "zdDEuZGV2Mi5hd3MuYnJpdGl2ZS1hcHAuY29tL2FwaS9vaWRjL3Rlc3QiLCJhdWQiOiJ0ZXN0IiwiZXhwIjoxNzIzMjEzNjE" \ - "1LCJpYXQiOjE2OTE1OTEyMTYsIm5iZiI6MTY5MTU5MTIxNn0.bXcAOCHUBxBCwJDnTvxxEuNlQfW5y8d--i_HgtgYL1ptKNx" \ - "GU2aAaEIXhNd81ArVetcHDZmG41rODCAuLmUm5aZN8EKXO4FyqlP9zaFb8JNjSZ_U0KbBUymWrx_KgSyqoue_qEYXa-BCzEq" \ - "nFyXHEdpIJYvQ3KB2dAmXTpPQtdAls7D0g6oKhGJYAwwMSr_bkKRecn1y6ctetiKno2CE_OrGikU7LPgijj4G-71d_cT7jRM" \ - "FrA7VOUdTJ1CmrWTRT22b8RSlqCVePUmOyQ9Zg-2na0gn39WR1hmUpXQQrqPpdyIxuc4lmZT3oR21qDT0CENJxdVmROWb5aH4cFLiXw" - - kube_creds = { - 'user': { - 'token': gke_token if 'gke' in profile.lower() else eks_token, - 'issuer_url': 'https://test1.dev2.aws.britive-app.com/api/oidc/test', - 'client_id': 'test', - 'expiration': '2024-08-09T22:50:09Z', - 'expiration_epoch': 1723243809 - }, - 'environment': { - 'cluster': { - 'server': exec_data['spec']['cluster']['server'], - 'certificate_authority_data': exec_data['spec']['cluster']['certificate-authority-data'] - }, - 'name': 'environ_name' - } - } + mode = 'kube-exec' # set for downstream processes if we are basing this only on the env var being present + from .helpers.k8s_exec_credential_builder import KubernetesExecCredentialProcessor + k8s_processor = KubernetesExecCredentialProcessor() # these 2 modes implicitly say that console access should be checked out without having to provide # the --console flag @@ -577,7 +525,7 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, else: cached_credentials_found = True - parts = self._split_profile_into_parts(profile) if not kube_creds else {'profile': profile, 'env': 'env', 'app': 'app'} + parts = self._split_profile_into_parts(profile) # create this params once so we can use it multiple places params = { @@ -591,9 +539,9 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, } if not cached_credentials_found: # nothing found in cache, cache is expired, or not a cachable mode - response = kube_creds or self._checkout(**params) - app_type = kube_creds or self._get_app_type(response['appContainerId']) - credentials = kube_creds or response['credentials'] + response = self._checkout(**params) + app_type = self._get_app_type(response['appContainerId']) + credentials = response['credentials'] # this handles the --force-renew flag # lets check to see if we should checkin this profile first and check it out again @@ -627,7 +575,7 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, credentials, aws_credentials_file, gcloud_key_file, - kube_exec_api_version + k8s_processor ).print() def import_existing_npm_config(self): diff --git a/src/pybritive/helpers/cloud_credential_printer.py b/src/pybritive/helpers/cloud_credential_printer.py index dbe1be3..5766cd9 100644 --- a/src/pybritive/helpers/cloud_credential_printer.py +++ b/src/pybritive/helpers/cloud_credential_printer.py @@ -260,28 +260,10 @@ def print_gcloudauth(self): class KubernetesCredentialPrinter(CloudCredentialPrinter): - def __init__(self, console, mode, profile, silent, credentials, cli, api_version): - self.api_version = api_version + def __init__(self, console, mode, profile, silent, credentials, cli, k8s_processor): + self.k8s_processor = k8s_processor super().__init__('Kubernetes', console, mode, profile, silent, credentials, cli) - # expected kube credentials format is - # creds = { - # 'user': { - # 'token': '...', - # 'issuer_url': '...', - # 'client_id': '...', - # 'expiration': '...', - # 'expiration_epoch': '...' - # }, - # 'environment': { - # 'cluster': { - # 'server': '...', - # 'certificate_authority_data': '...' - # }, - # 'name': '...' - # } - # } - def print_json(self): try: self.cli.print(json.dumps(self.credentials, indent=2), ignore_silent=True) @@ -290,19 +272,7 @@ def print_json(self): def print_kube(self): if self.mode_modifier == 'exec': - if self.api_version in ['client.authentication.k8s.io/v1', 'client.authentication.k8s.io/v1beta1']: - response = { - 'kind': 'ExecCredential', - 'apiVersion': self.api_version, - 'spec': {}, - 'status': { - 'expirationTimestamp': self.credentials['user']['expiration'], - 'token': self.credentials['user']['token'] - } - } - self.cli.print(json.dumps(response), ignore_silent=True) - else: - raise Exception(f'apiVersion {self.api_version} not accounted for.') + self.cli.print(self.k8s_processor.construct_exec_credential(self.credentials), ignore_silent=True) elif self.mode_modifier == 'config': # write file to ~/.britive/kubeconfig/... # clean up any older config file that are no longer required diff --git a/src/pybritive/helpers/k8s_exec.py b/src/pybritive/helpers/k8s_exec.py new file mode 100644 index 0000000..04a6052 --- /dev/null +++ b/src/pybritive/helpers/k8s_exec.py @@ -0,0 +1,100 @@ +import sys + + +sys.tracebacklimit = 0 + + +def get_args(): + from getopt import getopt # lazy load + from sys import argv # lazy load + options, non_options = getopt(argv[1:], 't:T:p:f:P:hv', [ + 'tenant=', + 'token=', + 'passphrase=', + 'help', + 'version' + ]) + + args = { + 'tenant': None, + 'token': None, + 'passphrase': None + } + + for opt, arg in options: + if opt in ('-t', '--tenant'): + args['tenant'] = arg + if opt in ('-T', '--token'): + args['token'] = arg + if opt in ('-p', '--passphrase'): + args['passphrase'] = arg + if opt in ('-h', '--help'): + usage() + if opt in ('-v', '--version'): + from platform import platform, python_version # lazy load + from pkg_resources import get_distribution # lazy load + cli_version = get_distribution('pybritive').version + print( + f'pybritive: {cli_version} / platform: {platform()} / python: {python_version()}' + ) + exit() + + return args + + +def usage(): + from sys import argv # lazy load + print("Usage : %s [-t/--tenant, -T/--token, -t/--passphrase, -f/--force-renew]" % (argv[0])) + exit() + + +def process(): + args = get_args() + + from .k8s_exec_credential_builder import KubernetesExecCredentialProcessor + + k8s_processor = KubernetesExecCredentialProcessor() + + from .cache import Cache # lazy load + creds = Cache(passphrase=args['passphrase']).get_credentials( + profile_name=k8s_processor.profile, + mode='kube-exec' + ) + if creds: + from datetime import datetime # lazy load + expiration = datetime.fromisoformat(creds['expirationTime'].replace('Z', '')) + now = datetime.utcnow() + if now > expiration: # creds have expired so set to none so new one get checked out + creds = None + else: + print(k8s_processor.construct_exec_credential(creds)) + exit() + + if not creds: + from ..britive_cli import BritiveCli # lazy load for performance purposes + + b = BritiveCli(tenant_name=args['tenant'], token=args['token'], passphrase=args['passphrase'], silent=True) + b.config.get_tenant() # have to load the config here as that work is generally done elsewhere + b.checkout( + alias=None, + blocktime=None, + console=False, + justification=None, + mode='kube-exec', + maxpolltime=None, + profile=k8s_processor.profile, + passphrase=args['passphrase'], + force_renew=None, + aws_credentials_file=None, + gcloud_key_file=None, + verbose=None + ) + exit() + + +def main(): + process() + + +if __name__ == '__main__': + main() diff --git a/src/pybritive/helpers/k8s_exec_credential_builder.py b/src/pybritive/helpers/k8s_exec_credential_builder.py new file mode 100644 index 0000000..f8959f7 --- /dev/null +++ b/src/pybritive/helpers/k8s_exec_credential_builder.py @@ -0,0 +1,55 @@ +import json +import os + + +class KubernetesExecCredentialProcessor: + def __init__(self): + self.api_version = None + self.profile = None + self.exec_data = None + self._parse() + + def _parse(self): + # parse the information provided by kube exec + self.exec_data = json.loads(os.environ.get('KUBERNETES_EXEC_INFO', 'null')) + + if not self.exec_data: # this env var HAS to exist if we are being invoked by k8s kubeconfig exec command + raise Exception( + 'could not find environment variable KUBERNETES_EXEC_INFO - is this command being run ' + 'within a kubeconfig context?' + ) + + self.api_version = self.exec_data.get('apiVersion') + + if not self.api_version: + raise ValueError('apiVersion not found. Cannot continue.') + + if self.api_version == 'client.authentication.k8s.io/v1alpha1': + raise ValueError(f'apiVersion {self.api_version} is not supported.') + + if self.api_version not in ['client.authentication.k8s.io/v1', 'client.authentication.k8s.io/v1beta1']: + raise Exception(f'apiVersion {self.api_version} not accounted for.') + + self.profile = self.exec_data.get('spec', {}).get('cluster', {}).get('config', {}).get('britive-profile') + + if not self.profile: + raise ValueError( + 'kubeconfig cluster extension named britive-profile not found or no profile value specified' + ) + + def construct_exec_credential(self, credentials: dict): + response = { + 'kind': 'ExecCredential', + 'apiVersion': self.api_version, + 'spec': { + 'cluster': { + 'server': 'https://AFEA586304825E6AE82B38EA8B8665D2.gr7.us-west-1.eks.amazonaws.com', + 'certificate-authority-data': 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJek1EUXhNREUwTkRRd01Wb1hEVE16TURRd056RTBORFF3TVZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTTZUCk93Q25NclkzK0lhY1ZqUFpGYitxdi9GT1ZtQVJhOGVjZXNvN0VlblhOT1hod05sNjF6Ykk0UGhVcjRwU2U5QU8KTGwwd0lBUkZheWdtdTk4YVc0bU44UjcyYkVLc041WlYvL2FNbTdwTVJab2dYRDdNWDFseVNqRzdYUDgzRzBVYQpXMGlhS0JPdE96Q0F6dzBOVXAzd0E5ZzV5bUNIeGk2V3ZMZHRWOU9PSnlKYjkzM3NDZGhsM3phWlN6QmlzL1daClJsU0xIODlveUZXY0w5NGF2UU1WeGRGZHg2TlFLZ0ZnMERsMXNGTlFYbFlTbXkyUS8zd3lubU1RSlZqcnV6a2gKUW1yNDRoSUNrUmtZQVd5OU1wR2kwSmswa0IrekkwQzZnWWhqOG1OTS9wZEk3Skd0OXBCWWMrOWdnYVRuQWN5SQpGMWxDYjNVYlQ0VU4vcXZqMFkwQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZPQi94ZXdrRENVaDA0Y2QvcXpHN0JJbFAzOGdNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSTV5T2FrMzNpanhhakZ4TkloNQpkek1iNVlxY1c1RlNUMmJOQlhITzQvSjRjNUduVWFvV1FDZnNJK0FHQ0tBMXl5WUxIZmJwVEt2TjNGUW9rWGlSCi9oSG1reGJqMTk2MlEwZDFWSkExdk1nUktLa3lBOTZYQ1UrN3hxci9HeExzN1huTWphNHhrcEJKaEw0RSt6b1kKNy9PcEZxR2ZpUitoSmtncWN1MTlQenczZmN4VVdDZHlLSjNNZVMwN1psTzZLMjBONnoxUUFrNXA3Mi9BTERnQgpHWTVUVFhmU2tHakhyU1pMWnVjb25ZM1BSci9iWmRGNEhUdWhHZzJ2aXA2SytCWjJLQUpURDBlUkQvYTRpY1pMCjl6R09YU21Hc1krakpDY0l5VnBrenpsdGczMVNHc0NCOEtPTlU4YzRhb0RJWVhOSnFybjRmYm5YZnI0VVEwam4KZmtZPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' + } + }, + 'status': { + 'expirationTimestamp': credentials['expirationTime'], + 'token': credentials['jwt'] + } + } + return json.dumps(response) From 5e8214ab56d2f0540c7b8352d647bfd250a4e568 Mon Sep 17 00:00:00 2001 From: twratl Date: Thu, 19 Oct 2023 16:52:22 -0400 Subject: [PATCH 03/10] kube exec helper - add federation provider --- src/pybritive/helpers/k8s_exec.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/pybritive/helpers/k8s_exec.py b/src/pybritive/helpers/k8s_exec.py index 04a6052..7a7ae97 100644 --- a/src/pybritive/helpers/k8s_exec.py +++ b/src/pybritive/helpers/k8s_exec.py @@ -7,10 +7,11 @@ def get_args(): from getopt import getopt # lazy load from sys import argv # lazy load - options, non_options = getopt(argv[1:], 't:T:p:f:P:hv', [ + options, non_options = getopt(argv[1:], 't:T:p:F:hv', [ 'tenant=', 'token=', 'passphrase=', + 'federation-provider=', 'help', 'version' ]) @@ -18,7 +19,8 @@ def get_args(): args = { 'tenant': None, 'token': None, - 'passphrase': None + 'passphrase': None, + 'federation_provider': None } for opt, arg in options: @@ -28,6 +30,8 @@ def get_args(): args['token'] = arg if opt in ('-p', '--passphrase'): args['passphrase'] = arg + if opt in ('-F', '--federation-provider'): + args['federation_provider'] = arg if opt in ('-h', '--help'): usage() if opt in ('-v', '--version'): @@ -44,7 +48,7 @@ def get_args(): def usage(): from sys import argv # lazy load - print("Usage : %s [-t/--tenant, -T/--token, -t/--passphrase, -f/--force-renew]" % (argv[0])) + print(f"Usage : {argv[0]} [-t/--tenant, -T/--token, -t/--passphrase, -F/--federation-provider]") exit() @@ -73,7 +77,13 @@ def process(): if not creds: from ..britive_cli import BritiveCli # lazy load for performance purposes - b = BritiveCli(tenant_name=args['tenant'], token=args['token'], passphrase=args['passphrase'], silent=True) + b = BritiveCli( + tenant_name=args['tenant'], + token=args['token'], + passphrase=args['passphrase'], + federation_provider=args['federation_provider'], + silent=True + ) b.config.get_tenant() # have to load the config here as that work is generally done elsewhere b.checkout( alias=None, From 357baf1a2fe89e544b9ec0df31b2d44f4742eaf0 Mon Sep 17 00:00:00 2001 From: twratl Date: Fri, 20 Oct 2023 08:57:32 -0400 Subject: [PATCH 04/10] versionn bump to 1.6.0rc1 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5590349..6527737 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pybritive -version = 1.5.0 +version = 1.6.0rc1 author = Britive Inc. author_email = support@britive.com description = A pure Python CLI for Britive From eb54cbec4bc88a2920fd9d979a623e5d4ec03fb4 Mon Sep 17 00:00:00 2001 From: twratl Date: Fri, 20 Oct 2023 08:57:54 -0400 Subject: [PATCH 05/10] uneeded comment removal --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 6527737..23387a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,6 @@ package_dir = packages = find: python_requires = >=3.7 -# urllib3 version logic due to https://github.com/britive/python-cli/security/dependabot/6 install_requires = click requests>=2.31.0 From 6149a770a8b06d90c844cc802ec073d6feed19f8 Mon Sep 17 00:00:00 2001 From: twratl Date: Mon, 23 Oct 2023 09:39:25 -0400 Subject: [PATCH 06/10] k8s work --- CHANGELOG.md | 20 ++++ src/pybritive/britive_cli.py | 56 +++++++-- src/pybritive/choices/mode.py | 1 - src/pybritive/commands/cache.py | 10 +- .../helpers/aws_credential_process.py | 3 +- src/pybritive/helpers/config.py | 24 +++- src/pybritive/helpers/k8s_exec.py | 19 +-- src/pybritive/helpers/kube_config_builder.py | 110 ++++++++++++++++++ 8 files changed, 212 insertions(+), 31 deletions(-) create mode 100644 src/pybritive/helpers/kube_config_builder.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ccffef7..2246392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ * As of v1.4.0 release candidates will be published in an effort to get new features out faster while still allowing time for full QA testing before moving the release candidate to a full release. +## v1.6.0rc1 [2023-10-XX] +#### What's New +* Initial support for Kubernetes - this functionality is not yet available publicly on the Britive Platform + +#### Enhancements +* Add command `cache kubeconfig` +* Add global config flag `auto-refresh-kube-config` set by `configure update global auto-refresh-kube-config true` +* Add checkout mode `k8s-exec` for use exclusively inside an `exec` command of a kube config file +* Add console helper script `pybritive-kube-exec` for use exclusively inside an `exec` command of a kube config file + +#### Bug Fixes +* None + +#### Dependencies +* None + +#### Other +* None + + ## v1.5.0 [2023-10-20] #### What's New * None diff --git a/src/pybritive/britive_cli.py b/src/pybritive/britive_cli.py index da4d93d..ca074f4 100644 --- a/src/pybritive/britive_cli.py +++ b/src/pybritive/britive_cli.py @@ -10,7 +10,6 @@ import sys import uuid import yaml - import click import jmespath from britive.britive import Britive @@ -28,9 +27,10 @@ class BritiveCli: - def __init__(self, tenant_name: str = None, token: str = None, silent: bool = False, - passphrase: str = None, federation_provider: str = None): + def __init__(self, tenant_name: str = None, token: str = None, silent: bool = False,passphrase: str = None, + federation_provider: str = None, from_helper_console_script: bool = False): self.silent = silent + self.from_helper_console_script = from_helper_console_script self.output_format = None self.tenant_name = None self.tenant_alias = None @@ -112,10 +112,10 @@ def login(self, explicit: bool = False, browser: str = None): except exceptions.UnauthorizedRequest: self._cleanup_credentials() - # if user called `pybritive login` and we should refresh the profile cache...do so - if explicit and self.config.auto_refresh_profile_cache(): - self._set_available_profiles() - self.cache_profiles() + # if user called `pybritive login` and we should get profiles...do so + should_get_profiles = any([self.config.auto_refresh_profile_cache(), self.config.auto_refresh_kube_config()]) + if explicit and should_get_profiles: + self._set_available_profiles() # will handle calling cache_profiles() and construct_kube_config() def _cleanup_credentials(self): self.set_credential_manager() @@ -337,12 +337,40 @@ def _set_available_profiles(self): 'profile_allows_console': profile['consoleAccess'], 'profile_allows_programmatic': profile['programmaticAccess'], 'profile_description': profile['profileDescription'], - '2_part_profile_format_allowed': app['requiresHierarchicalModel'] + '2_part_profile_format_allowed': app['requiresHierarchicalModel'], + 'env_properties': env.get('profileEnvironmentProperties', {}) } data.append(row) self.available_profiles = data if self.config.auto_refresh_profile_cache(): - self.cache_profiles(load=False) + self.cache_profiles() + if self.config.auto_refresh_kube_config(): + self.construct_kube_config() + + def construct_kube_config(self, from_cache_command=False): + if self.from_helper_console_script: + return + + if from_cache_command: + self.login() + self._set_available_profiles() + + profiles = [] + for p in self.available_profiles: + if p['app_type'].lower() == 'kubernetes': + props = p['env_properties'] + url = props.get('apiServerUrl') + cert = props.get('certificateAuthorityData') + if props and all([url, cert]): + profiles.append({ + 'app': p['app_name'], + 'env': p['env_name'], + 'profile': p['profile_name'], + 'url': url, + 'cert': cert, + }) + from .helpers.kube_config_builder import build_kube_config # lazy import as not everyone will want this + build_kube_config(profiles=profiles, config=self.config, username=self.b.my_access.whoami()['username']) def _get_app_type(self, application_id): self._set_available_profiles() @@ -692,11 +720,15 @@ def downloadsecret(self, path, blocktime, justification, maxpolltime, file): f.write(content) self.print(f'wrote contents of secret file to {path}') - def cache_profiles(self, load=True): - if load: + def cache_profiles(self, from_cache_command=False): + if self.from_helper_console_script: + return + profiles = [] + + if from_cache_command: self.login() self._set_available_profiles() - profiles = [] + for p in self.available_profiles: profile = self.escape_profile_element(p['app_name']) profile += '/' diff --git a/src/pybritive/choices/mode.py b/src/pybritive/choices/mode.py index 355554b..d48bab3 100644 --- a/src/pybritive/choices/mode.py +++ b/src/pybritive/choices/mode.py @@ -25,7 +25,6 @@ 'browser-chrome', 'browser-chromium', 'kube-exec', # bake into kubeconfig with oidc exec output and additional caching to make kubectl more performant - 'kube-config', # create a kubeconfig file with user oidc credentials and optionally cluster/context in ~/.britive/kubeconfig/ ], case_sensitive=False ) diff --git a/src/pybritive/commands/cache.py b/src/pybritive/commands/cache.py index 31fda3d..2cb14fc 100644 --- a/src/pybritive/commands/cache.py +++ b/src/pybritive/commands/cache.py @@ -14,7 +14,15 @@ def cache(): @britive_options(names='tenant,token,silent,passphrase,federation_provider') def profiles(ctx, tenant, token, silent, passphrase, federation_provider): """Cache profiles locally to facilitate auto-completion of profile names on checkin/checkout.""" - ctx.obj.britive.cache_profiles() + ctx.obj.britive.cache_profiles(from_cache_command=True) + + +@cache.command() +@build_britive +@britive_options(names='tenant,token,silent,passphrase,federation_provider') +def kubeconfig(ctx, tenant, token, silent, passphrase, federation_provider): + """Cache a Britive managed kube config file based on the profiles to which the caller has access.""" + ctx.obj.britive.construct_kube_config(from_cache_command=True) @cache.command() diff --git a/src/pybritive/helpers/aws_credential_process.py b/src/pybritive/helpers/aws_credential_process.py index 7b54244..bcfae2b 100644 --- a/src/pybritive/helpers/aws_credential_process.py +++ b/src/pybritive/helpers/aws_credential_process.py @@ -93,7 +93,8 @@ def main(): token=args['token'], passphrase=args['passphrase'], federation_provider=args['federation_provider'], - silent=True + silent=True, + from_helper_console_script=True ) b.config.get_tenant() # have to load the config here as that work is generally done b.checkout( diff --git a/src/pybritive/helpers/config.py b/src/pybritive/helpers/config.py index 6f97df4..aa1662f 100644 --- a/src/pybritive/helpers/config.py +++ b/src/pybritive/helpers/config.py @@ -46,7 +46,8 @@ def coalesce(*arg): 'default_tenant', 'output_format', 'credential_backend', - 'auto-refresh-profile-cache' + 'auto-refresh-profile-cache', + 'auto-refresh-kube-config' ] tenant_fields = [ @@ -64,7 +65,8 @@ class ConfigManager: def __init__(self, cli: object, tenant_name: str = None): self.tenant_name = tenant_name self.home = os.getenv('PYBRITIVE_HOME_DIR', str(Path.home())) - self.path = str(Path(self.home) / '.britive' / 'pybritive.config') # handle os specific separators properly + self.base_path = str(Path(self.home) / '.britive') + self.path = str(Path(self.base_path) / 'pybritive.config') self.config = None self.alias = None self.default_tenant = None @@ -177,6 +179,14 @@ def save_global(self, default_tenant_name: str = None, output_format: str = None self.config['global']['credential_backend'] = backend self.save() + def get_profile_aliases(self, reverse_keys: bool = False): + self.load() + aliases = self.config.get('profile-aliases', {}) + if reverse_keys: + return {v: k for k, v in aliases.items()} + else: + return aliases + def save_profile_alias(self, alias, profile): self.profile_aliases[alias] = profile self.config['profile-aliases'] = self.profile_aliases @@ -267,6 +277,9 @@ def validate_global(self, section, fields): if field == 'auto-refresh-profile-cache' and value not in ['true', 'false']: error = f'Invalid {section} field {field} value {value} provided. Invalid value choice.' self.validation_error_messages.append(error) + if field == 'auto-refresh-kube-config' and value not in ['true', 'false']: + error = f'Invalid {section} field {field} value {value} provided. Invalid value choice.' + self.validation_error_messages.append(error) if field == 'default_tenant': tenant_aliases_from_sections = [ extract_tenant(t) for t in self.config if t.startswith('tenant-') @@ -309,3 +322,10 @@ def auto_refresh_profile_cache(self): if value == 'true': return True return False + + def auto_refresh_kube_config(self): + self.load() + value = self.config.get('global', {}).get('auto-refresh-kube-config', 'false') + if value == 'true': + return True + return False diff --git a/src/pybritive/helpers/k8s_exec.py b/src/pybritive/helpers/k8s_exec.py index 7a7ae97..35c7169 100644 --- a/src/pybritive/helpers/k8s_exec.py +++ b/src/pybritive/helpers/k8s_exec.py @@ -1,20 +1,14 @@ -import sys - - -sys.tracebacklimit = 0 - - def get_args(): from getopt import getopt # lazy load from sys import argv # lazy load - options, non_options = getopt(argv[1:], 't:T:p:F:hv', [ + options = getopt(argv[1:], 't:T:p:F:hv', [ 'tenant=', 'token=', 'passphrase=', 'federation-provider=', 'help', 'version' - ]) + ])[0] args = { 'tenant': None, @@ -52,7 +46,7 @@ def usage(): exit() -def process(): +def main(): args = get_args() from .k8s_exec_credential_builder import KubernetesExecCredentialProcessor @@ -82,7 +76,8 @@ def process(): token=args['token'], passphrase=args['passphrase'], federation_provider=args['federation_provider'], - silent=True + silent=True, + from_helper_console_script=True ) b.config.get_tenant() # have to load the config here as that work is generally done elsewhere b.checkout( @@ -102,9 +97,5 @@ def process(): exit() -def main(): - process() - - if __name__ == '__main__': main() diff --git a/src/pybritive/helpers/kube_config_builder.py b/src/pybritive/helpers/kube_config_builder.py new file mode 100644 index 0000000..ce92c59 --- /dev/null +++ b/src/pybritive/helpers/kube_config_builder.py @@ -0,0 +1,110 @@ +import yaml +from pathlib import Path +from .config import ConfigManager +from ..britive_cli import BritiveCli +import random +import string + + +def sanitize(name: str): + name = name.lower() + name = name.replace(' ', '_').replace('/', "_").replace('\\', '_') + return name + + +def build_kube_config(profiles: list, config: ConfigManager, username: str): + # there will only ever be 1 user here, so we can hardcoded it + + # something unique that is not likely to clash with any other username that may be present in a kube config file + username = f'britive-{username}' + + aliases = config.get_profile_aliases(reverse_keys=True) + cluster_names = {} + for profile in profiles: + env_profile = f"{sanitize(profile['env'])}-{sanitize(profile['profile'].lower())}" + if env_profile not in cluster_names: + app = BritiveCli.escape_profile_element(profile['app']) + env = BritiveCli.escape_profile_element(profile['env']) + pro = BritiveCli.escape_profile_element(profile['profile']) + + escaped_profile_str = f"{app}/{env}/{pro}".lower() + alias = aliases.get(escaped_profile_str, None) + + cluster_names[env_profile] = { + 'apps': [], + 'url': profile['url'], + 'cert': profile['cert'], + 'escaped_profile': escaped_profile_str, + 'profile': f"{profile['app']}/{profile['env']}/{profile['profile']}".lower(), + 'alias': alias + } + cluster_names[env_profile]['apps'].append(sanitize(profile['app'])) + + users = [ + { + 'name': username, + 'user': { + 'exec': { + 'apiVersion': 'client.authentication.k8s.io/v1beta1', + 'command': 'pybritive-kube-exec', # todo - somehow get full path? not sure it is required? + 'env': None, + 'interactiveMode': 'Never', + 'provideClusterInfo': True + } + } + } + ] + contexts = [] + clusters = [] + + for env_profile, details in cluster_names.items(): + if len(details['apps']) == 1: + names = [env_profile] + else: + names = [f"{sanitize(a)}-{env_profile}" for a in details['apps']] + + cert = details['cert'] + url = details['url'] + + for name in names: + clusters.append( + { + 'name': name, + 'cluster': { + 'certificate-authority-data': cert, + 'server': url, + 'extensions': [ + { + 'name': 'client.authentication.k8s.io/exec', + 'extension': { + 'britive-profile': details.get('alias', details['escaped_profile']) + } + } + ] + } + } + ) + + contexts.append( + { + 'name': details.get('alias', name), + 'context': { + 'cluster': name, + 'user': username + } + } + ) + + kubeconfig = { + 'apiVersion': 'v1', + 'clusters': clusters, + 'contexts': contexts, + 'users': users, + 'kind': 'Config' + } + + kube_dir = Path(config.base_path) / 'kube' + kube_dir.mkdir(exist_ok=True) + filename = str(kube_dir / 'config') + with open(filename, 'w') as f: + yaml.dump(kubeconfig, f, default_flow_style=False) From 4da6882ca19b87b74697e6e1f6f3f7c3c2ee3448 Mon Sep 17 00:00:00 2001 From: twratl Date: Mon, 23 Oct 2023 09:47:29 -0400 Subject: [PATCH 07/10] cleanup --- src/pybritive/helpers/cloud_credential_printer.py | 5 ----- src/pybritive/helpers/k8s_exec_credential_builder.py | 7 +------ 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/pybritive/helpers/cloud_credential_printer.py b/src/pybritive/helpers/cloud_credential_printer.py index ddbb248..1770e2a 100644 --- a/src/pybritive/helpers/cloud_credential_printer.py +++ b/src/pybritive/helpers/cloud_credential_printer.py @@ -274,10 +274,5 @@ def print_json(self): def print_kube(self): if self.mode_modifier == 'exec': self.cli.print(self.k8s_processor.construct_exec_credential(self.credentials), ignore_silent=True) - elif self.mode_modifier == 'config': - # write file to ~/.britive/kubeconfig/... - # clean up any older config file that are no longer required - # print out `export KUBECONFIG=~/.britive/kubeconfig/...` - pass else: raise ValueError(f'--mode modifier {self.mode_modifier} for mode {self.mode} not supported') diff --git a/src/pybritive/helpers/k8s_exec_credential_builder.py b/src/pybritive/helpers/k8s_exec_credential_builder.py index f8959f7..61b743f 100644 --- a/src/pybritive/helpers/k8s_exec_credential_builder.py +++ b/src/pybritive/helpers/k8s_exec_credential_builder.py @@ -41,12 +41,7 @@ def construct_exec_credential(self, credentials: dict): response = { 'kind': 'ExecCredential', 'apiVersion': self.api_version, - 'spec': { - 'cluster': { - 'server': 'https://AFEA586304825E6AE82B38EA8B8665D2.gr7.us-west-1.eks.amazonaws.com', - 'certificate-authority-data': 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJek1EUXhNREUwTkRRd01Wb1hEVE16TURRd056RTBORFF3TVZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTTZUCk93Q25NclkzK0lhY1ZqUFpGYitxdi9GT1ZtQVJhOGVjZXNvN0VlblhOT1hod05sNjF6Ykk0UGhVcjRwU2U5QU8KTGwwd0lBUkZheWdtdTk4YVc0bU44UjcyYkVLc041WlYvL2FNbTdwTVJab2dYRDdNWDFseVNqRzdYUDgzRzBVYQpXMGlhS0JPdE96Q0F6dzBOVXAzd0E5ZzV5bUNIeGk2V3ZMZHRWOU9PSnlKYjkzM3NDZGhsM3phWlN6QmlzL1daClJsU0xIODlveUZXY0w5NGF2UU1WeGRGZHg2TlFLZ0ZnMERsMXNGTlFYbFlTbXkyUS8zd3lubU1RSlZqcnV6a2gKUW1yNDRoSUNrUmtZQVd5OU1wR2kwSmswa0IrekkwQzZnWWhqOG1OTS9wZEk3Skd0OXBCWWMrOWdnYVRuQWN5SQpGMWxDYjNVYlQ0VU4vcXZqMFkwQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZPQi94ZXdrRENVaDA0Y2QvcXpHN0JJbFAzOGdNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSTV5T2FrMzNpanhhakZ4TkloNQpkek1iNVlxY1c1RlNUMmJOQlhITzQvSjRjNUduVWFvV1FDZnNJK0FHQ0tBMXl5WUxIZmJwVEt2TjNGUW9rWGlSCi9oSG1reGJqMTk2MlEwZDFWSkExdk1nUktLa3lBOTZYQ1UrN3hxci9HeExzN1huTWphNHhrcEJKaEw0RSt6b1kKNy9PcEZxR2ZpUitoSmtncWN1MTlQenczZmN4VVdDZHlLSjNNZVMwN1psTzZLMjBONnoxUUFrNXA3Mi9BTERnQgpHWTVUVFhmU2tHakhyU1pMWnVjb25ZM1BSci9iWmRGNEhUdWhHZzJ2aXA2SytCWjJLQUpURDBlUkQvYTRpY1pMCjl6R09YU21Hc1krakpDY0l5VnBrenpsdGczMVNHc0NCOEtPTlU4YzRhb0RJWVhOSnFybjRmYm5YZnI0VVEwam4KZmtZPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' - } - }, + 'spec': {}, 'status': { 'expirationTimestamp': credentials['expirationTime'], 'token': credentials['jwt'] From 1cb59c5b90c5dbd8ac75d8348886cc8e95823408 Mon Sep 17 00:00:00 2001 From: twratl Date: Mon, 23 Oct 2023 10:08:01 -0400 Subject: [PATCH 08/10] remove check for KUBERNETES_EXEC_INFO env var and instead rely on the caller using the kube-exec mode --- src/pybritive/britive_cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pybritive/britive_cli.py b/src/pybritive/britive_cli.py index ca074f4..bef6666 100644 --- a/src/pybritive/britive_cli.py +++ b/src/pybritive/britive_cli.py @@ -530,8 +530,7 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, # handle kube-exec since the profile is actually going to be passed in via another method # and perform some basic validation so we don't waste time performing a checkout when we # will not be able to return a response back to kubectl via the exec command - if mode == 'kube-exec' or 'KUBERNETES_EXEC_INFO' in os.environ: - mode = 'kube-exec' # set for downstream processes if we are basing this only on the env var being present + if mode == 'kube-exec': from .helpers.k8s_exec_credential_builder import KubernetesExecCredentialProcessor k8s_processor = KubernetesExecCredentialProcessor() From 013593b0ec6858cd65eedc76ea019370f0cad6d9 Mon Sep 17 00:00:00 2001 From: twratl Date: Wed, 25 Oct 2023 09:55:27 -0400 Subject: [PATCH 09/10] k8s changes --- CHANGELOG.md | 6 +- docs/index.md | 2 +- src/pybritive/britive_cli.py | 38 ++++-- src/pybritive/helpers/cache.py | 8 +- src/pybritive/helpers/config.py | 15 ++- src/pybritive/helpers/kube_config_builder.py | 132 +++++++++++++++---- 6 files changed, 164 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2246392..d2d85c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,15 @@ ## v1.6.0rc1 [2023-10-XX] #### What's New -* Initial support for Kubernetes - this functionality is not yet available publicly on the Britive Platform +* Initial support for Kubernetes - this functionality is not yet available publicly on the Britive Platform - this is a beta feature for internal use only #### Enhancements * Add command `cache kubeconfig` +* Update command `cache clear` to delete the kube config file if it exists * Add global config flag `auto-refresh-kube-config` set by `configure update global auto-refresh-kube-config true` * Add checkout mode `k8s-exec` for use exclusively inside an `exec` command of a kube config file * Add console helper script `pybritive-kube-exec` for use exclusively inside an `exec` command of a kube config file +* Add the `pybritive` package version into the `User-Agent` string used by the Britive Python SDK (`britive` package) #### Bug Fixes * None @@ -19,7 +21,7 @@ * None #### Other -* None +* Documentation update on bash command to add the python `bin` path to your `PATH` environment variable. ## v1.5.0 [2023-10-20] diff --git a/docs/index.md b/docs/index.md index e45fb14..493c43e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,7 +42,7 @@ You will need to add this location to your path. The following command will do t this command into your `.bashrc, .zshrc, etc.` file, so it will always get executed on new terminal windows. ~~~ -export PATH=\"`python3 -m site --user-base`/bin:\$PATH\" +export PATH=`python3 -m site --user-base`/bin:$PATH ~~~ ## Tenant Configuration diff --git a/src/pybritive/britive_cli.py b/src/pybritive/britive_cli.py index bef6666..7a1134f 100644 --- a/src/pybritive/britive_cli.py +++ b/src/pybritive/britive_cli.py @@ -9,6 +9,7 @@ from pathlib import Path import sys import uuid +import pkg_resources import yaml import click import jmespath @@ -112,11 +113,26 @@ def login(self, explicit: bool = False, browser: str = None): except exceptions.UnauthorizedRequest: self._cleanup_credentials() + self._update_sdk_user_agent() + # if user called `pybritive login` and we should get profiles...do so should_get_profiles = any([self.config.auto_refresh_profile_cache(), self.config.auto_refresh_kube_config()]) if explicit and should_get_profiles: self._set_available_profiles() # will handle calling cache_profiles() and construct_kube_config() + def _update_sdk_user_agent(self): + # update the user agent to include the pybritive cli version + user_agent = self.b.session.headers.get('User-Agent') + + try: + version = pkg_resources.get_distribution('pybritive').version + except Exception: + version = 'unknown' + + self.b.session.headers.update({ + 'User-Agent': f'pybritive/{version} {user_agent}' + }) + def _cleanup_credentials(self): self.set_credential_manager() self.credential_manager.delete() @@ -317,7 +333,7 @@ def list_environments(self): data.append(row) self.print(data, ignore_silent=True) - def _set_available_profiles(self): + def _set_available_profiles(self, from_cache_command=False): if not self.available_profiles: data = [] for app in self.b.my_access.list_profiles(): @@ -342,10 +358,11 @@ def _set_available_profiles(self): } data.append(row) self.available_profiles = data - if self.config.auto_refresh_profile_cache(): - self.cache_profiles() - if self.config.auto_refresh_kube_config(): - self.construct_kube_config() + + if not from_cache_command and self.config.auto_refresh_profile_cache(): + self.cache_profiles() + if not from_cache_command and self.config.auto_refresh_kube_config(): + self.construct_kube_config() def construct_kube_config(self, from_cache_command=False): if self.from_helper_console_script: @@ -353,7 +370,7 @@ def construct_kube_config(self, from_cache_command=False): if from_cache_command: self.login() - self._set_available_profiles() + self._set_available_profiles(from_cache_command=from_cache_command) profiles = [] for p in self.available_profiles: @@ -370,7 +387,12 @@ def construct_kube_config(self, from_cache_command=False): 'cert': cert, }) from .helpers.kube_config_builder import build_kube_config # lazy import as not everyone will want this - build_kube_config(profiles=profiles, config=self.config, username=self.b.my_access.whoami()['username']) + build_kube_config( + profiles=profiles, + config=self.config, + username=self.b.my_access.whoami()['username'], + cli=self + ) def _get_app_type(self, application_id): self._set_available_profiles() @@ -726,7 +748,7 @@ def cache_profiles(self, from_cache_command=False): if from_cache_command: self.login() - self._set_available_profiles() + self._set_available_profiles(from_cache_command=from_cache_command) for p in self.available_profiles: profile = self.escape_profile_element(p['app_name']) diff --git a/src/pybritive/helpers/cache.py b/src/pybritive/helpers/cache.py index 603e1f9..8c773eb 100644 --- a/src/pybritive/helpers/cache.py +++ b/src/pybritive/helpers/cache.py @@ -9,7 +9,8 @@ def __init__(self, passphrase: str = None): self.passphrase = passphrase self.string_encryptor = StringEncryption(passphrase=self.passphrase) home = os.getenv('PYBRITIVE_HOME_DIR', str(Path.home())) - self.path = str(Path(home) / '.britive' / 'pybritive.cache') # handle os specific separators properly + self.base_path = str(Path(home) / '.britive') + self.path = str(Path(self.base_path) / 'pybritive.cache') # handle os specific separators properly self.cache = {} self.default_key_values = { 'profiles': [], @@ -50,9 +51,14 @@ def save_profiles(self, profiles: list): self.write() def clear(self): + # write empty cache file self.cache = self.default_key_values self.write() + # delete kube config if it exists + kubeconfig = Path(self.base_path) / 'kube' / 'config' + kubeconfig.unlink(missing_ok=True) + def get_credentials(self, profile_name: str, mode: str = 'awscredentialprocess'): try: ciphertext = self.cache[mode].get(profile_name.lower()) diff --git a/src/pybritive/helpers/config.py b/src/pybritive/helpers/config.py index aa1662f..047b8a9 100644 --- a/src/pybritive/helpers/config.py +++ b/src/pybritive/helpers/config.py @@ -71,6 +71,8 @@ def __init__(self, cli: object, tenant_name: str = None): self.alias = None self.default_tenant = None self.tenants = None + self.tenants_by_name = None + self.aliases_and_names = None self.profile_aliases = {} self.cli = cli self.loaded = False @@ -106,10 +108,17 @@ def load(self, force=False): self.alias = None # will be set in self.get_tenant() self.default_tenant = self.config.get('global', {}).get('default_tenant') self.tenants = {} + self.tenants_by_name = {} for key in self.config: if key.startswith('tenant-'): alias = extract_tenant(key) - self.tenants[alias] = self.config[key] + item = {**self.config[key], **{'alias': alias}} + self.tenants[alias] = item + + name = self.config[key].get('name', alias) + if name != alias: + self.tenants_by_name[name] = item + self.aliases_and_names = {**self.tenants, **self.tenants_by_name} self.profile_aliases = self.config.get('profile-aliases', {}) self.loaded = True @@ -137,12 +146,12 @@ def get_tenant(self): provided_tenant_name = list(self.tenants)[0] # if we get here then we now have a tenant name we can check to ensure exists - if provided_tenant_name not in self.tenants and not name: + if provided_tenant_name not in self.aliases_and_names and not name: raise click.ClickException(f'Tenant name "{provided_tenant_name}" not found in {self.path}') self.alias = provided_tenant_name or name # return details about the requested tenant - return self.tenants.get(provided_tenant_name, {'name': name}) + return self.aliases_and_names.get(provided_tenant_name, {'name': name, 'alias': name}) def save(self): self.validate() # ensure we are actually writing a valid config diff --git a/src/pybritive/helpers/kube_config_builder.py b/src/pybritive/helpers/kube_config_builder.py index ce92c59..9bcce77 100644 --- a/src/pybritive/helpers/kube_config_builder.py +++ b/src/pybritive/helpers/kube_config_builder.py @@ -2,24 +2,75 @@ from pathlib import Path from .config import ConfigManager from ..britive_cli import BritiveCli -import random -import string +import os def sanitize(name: str): name = name.lower() - name = name.replace(' ', '_').replace('/', "_").replace('\\', '_') + # name = name.replace(' ', '_').replace('/', "_").replace('\\', '_') return name -def build_kube_config(profiles: list, config: ConfigManager, username: str): - # there will only ever be 1 user here, so we can hardcoded it +def check_env_var(filename, cli: BritiveCli): + kubeconfig = os.getenv('KUBECONFIG') - # something unique that is not likely to clash with any other username that may be present in a kube config file - username = f'britive-{username}' + # no env var present + if not kubeconfig: + command = f'export KUBECONFIG=~/.kube/config:{filename}' + cli.print(f'Please ensure your KUBECONFIG environment variable includes the Britive managed kube config file.') + cli.print(command) + else: + for configfile in kubeconfig.split(':'): + full_path = str(Path(configfile).expanduser()).lower() + if filename.lower() == full_path: + return # we found what we came for - silently continue - aliases = config.get_profile_aliases(reverse_keys=True) + # if we get here we need to instruct the user to add the britive managed kube config file + cli.print(f'Please modify your KUBECONFIG environment variable to include the ' + f'Britive managed kube config file.') + command = f'export KUBECONFIG="${{KUBECONFIG}}:{filename}"' + cli.print(command) + + +def merge_new_with_existing(clusters, contexts, users, filename, tenant, assigned_aliases): + # get the existing config, so we can pop out all + # items related to this tenant as we will be replacing + # them with the above created items + existing_kubeconfig = {} + if Path(filename).exists(): + with open(filename, 'r') as f: + existing_kubeconfig = yaml.safe_load(f) or {} + + prefix = f'{tenant}-' + for cluster in existing_kubeconfig.get('clusters', []): + if not cluster.get('name', '').startswith(prefix): + clusters.append(cluster) + + for context in existing_kubeconfig.get('contexts', []): + name = context.get('name', '') + if not name.startswith(prefix) and name not in assigned_aliases: + contexts.append(context) + + for user in existing_kubeconfig.get('users', []): + if not user.get('name', '').startswith(prefix): + users.append(user) + + kubeconfig = { + 'apiVersion': 'v1', + 'clusters': clusters, + 'contexts': contexts, + 'users': users, + 'kind': 'Config' + } + + # write out the config file + with open(filename, 'w') as f: + yaml.safe_dump(kubeconfig, f, default_flow_style=False, encoding='utf-8') + + +def parse_profiles(profiles, aliases): cluster_names = {} + assigned_aliases = [] for profile in profiles: env_profile = f"{sanitize(profile['env'])}-{sanitize(profile['profile'].lower())}" if env_profile not in cluster_names: @@ -29,6 +80,7 @@ def build_kube_config(profiles: list, config: ConfigManager, username: str): escaped_profile_str = f"{app}/{env}/{pro}".lower() alias = aliases.get(escaped_profile_str, None) + assigned_aliases.append(alias) cluster_names[env_profile] = { 'apps': [], @@ -39,21 +91,28 @@ def build_kube_config(profiles: list, config: ConfigManager, username: str): 'alias': alias } cluster_names[env_profile]['apps'].append(sanitize(profile['app'])) + return [cluster_names, assigned_aliases] + +def build_tenant_config(tenant, cluster_names, username): users = [ { 'name': username, 'user': { 'exec': { 'apiVersion': 'client.authentication.k8s.io/v1beta1', - 'command': 'pybritive-kube-exec', # todo - somehow get full path? not sure it is required? + 'command': 'pybritive-kube-exec', + 'args': [ + '-t', + tenant + ], 'env': None, 'interactiveMode': 'Never', 'provideClusterInfo': True } } } - ] + ] if len(cluster_names.keys()) > 0 else [] contexts = [] clusters = [] @@ -69,7 +128,7 @@ def build_kube_config(profiles: list, config: ConfigManager, username: str): for name in names: clusters.append( { - 'name': name, + 'name': f'{tenant}-{name}', 'cluster': { 'certificate-authority-data': cert, 'server': url, @@ -87,24 +146,53 @@ def build_kube_config(profiles: list, config: ConfigManager, username: str): contexts.append( { - 'name': details.get('alias', name), + 'name': details.get('alias', f'{tenant}-{name}'), 'context': { - 'cluster': name, + 'cluster': f'{tenant}-{name}', 'user': username } } ) + return [clusters, contexts, users] - kubeconfig = { - 'apiVersion': 'v1', - 'clusters': clusters, - 'contexts': contexts, - 'users': users, - 'kind': 'Config' - } +def build_kube_config(profiles: list, config: ConfigManager, username: str, cli: BritiveCli): + tenant = config.get_tenant()['alias'].lower() # must be run first to set the tenant alias in the config + + # something unique that is not likely to clash with any other username that may be present in a kube config file + # add the tenant details which will mean 1 user per tenant + username = f'{tenant}-{username}' + + # grab the aliases + aliases = config.get_profile_aliases(reverse_keys=True) + + # parse all the profiles + cluster_names, assigned_aliases = parse_profiles(profiles, aliases) + + # establish the 3 elements of the config + clusters, contexts, users = build_tenant_config( + tenant=tenant, + cluster_names=cluster_names, + username=username + ) + + # calculate the path for the config kube_dir = Path(config.base_path) / 'kube' kube_dir.mkdir(exist_ok=True) filename = str(kube_dir / 'config') - with open(filename, 'w') as f: - yaml.dump(kubeconfig, f, default_flow_style=False) + + # merge any existing config with the new config + # and write it to disk + merge_new_with_existing( + clusters=clusters, + contexts=contexts, + users=users, + tenant=tenant, + filename=filename, + assigned_aliases=assigned_aliases + ) + + # if required ensure we tell the user they need to modify their KUBECONFIG env var + # in order to pick up the Britive managed kube config file + if len(clusters) > 0: + check_env_var(filename=filename, cli=cli) From 3e92cdff470c23f9a47cbddc5fabbae1c5cea64b Mon Sep 17 00:00:00 2001 From: twratl Date: Wed, 25 Oct 2023 09:58:39 -0400 Subject: [PATCH 10/10] changelog date update --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2d85c3..8cff8db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ * As of v1.4.0 release candidates will be published in an effort to get new features out faster while still allowing time for full QA testing before moving the release candidate to a full release. -## v1.6.0rc1 [2023-10-XX] +## v1.6.0rc1 [2023-10-25] #### What's New * Initial support for Kubernetes - this functionality is not yet available publicly on the Britive Platform - this is a beta feature for internal use only