From 466a441b4b1d01657798ae89d5cfa491f623bba6 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Sun, 12 Dec 2021 22:29:50 -0800 Subject: [PATCH] [k8s-extension] Release v1.0.2 with Update Api-Version Support for Getting Clusters from RPs (#4202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create pull.yml * Update pull.yml * Update azure-pipelines.yml * Initial commit of k8s-extension * Update pipelines file * Update CODEOWNERS * Update private preview pipelines * Remove open service mesh from public release * Update pipeline files * Update public extension pipeline * Change condition variable * Add version to public preview/private preview * Update pipelines * Add different testing based on private branch * Add annotations to extension model * Update k8s-custom-pipelines.yml * Update SDKs with Updated Swagger Spec for 2020-07-01-preview (#13) * Update sdks with updated swagger spec * Update version and history rst * Reorder release history timeline * Fix ExtensionInstanceForCreate for import * remove py2 bdist support * Add custom table formatting * Remove unnecessary files * Fix style issues * Fix branch based on comments * Update identity piece manually * Don't handle defaults at the CLI level * Remove defaults from CLI client * Check null target namespace with namespace scope * Update style * Add cassandra operator and location to model * Stage Public Version of k8s-extension 0.2.0 for official release (#15) * Create pull.yml * Update pull.yml * Update azure-pipelines.yml * Initial commit of k8s-extension * Update pipelines file * Update CODEOWNERS * Update private preview pipelines * Remove open service mesh from public release * Update pipeline files * Update public extension pipeline * Change condition variable * Add version to public preview/private preview * Update pipelines * Add different testing based on private branch * Add annotations to extension model * Update k8s-custom-pipelines.yml * Update SDKs with Updated Swagger Spec for 2020-07-01-preview (#13) * Update sdks with updated swagger spec * Update version and history rst * Reorder release history timeline * Fix ExtensionInstanceForCreate for import * remove py2 bdist support * Add custom table formatting * Remove unnecessary files * Fix style issues * Fix branch based on comments * Update identity piece manually * Don't handle defaults at the CLI level * Remove defaults from CLI client * Check null target namespace with namespace scope * Update style * Add cassandra operator and location to model Co-authored-by: action@github.com * Remove custom pipelines file * Update extension description, remove private const * Update pipeline file * Disable refs docs * Update to include better create warning logs and remove update context (#20) * Update to include better create warning logs and remove update context * Remove help text for update * Fix spelling error * Update message * Fix k8s-extension conflict with private version * Fix style errors * Fix filename * add customization for microsoft.azureml.kubernetes (#23) * add customization for microsoft.azureml.kubernetes * Update release history Co-authored-by: Yue Yu Co-authored-by: jonathan-innis * Add E2E Testing from Separate branch into internal code (#26) * Add internal e2e testing * Change to testing folder * Inference CLI validation for Scoring FE (#24) * cli validation starter * added the call to the fe validation function * nodeport validation not required * test fix Co-authored-by: Jonathan Innis * legal warning added (#27) * Remove deprecated method logger.warn * Update k8s-custom-pipelines.yml for Azure Pipelines * Update k8s-custom-pipelines.yml for Azure Pipelines * Add Azure Defender to E2E testing (#28) * Add azure defender testing to e2e * Remove the debug flag * Add configuration testing * Fix pipeline failures * Make test script more intuitive * Remove parameter from testing * Fix wrong location for k8s config whl * Fix pip upgrade issue * Fix pip install upgrade issue * Fix pip install issue * delete resurce in testcase (#29) Co-authored-by: Yue Yu Co-authored-by: Jonathan Innis * Check Provider is Registered with Subscription Before Making Requests (#18) * Add check for KubernetesConfiguration * Disable pylint and rename * Update provider registration link * Update version * Remove extra blank line * Fix bug in import * only validate scoring fe when inference is enabled (#31) * only validate scoring fe when inference is enabled * Fix versioning Co-authored-by: Yue Yu Co-authored-by: jonathan-innis * Provider registration case insensitive * do not validate against scoring fe if inference is not enabled. (#33) * do not validate against scoring fe if inference is not enabled. * add inference enabled scenario * refine * increase sleeping time * fix Co-authored-by: Yue Yu Co-authored-by: Jonathan Innis * Add OSM as Public Preview Extension (#34) * Add OSM as public preview extension * Add osm testing * Add release train to tests * Fix failing osm test * Upgrade pip in integration testing * Remove ununsed import * Fix release train check in update * Parallelize E2E Testing (#36) * Add OSM as public preview extension * Add osm testing * Update test logic to parallelize * Fix test success checking * Parallelize extension testing * Better error checking logic * Fix azureml deletion * Fix private build (#40) * change amlk8s to amlarc (#42) Co-authored-by: Yue Yu * Servicebus client model changes (#44) * Servicebus client model changes * Fix testing script * Update history file and pipeline * Update min cli core version for track 2 updates * Read SSL cert and key from files (#38) * first sketch of the change fixes removed extra blank lines changes regarding param renaming added ssl tests added more detail to the unit test additional import moved pem files out of public folder fixed import chenged import changed import unit tests fix unit test fix fixed unit tests fixed unit test unit test fix changes int test cert and key * test protected config * fix test typo * temporary changes reverted * fixing tests * fixed file paths * removed accidentally added file * changes according to review comments * more changes according to review comments * changes according to review comments Co-authored-by: Jonathan Innis * Upgrade release version * Liakaz/inference read ssl from file (#47) * first sketch of the change fixes removed extra blank lines changes regarding param renaming added ssl tests added more detail to the unit test additional import moved pem files out of public folder fixed import chenged import changed import unit tests fix unit test fix fixed unit tests fixed unit test unit test fix changes int test cert and key * test protected config * fix test typo * temporary changes reverted * fixing tests * fixed file paths * removed accidentally added file * changes according to review comments * more changes according to review comments * changes according to review comments * fixed decode error * renamed the experimental param Co-authored-by: Jonathan Innis * Fix style issues (#51) * Fixed scoring fe related extension param names (#49) * fixed scoring fe related extension params * bug fix and style fixes * variable rename * fixed the error type * set cluster to prod by default * Add distro validation for osm-arc (#50) * Add distro validation for osm-arc * fixed indentation * Fix linting * Resolve comments * Add unit test * fix lint Co-authored-by: Jonathan Innis * Add distro validation for osm-arc (#50) * Add distro validation for osm-arc * fixed indentation * Fix linting * Resolve comments * Add unit test * fix lint Co-authored-by: Jonathan Innis * Add distro validation for osm-arc (#53) removed release-train logic * Add Custom Delete Logic for Partners (#54) * Add custom delete logic * Fix failing unit tests * Add warning message when deleting amlarc extension (#55) * add warning message * fix indentation * Update release version * Remove Pyhelm from OSM customization (#58) * Fix OSM pyhelm bug * Debug bootstrap error * Update release message * Remove pyhelm dependency * Update tests to only check extensionconfig creation (#61) * Update tests to only check extensionconfig creation * Single set of CRUD for AzureML * Debug logs for connectedk8s * Increase open service mesh version number * Update k8s-extension Models to Track2 (#64) * Update k8s-extension models to track2 * Add debug for failed cleanup * Increase version number * Exit 0 on failed cleanup * Fix identity in wrong place in model (#66) * Readd osm-arc distro validation (#62) * Add distro validation for osm-arc removed release-train logic * Readd osm_arc distro validation * Fix style * Rm space * Edit test * Fixed tests and error logic * Remove dependency * Add delete method Co-authored-by: Jonathan Innis * Don't Send Identity Headers If In DF (#67) * Don't send identity for clusters in dogfood * Add location to model for identity * Add identity validation to testing * Use default extension with identity instead of Cassandra specific (#69) * Remove the identity check for now * Add -t for clusterType parameter (#71) * Adding a flag for AKS to AMLARC migration and set up corresponding FE… (#65) * Adding a flag for AKS to AMLARC migration and set up corresponding FE helm values * Remove one extra line * Adding Scoring FE IS_AKS_MIGRATION check logic for helm values Co-authored-by: Harry Yang Co-authored-by: Jonathan Innis * remove version requirement and auto upgrade minor version check (#72) * Custom User Confirmation for Partners (#70) * Custom user confirmation * Check for disable confirm prompty for confirmation * Add yes to delete command * Code cleanup and style fixes (#73) * Enabled identity by default (#74) * Increase version * Fix df check and add unit test (#77) * Bump extension version * Pin helm version * Extensions GA changes into Public Branch (#79) * Add openservicemesh back * OpenServiceMesh import * Update osm with new extension model * Add back private file * Add Azure ML to list of private extensions (#16) * Update k8s-custom-pipelines.yml * Add Microsoft.PolicyInsights extension (#17) * Add Policy extension * Update comment * Update args * Fix linting errors Co-authored-by: Jonathan Innis * Add HISTORY_private file for private preview * Change versioning scheme * Update the code for supporting both extensions at once * Fix style issue * Remove old consts file * change the resource tag from managed_by:amlk8s to created_by:amlk8s-e… (#22) * change the resource tag from managed_by:amlk8s to created_by:amlk8s-extension * remove the lock when creating resources * fix lint * update version and HISTORY_private.rst * change error message Co-authored-by: Yue Yu * Update the beta version with upstream * Update the private history file * Add upgrade pip to pipeline * Move pip install within virtualenv * Merge in k8s-extension/public (0.3.1) (#32) * delete resurce in testcase (#29) Co-authored-by: Yue Yu Co-authored-by: Jonathan Innis * Check Provider is Registered with Subscription Before Making Requests (#18) * Add check for KubernetesConfiguration * Disable pylint and rename * Update provider registration link * Update version * Remove extra blank line * Fix bug in import * only validate scoring fe when inference is enabled (#31) * only validate scoring fe when inference is enabled * Fix versioning Co-authored-by: Yue Yu Co-authored-by: jonathan-innis * Update private release Co-authored-by: yuyue9284 <15863499+yuyue9284@users.noreply.github.com> Co-authored-by: Yue Yu * Release Version 0.4.0-b1 (#37) * Merge k8s-extension/public into k8s-extension/private * Update the version * Fix testing concurrency * K8s extension/private 0.4.0b2 (#41) * Fix private build (#40) * Update version * Upgrade to v0.5.2 * Fix policy bug * Increase private version * Update consts_private.py * Increase private version * Increase version with public * Add flux to private version * Update models for 2021-05-01-preview * Add async models to version * Add no wait to delete and create * support managed cluster * Bump version * Pin helm version * Add cmd to delete call * Add force deletion * add dapr extension (#78) Signed-off-by: Ji An Liu * Fix failing integration tests * Adding the GA changes for private branch * Fix confirm prompt * Fix update E2E tests Co-authored-by: jonathan-innis Co-authored-by: action@github.com Co-authored-by: nreisch Co-authored-by: yuyue9284 <15863499+yuyue9284@users.noreply.github.com> Co-authored-by: Yue Yu Co-authored-by: anagg929 <59664801+anagg929@users.noreply.github.com> Co-authored-by: Ji'an Liu Co-authored-by: nanthi * Fix configuration settings in update * Only provide confirmation when specifying settings * Fix style issues * Cassandra tests with update (#81) * Add Microsoft.PolicyInsights extension for public preview (#83) * Add Azure Policy * Remove custom configuration and update tests * Yuyu3/fix upgrade public (#85) * populate configuration protected settings for azureml bump version && add log fetch connection string only if configuration protected settings are set update ssl key * bump the version * reverse changes on version and HISTORY.rst * inferenceLoadBalancerHA Co-authored-by: Yue Yu * Remove Parallel Powershell Jobs (#82) * Unparallelize tests * Moved location of pipeline file * Remove the parallel invoke expression calls * Add templates to testing * Remove policy update test from extension E2E (#88) * feIsNodePort, feIsInternalLoadBalancer (#87) Co-authored-by: Yue Yu Co-authored-by: Jonathan Innis * Fix history file * Add one more prompt for amlarc extension update (#94) * Add one more prompt for amlarc extension update * fix pylint issue * fix pylint issue * fix pylint issue * fix pylint issue Co-authored-by: Youhua Tu Co-authored-by: Youhua Tu * Update Identity Creation for Appliance to Latest Version (#95) * Update appliance API to latest version for identity * Create a utils file with get parent_api_version * Fix style errors * Bump version * Remove unneeded files * Remove additional entry from history Co-authored-by: action@github.com Co-authored-by: yuyue9284 <15863499+yuyue9284@users.noreply.github.com> Co-authored-by: Yue Yu Co-authored-by: Lia Kazakova <58274127+liakaz@users.noreply.github.com> Co-authored-by: Niranjan Shankar Co-authored-by: jingyizhu99 <83610845+jingyizhu99@users.noreply.github.com> Co-authored-by: Harry Yang Co-authored-by: Harry Yang Co-authored-by: Thomas Stringer Co-authored-by: NarayanThiru Co-authored-by: nreisch Co-authored-by: anagg929 <59664801+anagg929@users.noreply.github.com> Co-authored-by: Ji'an Liu Co-authored-by: nanthi Co-authored-by: youhuatuyh <87928654+youhuatuyh@users.noreply.github.com> Co-authored-by: Youhua Tu Co-authored-by: Youhua Tu --- .flake8 | 1 + src/k8s-extension/HISTORY.rst | 8 + .../azext_k8s_extension/consts.py | 18 +- .../azext_k8s_extension/custom.py | 355 +++++++++++------- .../partner_extensions/AzureMLKubernetes.py | 106 +++++- .../partner_extensions/DefaultExtension.py | 70 +++- .../PartnerExtensionModel.py | 50 ++- .../azext_k8s_extension/utils.py | 43 +++ src/k8s-extension/setup.py | 39 +- 9 files changed, 494 insertions(+), 196 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/utils.py diff --git a/.flake8 b/.flake8 index 0295f1c0277..5d2b0466e0e 100644 --- a/.flake8 +++ b/.flake8 @@ -7,6 +7,7 @@ ignore = F401, # imported but unused, too many violations, to be removed in the future F811, # redefinition of unused, to be removed in the future C901 # code flow is too complex, too many violations, to be removed in the future + W503 # line break before binary operator effect on readability is subjective W504 # line break after binary operator effect on readability is subjective exclude = */vendored_sdks diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 9bc3cc21746..c2c7f13cbef 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -2,6 +2,14 @@ Release History =============== + +1.0.2 +++++++++++++++++++ +* Update api-version for calls to Microsoft.ResourceConnector to 2021-10-31-preview +* Update api-version for calls to Microsoft.ContainerService to 2021-10-01 +* Update api-version for calls to Microsoft.Kubernetes to 2021-10-01 +* microsoft.azureml.kubernetes: Add one more prompt for amlarc extension update + 1.0.1 ++++++++++++++++++ * microsoft.azureml.kubernetes: Retrieve relay and service bus connection string when update the configuration protected settings of the extension. diff --git a/src/k8s-extension/azext_k8s_extension/consts.py b/src/k8s-extension/azext_k8s_extension/consts.py index 350344bc53a..dff2e32b886 100644 --- a/src/k8s-extension/azext_k8s_extension/consts.py +++ b/src/k8s-extension/azext_k8s_extension/consts.py @@ -4,8 +4,20 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -EXTENSION_NAME = 'k8s-extension' +EXTENSION_NAME = "k8s-extension" EXTENSION_PACKAGE_NAME = "azext_k8s_extension" -PROVIDER_NAMESPACE = 'Microsoft.KubernetesConfiguration' +PROVIDER_NAMESPACE = "Microsoft.KubernetesConfiguration" REGISTERED = "Registered" -DF_RM_HOSTNAME = 'api-dogfood.resources.windows-int.net' +DF_RM_HOSTNAME = "api-dogfood.resources.windows-int.net" + +CONNECTED_CLUSTER_RP = "Microsoft.Kubernetes" +MANAGED_CLUSTER_RP = "Microsoft.ContainerService" +APPLIANCE_RP = "Microsoft.ResourceConnector" + +CONNECTED_CLUSTER_TYPE = "connectedclusters" +MANAGED_CLUSTER_TYPE = "managedclusters" +APPLIANCE_TYPE = "appliances" + +CONNECTED_CLUSTER_API_VERSION = "2021-10-01" +MANAGED_CLUSTER_API_VERSION = "2021-10-01" +APPLIANCE_API_VERSION = "2021-10-31-preview" diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index d3fd501a0f2..8f845333f3b 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -5,12 +5,18 @@ # pylint: disable=unused-argument,too-many-locals -import json -from urllib.parse import urlparse +from azext_k8s_extension.utils import ( + get_cluster_rp_api_version, + is_dogfood_cluster, + read_config_settings_file, +) from knack.log import get_logger -from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ - InvalidArgumentValueError, RequiredArgumentMissingError +from azure.cli.core.azclierror import ( + ResourceNotFoundError, + MutuallyExclusiveArgumentError, + RequiredArgumentMissingError, +) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait from azure.core.exceptions import HttpResponseError @@ -22,7 +28,10 @@ from .partner_extensions.OpenServiceMesh import OpenServiceMesh from .partner_extensions.AzureMLKubernetes import AzureMLKubernetes from .partner_extensions.Dapr import Dapr -from .partner_extensions.DefaultExtension import DefaultExtension, user_confirmation_factory +from .partner_extensions.DefaultExtension import ( + DefaultExtension, + user_confirmation_factory, +) from . import consts from ._client_factory import cf_resources @@ -33,11 +42,11 @@ # A factory method to return the correct extension class based off of the extension name def ExtensionFactory(extension_name): extension_map = { - 'microsoft.azuremonitor.containers': ContainerInsights, - 'microsoft.azuredefender.kubernetes': AzureDefender, - 'microsoft.openservicemesh': OpenServiceMesh, - 'microsoft.azureml.kubernetes': AzureMLKubernetes, - 'microsoft.dapr': Dapr, + "microsoft.azuremonitor.containers": ContainerInsights, + "microsoft.azuredefender.kubernetes": AzureDefender, + "microsoft.openservicemesh": OpenServiceMesh, + "microsoft.azureml.kubernetes": AzureMLKubernetes, + "microsoft.dapr": Dapr, } # Return the extension if we find it in the map, else return the default @@ -45,14 +54,14 @@ def ExtensionFactory(extension_name): def show_k8s_extension(client, resource_group_name, cluster_name, name, cluster_type): - """Get an existing K8s Extension. - """ + """Get an existing K8s Extension.""" # Determine ClusterRP - cluster_rp = __get_cluster_rp(cluster_type) + cluster_rp, _ = get_cluster_rp_api_version(cluster_type) try: - extension = client.get(resource_group_name, - cluster_rp, cluster_type, cluster_name, name) + extension = client.get( + resource_group_name, cluster_rp, cluster_type, cluster_name, name + ) return extension except HttpResponseError as ex: # Customize the error message for resources not found @@ -60,46 +69,69 @@ def show_k8s_extension(client, resource_group_name, cluster_name, name, cluster_ # If Cluster not found if ex.message.__contains__("(ResourceNotFound)"): message = "{0} Verify that the cluster-type is correct and the resource exists.".format( - ex.message) + ex.message + ) # If Configuration not found - elif ex.message.__contains__("Operation returned an invalid status code 'Not Found'"): - message = "(ExtensionNotFound) The Resource {0}/{1}/{2}/Microsoft.KubernetesConfiguration/" \ - "extensions/{3} could not be found!".format( - cluster_rp, cluster_type, cluster_name, name) + elif ex.message.__contains__( + "Operation returned an invalid status code 'Not Found'" + ): + message = ( + "(ExtensionNotFound) The Resource {0}/{1}/{2}/Microsoft.KubernetesConfiguration/" + "extensions/{3} could not be found!".format( + cluster_rp, cluster_type, cluster_name, name + ) + ) else: message = ex.message raise ResourceNotFoundError(message) from ex raise ex -def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, cluster_type, - extension_type, scope=None, auto_upgrade_minor_version=None, release_train=None, - version=None, target_namespace=None, release_namespace=None, configuration_settings=None, - configuration_protected_settings=None, configuration_settings_file=None, - configuration_protected_settings_file=None, no_wait=False): - """Create a new Extension Instance. - """ +def create_k8s_extension( + cmd, + client, + resource_group_name, + cluster_name, + name, + cluster_type, + extension_type, + scope=None, + auto_upgrade_minor_version=None, + release_train=None, + version=None, + target_namespace=None, + release_namespace=None, + configuration_settings=None, + configuration_protected_settings=None, + configuration_settings_file=None, + configuration_protected_settings_file=None, + no_wait=False, +): + """Create a new Extension Instance.""" extension_type_lower = extension_type.lower() - cluster_rp = __get_cluster_rp(cluster_type) + cluster_rp, _ = get_cluster_rp_api_version(cluster_type) # Configuration Settings & Configuration Protected Settings if configuration_settings is not None and configuration_settings_file is not None: raise MutuallyExclusiveArgumentError( - 'Error! Both configuration-settings and configuration-settings-file cannot be provided.' + "Error! Both configuration-settings and configuration-settings-file cannot be provided." ) - if configuration_protected_settings is not None and configuration_protected_settings_file is not None: + if ( + configuration_protected_settings is not None + and configuration_protected_settings_file is not None + ): raise MutuallyExclusiveArgumentError( - 'Error! Both configuration-protected-settings and configuration-protected-settings-file ' - 'cannot be provided.' + "Error! Both configuration-protected-settings and configuration-protected-settings-file " + "cannot be provided." ) config_settings = {} config_protected_settings = {} # Get Configuration Settings from file if configuration_settings_file is not None: - config_settings = __read_config_settings_file(configuration_settings_file) + config_settings = read_config_settings_file(configuration_settings_file) if configuration_settings is not None: for dicts in configuration_settings: @@ -108,7 +140,9 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c # Get Configuration Protected Settings from file if configuration_protected_settings_file is not None: - config_protected_settings = __read_config_settings_file(configuration_protected_settings_file) + config_protected_settings = read_config_settings_file( + configuration_protected_settings_file + ) if configuration_protected_settings is not None: for dicts in configuration_protected_settings: @@ -127,12 +161,29 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c # Get the extension class based on the extension name extension_class = ExtensionFactory(extension_type_lower) extension_instance, name, create_identity = extension_class.Create( - cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type_lower, scope, - auto_upgrade_minor_version, release_train, version, target_namespace, release_namespace, config_settings, - config_protected_settings, configuration_settings_file, configuration_protected_settings_file) + cmd, + client, + resource_group_name, + cluster_name, + name, + cluster_type, + extension_type_lower, + scope, + auto_upgrade_minor_version, + release_train, + version, + target_namespace, + release_namespace, + config_settings, + config_protected_settings, + configuration_settings_file, + configuration_protected_settings_file, + ) # Common validations - __validate_version_and_auto_upgrade(extension_instance.version, extension_instance.auto_upgrade_minor_version) + __validate_version_and_auto_upgrade( + extension_instance.version, extension_instance.auto_upgrade_minor_version + ) __validate_scope_after_customization(extension_instance.scope) # Check that registration has been done on Microsoft.KubernetesConfiguration for the subscription @@ -141,50 +192,80 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c # Create identity, if required # We don't create the identity if we are in DF if create_identity and not is_dogfood_cluster(cmd): - identity_object, location = __create_identity(cmd, resource_group_name, cluster_name, cluster_type, cluster_rp) + identity_object, location = __create_identity( + cmd, resource_group_name, cluster_name, cluster_type + ) if identity_object is not None and location is not None: - extension_instance.identity, extension_instance.location = identity_object, location + extension_instance.identity, extension_instance.location = ( + identity_object, + location, + ) # Try to create the resource - return sdk_no_wait(no_wait, client.begin_create, resource_group_name, - cluster_rp, cluster_type, cluster_name, name, extension_instance) + return sdk_no_wait( + no_wait, + client.begin_create, + resource_group_name, + cluster_rp, + cluster_type, + cluster_name, + name, + extension_instance, + ) def list_k8s_extension(client, resource_group_name, cluster_name, cluster_type): - cluster_rp = __get_cluster_rp(cluster_type) + cluster_rp, _ = get_cluster_rp_api_version(cluster_type) return client.list(resource_group_name, cluster_rp, cluster_type, cluster_name) -def update_k8s_extension(cmd, client, resource_group_name, cluster_name, name, cluster_type, - auto_upgrade_minor_version='', release_train='', version='', - configuration_settings=None, configuration_protected_settings=None, - configuration_settings_file=None, configuration_protected_settings_file=None, - no_wait=False, yes=False): - """Patch an existing Extension Instance. - """ - - if configuration_settings or \ - configuration_protected_settings or \ - configuration_settings_file or \ - configuration_protected_settings_file: - msg = ('Updating properties in --config-settings or --config-protected-settings may lead to undesirable state' - ' if the cluster extension type does not support it. Please refer to the documentation of the' - ' cluster extension service to check if updates to these properties is supported.' - ' Do you wish to proceed?') +def update_k8s_extension( + cmd, + client, + resource_group_name, + cluster_name, + name, + cluster_type, + auto_upgrade_minor_version="", + release_train="", + version="", + configuration_settings=None, + configuration_protected_settings=None, + configuration_settings_file=None, + configuration_protected_settings_file=None, + no_wait=False, + yes=False, +): + """Patch an existing Extension Instance.""" + + if ( + configuration_settings + or configuration_protected_settings + or configuration_settings_file + or configuration_protected_settings_file + ): + msg = ( + "Updating properties in --config-settings or --config-protected-settings may lead to undesirable state" + " if the cluster extension type does not support it. Please refer to the documentation of the" + " cluster extension service to check if updates to these properties is supported." + " Do you wish to proceed?" + ) user_confirmation_factory(cmd, yes, msg) # Determine ClusterRP - cluster_rp = __get_cluster_rp(cluster_type) + cluster_rp, _ = get_cluster_rp_api_version(cluster_type) # We need to determine the ExtensionType to call ExtensionFactory and create Extension class - extension = show_k8s_extension(client, resource_group_name, cluster_name, name, cluster_type) + extension = show_k8s_extension( + client, resource_group_name, cluster_name, name, cluster_type + ) extension_type_lower = extension.extension_type.lower() config_settings = {} config_protected_settings = {} # Get Configuration Settings from file if configuration_settings_file is not None: - config_settings = __read_config_settings_file(configuration_settings_file) + config_settings = read_config_settings_file(configuration_settings_file) if configuration_settings is not None: for dicts in configuration_settings: @@ -193,7 +274,9 @@ def update_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c # Get Configuration Protected Settings from file if configuration_protected_settings_file is not None: - config_protected_settings = __read_config_settings_file(configuration_protected_settings_file) + config_protected_settings = read_config_settings_file( + configuration_protected_settings_file + ) if configuration_protected_settings is not None: for dicts in configuration_protected_settings: @@ -203,55 +286,89 @@ def update_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c # Get the extension class based on the extension type extension_class = ExtensionFactory(extension_type_lower) - upd_extension = extension_class.Update(cmd, resource_group_name, cluster_name, auto_upgrade_minor_version, release_train, version, - config_settings, config_protected_settings) - - return sdk_no_wait(no_wait, client.begin_update, resource_group_name, cluster_rp, cluster_type, - cluster_name, name, upd_extension) - - -def delete_k8s_extension(cmd, client, resource_group_name, cluster_name, name, cluster_type, - no_wait=False, yes=False, force=False): - """Delete an existing Kubernetes Extension. - """ + upd_extension = extension_class.Update( + cmd, + resource_group_name, + cluster_name, + auto_upgrade_minor_version, + release_train, + version, + config_settings, + config_protected_settings, + yes, + ) + + return sdk_no_wait( + no_wait, + client.begin_update, + resource_group_name, + cluster_rp, + cluster_type, + cluster_name, + name, + upd_extension, + ) + + +def delete_k8s_extension( + cmd, + client, + resource_group_name, + cluster_name, + name, + cluster_type, + no_wait=False, + yes=False, + force=False, +): + """Delete an existing Kubernetes Extension.""" # Determine ClusterRP - cluster_rp = __get_cluster_rp(cluster_type) + cluster_rp, _ = get_cluster_rp_api_version(cluster_type) extension = None try: - extension = client.get(resource_group_name, cluster_rp, cluster_type, cluster_name, name) + extension = client.get( + resource_group_name, cluster_rp, cluster_type, cluster_name, name + ) except HttpResponseError: - logger.warning("No extension with name '%s' found on cluster '%s', so nothing to delete", name, cluster_name) + logger.warning( + "No extension with name '%s' found on cluster '%s', so nothing to delete", + name, + cluster_name, + ) return None extension_class = ExtensionFactory(extension.extension_type.lower()) # If there is any custom delete logic, this will call the logic - extension_class.Delete(cmd, client, resource_group_name, cluster_name, - name, cluster_type, yes) - - return sdk_no_wait(no_wait, client.begin_delete, resource_group_name, - cluster_rp, cluster_type, cluster_name, name, force_delete=force) - - -def __create_identity(cmd, resource_group_name, cluster_name, cluster_type, cluster_rp): + extension_class.Delete( + cmd, client, resource_group_name, cluster_name, name, cluster_type, yes + ) + + return sdk_no_wait( + no_wait, + client.begin_delete, + resource_group_name, + cluster_rp, + cluster_type, + cluster_name, + name, + force_delete=force, + ) + + +def __create_identity(cmd, resource_group_name, cluster_name, cluster_type): subscription_id = get_subscription_id(cmd.cli_ctx) resources = cf_resources(cmd.cli_ctx, subscription_id) - cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/{2}/{3}/{4}'.format(subscription_id, - resource_group_name, - cluster_rp, - cluster_type, - cluster_name) - - if cluster_rp == 'Microsoft.Kubernetes': - parent_api_version = '2020-01-01-preview' - elif cluster_rp == 'Microsoft.ResourceConnector': - parent_api_version = '2020-09-15-privatepreview' - elif cluster_rp == 'Microsoft.ContainerService': + if cluster_type.lower() == consts.MANAGED_CLUSTER_TYPE: return None, None - else: - raise InvalidArgumentValueError( - "Error! Cluster type '{}' is not supported for extension identity".format(cluster_type) + + cluster_rp, parent_api_version = get_cluster_rp_api_version(cluster_type) + + cluster_resource_id = ( + "/subscriptions/{0}/resourceGroups/{1}/providers/{2}/{3}/{4}".format( + subscription_id, resource_group_name, cluster_rp, cluster_type, cluster_name ) + ) try: resource = resources.get_by_id(cluster_resource_id, parent_api_version) @@ -263,32 +380,25 @@ def __create_identity(cmd, resource_group_name, cluster_name, cluster_type, clus return Identity(type=identity_type), location -def __get_cluster_rp(cluster_type): - rp = "" - if cluster_type.lower() == 'connectedclusters': - rp = 'Microsoft.Kubernetes' - elif cluster_type.lower() == 'appliances': - rp = 'Microsoft.ResourceConnector' - elif cluster_type.lower() == '' or cluster_type.lower() == 'managedclusters': - rp = 'Microsoft.ContainerService' - else: - raise InvalidArgumentValueError("Error! Cluster type '{}' is not supported".format(cluster_type)) - return rp - - def __validate_scope_and_namespace(scope, release_namespace, target_namespace): - if scope == 'cluster': + if scope == "cluster": if target_namespace is not None: message = "When --scope is 'cluster', --target-namespace must not be given." raise MutuallyExclusiveArgumentError(message) else: if release_namespace is not None: - message = "When --scope is 'namespace', --release-namespace must not be given." + message = ( + "When --scope is 'namespace', --release-namespace must not be given." + ) raise MutuallyExclusiveArgumentError(message) def __validate_scope_after_customization(scope_obj: Scope): - if scope_obj is not None and scope_obj.namespace is not None and scope_obj.namespace.target_namespace is None: + if ( + scope_obj is not None + and scope_obj.namespace is not None + and scope_obj.namespace.target_namespace is None + ): message = "When --scope is 'namespace', --target-namespace must be given." raise RequiredArgumentMissingError(message) @@ -300,18 +410,3 @@ def __validate_version_and_auto_upgrade(version, auto_upgrade_minor_version): raise MutuallyExclusiveArgumentError(message) auto_upgrade_minor_version = False - - -def __read_config_settings_file(file_path): - try: - with open(file_path, 'r') as f: - settings = json.load(f) - if len(settings) == 0: - raise Exception("File {} is empty".format(file_path)) - return settings - except ValueError as ex: - raise Exception("File {} is not a valid JSON file".format(file_path)) from ex - - -def is_dogfood_cluster(cmd): - return urlparse(cmd.cli_ctx.cloud.endpoints.resource_manager).hostname == consts.DF_RM_HOSTNAME diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 706973d9a13..d9c5f5e31f0 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -10,6 +10,7 @@ import copy from hashlib import md5 from typing import Any, Dict, List, Tuple +from azext_k8s_extension.utils import get_cluster_rp_api_version import azure.mgmt.relay import azure.mgmt.relay.models @@ -84,6 +85,9 @@ def __init__(self): # constants for existing AKS to AMLARC migration self.IS_AKS_MIGRATION = 'isAKSMigration' + # constants for others in Spec + self.installNvidiaDevicePlugin = 'installNvidiaDevicePlugin' + # reference mapping self.reference_mapping = { self.RELAY_SERVER_CONNECTION_STRING: [self.RELAY_CONNECTION_STRING_KEY], @@ -108,7 +112,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t # get the arc's location subscription_id = get_subscription_id(cmd.cli_ctx) - cluster_rp, parent_api_version = _get_cluster_rp_api_version(cluster_type) + cluster_rp, parent_api_version = get_cluster_rp_api_version(cluster_type) cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/{2}' \ '/{3}/{4}'.format(subscription_id, resource_group_name, cluster_rp, cluster_type, cluster_name) cluster_location = '' @@ -165,9 +169,83 @@ def Delete(self, cmd, client, resource_group_name, cluster_name, name, cluster_t user_confirmation_factory(cmd, yes) def Update(self, cmd, resource_group_name, cluster_name, auto_upgrade_minor_version, release_train, version, configuration_settings, - configuration_protected_settings): + configuration_protected_settings, yes=False): self.__normalize_config(configuration_settings, configuration_protected_settings) + # Prompt message to ask customer to confirm again + if len(configuration_settings) > 0: + impactScenario = "" + messageBody = "" + disableTraining = False + disableInference = False + disableNvidiaDevicePlugin = False + hasAllowInsecureConnections = False + hasPrivateEndpointNodeport = False + hasPrivateEndpointILB = False + hasNodeSelector = False + enableLogAnalyticsWS = False + + enableTraining = _get_value_from_config_protected_config(self.ENABLE_TRAINING, configuration_settings, configuration_protected_settings) + if enableTraining is not None: + disableTraining = str(enableTraining).lower() == 'false' + if disableTraining: + messageBody = messageBody + "enableTraining from True to False,\n" + + enableInference = _get_value_from_config_protected_config(self.ENABLE_INFERENCE, configuration_settings, configuration_protected_settings) + if enableInference is not None: + disableInference = str(enableInference).lower() == 'false' + if disableInference: + messageBody = messageBody + "enableInference from True to False,\n" + + installNvidiaDevicePlugin = _get_value_from_config_protected_config(self.installNvidiaDevicePlugin, configuration_settings, configuration_protected_settings) + if installNvidiaDevicePlugin is not None: + disableNvidiaDevicePlugin = str(installNvidiaDevicePlugin).lower() == 'false' + if disableNvidiaDevicePlugin: + messageBody = messageBody + "installNvidiaDevicePlugin from True to False if Nvidia GPU is used,\n" + + allowInsecureConnections = _get_value_from_config_protected_config(self.allowInsecureConnections, configuration_settings, configuration_protected_settings) + if allowInsecureConnections is not None: + hasAllowInsecureConnections = True + messageBody = messageBody + "allowInsecureConnections\n" + + privateEndpointNodeport = _get_value_from_config_protected_config(self.privateEndpointNodeport, configuration_settings, configuration_protected_settings) + if privateEndpointNodeport is not None: + hasPrivateEndpointNodeport = True + messageBody = messageBody + "privateEndpointNodeport\n" + + privateEndpointILB = _get_value_from_config_protected_config(self.privateEndpointILB, configuration_settings, configuration_protected_settings) + if privateEndpointILB is not None: + hasPrivateEndpointILB = True + messageBody = messageBody + "privateEndpointILB\n" + + hasNodeSelector = _check_nodeselector_existed(configuration_settings, configuration_protected_settings) + if hasNodeSelector: + messageBody = messageBody + "nodeSelector. Update operation can't remove an existed node selector, but can update or add new ones.\n" + + logAnalyticsWS = _get_value_from_config_protected_config(self.LOG_ANALYTICS_WS_ENABLED, configuration_settings, configuration_protected_settings) + if logAnalyticsWS is not None: + enableLogAnalyticsWS = str(logAnalyticsWS).lower() == 'true' + if enableLogAnalyticsWS: + messageBody = messageBody + "To update logAnalyticsWS from False to True, please provide all original configurationProtectedSettings. Otherwise, those settings would be considered obsolete and deleted.\n" + + if disableTraining or disableNvidiaDevicePlugin or hasNodeSelector: + impactScenario = "jobs" + + if disableInference or disableNvidiaDevicePlugin or hasAllowInsecureConnections or hasPrivateEndpointNodeport or hasPrivateEndpointILB or hasNodeSelector: + if impactScenario == "": + impactScenario = "online endpoints and deployments" + else: + impactScenario = impactScenario + ", online endpoints and deployments" + + if impactScenario != "": + message = ("\nThe following configuration update will IMPACT your active Machine Learning " + impactScenario + + ". It will be the safe update if the cluster doesn't have active Machine Learning " + impactScenario + ".\n\n" + messageBody + "\nProceed?") + user_confirmation_factory(cmd, yes, message=message) + else: + if enableLogAnalyticsWS: + message = "\n" + messageBody + "\nProceed?" + user_confirmation_factory(cmd, yes, message=message) + if len(configuration_protected_settings) > 0: subscription_id = get_subscription_id(cmd.cli_ctx) @@ -543,18 +621,12 @@ def _get_value_from_config_protected_config(key, config, protected_config): return protected_config.get(key) -def _get_cluster_rp_api_version(cluster_type) -> Tuple[str, str]: - rp = '' - parent_api_version = '' - if cluster_type.lower() == 'connectedclusters': - rp = 'Microsoft.Kubernetes' - parent_api_version = '2020-01-01-preview' - elif cluster_type.lower() == 'appliances': - rp = 'Microsoft.ResourceConnector' - parent_api_version = '2020-09-15-privatepreview' - elif cluster_type.lower() == '' or cluster_type.lower() == 'managedclusters': - rp = 'Microsoft.ContainerService' - parent_api_version = '2021-05-01' - else: - raise InvalidArgumentValueError("Error! Cluster type '{}' is not supported".format(cluster_type)) - return rp, parent_api_version +def _check_nodeselector_existed(configuration_settings, configuration_protected_settings): + config_keys = configuration_settings.keys() + config_protected_keys = configuration_protected_settings.keys() + all_keys = set(config_keys) | set(config_protected_keys) + if all_keys: + for key in all_keys: + if "nodeSelector" in key: + return True + return False diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py index 5b76e500635..a15defc72d2 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -17,19 +17,35 @@ class DefaultExtension(PartnerExtensionModel): - def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, - scope, auto_upgrade_minor_version, release_train, version, target_namespace, - release_namespace, configuration_settings, configuration_protected_settings, - configuration_settings_file, configuration_protected_settings_file): + def Create( + self, + cmd, + client, + resource_group_name, + cluster_name, + name, + cluster_type, + extension_type, + scope, + auto_upgrade_minor_version, + release_train, + version, + target_namespace, + release_namespace, + configuration_settings, + configuration_protected_settings, + configuration_settings_file, + configuration_protected_settings_file, + ): """Default validations & defaults for Create - Must create and return a valid 'Extension' object. + Must create and return a valid 'Extension' object. """ ext_scope = None if scope is not None: - if scope.lower() == 'cluster': + if scope.lower() == "cluster": scope_cluster = ScopeCluster(release_namespace=release_namespace) ext_scope = Scope(cluster=scope_cluster, namespace=None) - elif scope.lower() == 'namespace': + elif scope.lower() == "namespace": scope_namespace = ScopeNamespace(target_namespace=target_namespace) ext_scope = Scope(namespace=scope_namespace, cluster=None) @@ -41,27 +57,43 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t version=version, scope=ext_scope, configuration_settings=configuration_settings, - configuration_protected_settings=configuration_protected_settings + configuration_protected_settings=configuration_protected_settings, ) return extension, name, create_identity - def Update(self, cmd, resource_group_name, cluster_name, auto_upgrade_minor_version, release_train, version, configuration_settings, - configuration_protected_settings): + def Update( + self, + cmd, + resource_group_name, + cluster_name, + auto_upgrade_minor_version, + release_train, + version, + configuration_settings, + configuration_protected_settings, + yes=False, + ): """Default validations & defaults for Update - Must create and return a valid 'PatchExtension' object. + Must create and return a valid 'PatchExtension' object. """ - return PatchExtension(auto_upgrade_minor_version=auto_upgrade_minor_version, - release_train=release_train, - version=version, - configuration_settings=configuration_settings, - configuration_protected_settings=configuration_protected_settings) + return PatchExtension( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + ) - def Delete(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, yes): + def Delete( + self, cmd, client, resource_group_name, cluster_name, name, cluster_type, yes + ): user_confirmation_factory(cmd, yes) -def user_confirmation_factory(cmd, yes, message="Are you sure you want to perform this operation?"): - if cmd.cli_ctx.config.getboolean('core', 'disable_confirm_prompt', fallback=False): +def user_confirmation_factory( + cmd, yes, message="Are you sure you want to perform this operation?" +): + if cmd.cli_ctx.config.getboolean("core", "disable_confirm_prompt", fallback=False): return user_confirmation(message, yes=yes) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py index 33c8f683591..d4f1eeba6c3 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py @@ -10,18 +10,52 @@ class PartnerExtensionModel(ABC): @abstractmethod - def Create(self, cmd, client, resource_group_name: str, cluster_name: str, name: str, cluster_type: str, - extension_type: str, scope: str, auto_upgrade_minor_version: bool, release_train: str, version: str, - target_namespace: str, release_namespace: str, configuration_settings: dict, - configuration_protected_settings: dict, configuration_settings_file: str, - configuration_protected_settings_file: str) -> Extension: + def Create( + self, + cmd, + client, + resource_group_name: str, + cluster_name: str, + name: str, + cluster_type: str, + extension_type: str, + scope: str, + auto_upgrade_minor_version: bool, + release_train: str, + version: str, + target_namespace: str, + release_namespace: str, + configuration_settings: dict, + configuration_protected_settings: dict, + configuration_settings_file: str, + configuration_protected_settings_file: str, + ) -> Extension: pass @abstractmethod - def Update(self, cmd, resource_group_name: str, cluster_name: str, auto_upgrade_minor_version: bool, release_train: str, version: str, - configuration_settings: dict, configuration_protected_settings: dict) -> PatchExtension: + def Update( + self, + cmd, + resource_group_name: str, + cluster_name: str, + auto_upgrade_minor_version: bool, + release_train: str, + version: str, + configuration_settings: dict, + configuration_protected_settings: dict, + yes: bool, + ) -> PatchExtension: pass @abstractmethod - def Delete(self, cmd, client, resource_group_name: str, cluster_name: str, name: str, cluster_type: str, yes: bool): + def Delete( + self, + cmd, + client, + resource_group_name: str, + cluster_name: str, + name: str, + cluster_type: str, + yes: bool, + ): pass diff --git a/src/k8s-extension/azext_k8s_extension/utils.py b/src/k8s-extension/azext_k8s_extension/utils.py new file mode 100644 index 00000000000..9c7322cdf70 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/utils.py @@ -0,0 +1,43 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +from typing import Tuple +from urllib.parse import urlparse +from azext_k8s_extension import consts +from azure.cli.core.azclierror import InvalidArgumentValueError + + +def get_cluster_rp_api_version(cluster_type) -> Tuple[str, str]: + if cluster_type.lower() == consts.CONNECTED_CLUSTER_TYPE: + return consts.CONNECTED_CLUSTER_RP, consts.CONNECTED_CLUSTER_API_VERSION + if cluster_type.lower() == consts.APPLIANCE_TYPE: + return consts.APPLIANCE_RP, consts.APPLIANCE_API_VERSION + if ( + cluster_type.lower() == "" + or cluster_type.lower() == consts.MANAGED_CLUSTER_TYPE + ): + return consts.MANAGED_CLUSTER_RP, consts.MANAGED_CLUSTER_API_VERSION + raise InvalidArgumentValueError( + "Error! Cluster type '{}' is not supported".format(cluster_type) + ) + + +def read_config_settings_file(file_path): + try: + with open(file_path, "r") as f: + settings = json.load(f) + if len(settings) == 0: + raise Exception("File {} is empty".format(file_path)) + return settings + except ValueError as ex: + raise Exception("File {} is not a valid JSON file".format(file_path)) from ex + + +def is_dogfood_cluster(cmd): + return ( + urlparse(cmd.cli_ctx.cloud.endpoints.resource_manager).hostname + == consts.DF_RM_HOSTNAME + ) diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 40946af2ee3..fce65b7232c 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -13,45 +13,46 @@ from azure_bdist_wheel import cmdclass except ImportError: from distutils import log as logger + logger.warn("Wheel is not available, disabling bdist_wheel hook") # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'License :: OSI Approved :: MIT License', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: MIT License", ] # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "1.0.1" +VERSION = "1.0.2" -with open('README.rst', 'r', encoding='utf-8') as f: +with open("README.rst", "r", encoding="utf-8") as f: README = f.read() -with open('HISTORY.rst', 'r', encoding='utf-8') as f: +with open("HISTORY.rst", "r", encoding="utf-8") as f: HISTORY = f.read() setup( name="k8s-extension", version=VERSION, - description='Microsoft Azure Command-Line Tools K8s-extension Extension', + description="Microsoft Azure Command-Line Tools K8s-extension Extension", # TODO: Update author and email, if applicable - author='Microsoft Corporation', - author_email='azpycli@microsoft.com', + author="Microsoft Corporation", + author_email="azpycli@microsoft.com", # TODO: consider pointing directly to your source code instead of the generic repo - url='https://github.com/Azure/azure-cli-extensions', - long_description=README + '\n\n' + HISTORY, - license='MIT', + url="https://github.com/Azure/azure-cli-extensions", + long_description=README + "\n\n" + HISTORY, + license="MIT", classifiers=CLASSIFIERS, packages=find_packages(), install_requires=DEPENDENCIES, - package_data={'azext_k8s_extension': ['azext_metadata.json']}, + package_data={"azext_k8s_extension": ["azext_metadata.json"]}, )