From bd23e0bd85e964d8d8abec60fbb764e5ca0e2592 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 1 Apr 2021 10:58:16 -0700 Subject: [PATCH 1/9] Add check for provider registration and refactor --- .../_client_factory.py | 9 +- .../azext_k8s_configuration/_consts.py | 6 + .../azext_k8s_configuration/_params.py | 8 +- .../azext_k8s_configuration/_utils.py | 110 ++++++++++ .../azext_k8s_configuration/_validators.py | 88 +++++++- .../azext_k8s_configuration/custom.py | 201 +++--------------- .../tests/latest/test_validators.py | 50 ++--- 7 files changed, 257 insertions(+), 215 deletions(-) create mode 100644 src/k8s-configuration/azext_k8s_configuration/_consts.py create mode 100644 src/k8s-configuration/azext_k8s_configuration/_utils.py diff --git a/src/k8s-configuration/azext_k8s_configuration/_client_factory.py b/src/k8s-configuration/azext_k8s_configuration/_client_factory.py index d294b6fdfa4..86cbd33b7f4 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_client_factory.py +++ b/src/k8s-configuration/azext_k8s_configuration/_client_factory.py @@ -3,13 +3,18 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.core.commands.client_factory import get_mgmt_service_client -def cf_k8s_configuration(cli_ctx, *_): - from azure.cli.core.commands.client_factory import get_mgmt_service_client +def cf_k8s_configuration(cli_ctx, *_): from azext_k8s_configuration.vendored_sdks import SourceControlConfigurationClient return get_mgmt_service_client(cli_ctx, SourceControlConfigurationClient) def cf_k8s_configuration_operation(cli_ctx, _): return cf_k8s_configuration(cli_ctx).source_control_configurations + + +def _resource_providers_client(cli_ctx): + from azure.mgmt.resource import ResourceManagementClient + return get_mgmt_service_client(cli_ctx, ResourceManagementClient).providers diff --git a/src/k8s-configuration/azext_k8s_configuration/_consts.py b/src/k8s-configuration/azext_k8s_configuration/_consts.py new file mode 100644 index 00000000000..a4b36021903 --- /dev/null +++ b/src/k8s-configuration/azext_k8s_configuration/_consts.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +PROVIDER_NAMESPACE = 'Microsoft.KubernetesConfiguration' diff --git a/src/k8s-configuration/azext_k8s_configuration/_params.py b/src/k8s-configuration/azext_k8s_configuration/_params.py index 9198ef3882e..c9faab38cfb 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_params.py +++ b/src/k8s-configuration/azext_k8s_configuration/_params.py @@ -13,7 +13,7 @@ ) from azure.cli.core.commands.validators import get_default_location_from_resource_group -from ._validators import validate_configuration_type, validate_operator_namespace, validate_operator_instance_name +from ._validators import _validate_configuration_type, _validate_operator_namespace, _validate_operator_instance_name def load_arguments(self, _): @@ -38,7 +38,7 @@ def load_arguments(self, _): arg_type=get_enum_type(['namespace', 'cluster']), help='''Specify scope of the operator to be 'namespace' or 'cluster' ''') c.argument('configuration_type', - validator=validate_configuration_type, + validator=_validate_configuration_type, arg_type=get_enum_type(['sourceControlConfiguration']), help='Type of the configuration') c.argument('enable_helm_operator', @@ -60,11 +60,11 @@ def load_arguments(self, _): c.argument('operator_instance_name', arg_group="Operator", help='Instance name of the Operator', - validator=validate_operator_instance_name) + validator=_validate_operator_instance_name) c.argument('operator_namespace', arg_group="Operator", help='Namespace in which to install the Operator', - validator=validate_operator_namespace) + validator=_validate_operator_namespace) c.argument('operator_type', arg_group="Operator", help='''Type of the operator. Valid value is 'flux' ''') diff --git a/src/k8s-configuration/azext_k8s_configuration/_utils.py b/src/k8s-configuration/azext_k8s_configuration/_utils.py new file mode 100644 index 00000000000..8d3aecd86b4 --- /dev/null +++ b/src/k8s-configuration/azext_k8s_configuration/_utils.py @@ -0,0 +1,110 @@ +import io +import base64 +from azure.cli.core.azclierror import InvalidArgumentValueError, \ + RequiredArgumentMissingError, MutuallyExclusiveArgumentError + +from paramiko.ed25519key import Ed25519Key +from paramiko.ssh_exception import SSHException +from Crypto.PublicKey import RSA, ECC, DSA + + +def _get_protected_settings(ssh_private_key, ssh_private_key_file, https_user, https_key): + protected_settings = {} + ssh_private_key_data = _get_data_from_key_or_file(ssh_private_key, ssh_private_key_file) + + # Add gitops private key data to protected settings if exists + # Dry-run all key types to determine if the private key is in a valid format + invalid_rsa_key, invalid_ecc_key, invalid_dsa_key, invalid_ed25519_key = (False, False, False, False) + if ssh_private_key_data != '': + try: + RSA.import_key(_from_base64(ssh_private_key_data)) + except ValueError: + invalid_rsa_key = True + try: + ECC.import_key(_from_base64(ssh_private_key_data)) + except ValueError: + invalid_ecc_key = True + try: + DSA.import_key(_from_base64(ssh_private_key_data)) + except ValueError: + invalid_dsa_key = True + try: + key_obj = io.StringIO(_from_base64(ssh_private_key_data).decode('utf-8')) + Ed25519Key(file_obj=key_obj) + except SSHException: + invalid_ed25519_key = True + + if invalid_rsa_key and invalid_ecc_key and invalid_dsa_key and invalid_ed25519_key: + raise InvalidArgumentValueError( + 'Error! ssh private key provided in invalid format', + 'Verify the key provided is a valid PEM-formatted key of type RSA, ECC, DSA, or Ed25519') + protected_settings["sshPrivateKey"] = ssh_private_key_data + + # Check if both httpsUser and httpsKey exist, then add to protected settings + if https_user != '' and https_key != '': + protected_settings['httpsUser'] = _to_base64(https_user) + protected_settings['httpsKey'] = _to_base64(https_key) + elif https_user != '': + raise RequiredArgumentMissingError( + 'Error! --https-user used without --https-key', + 'Try providing both --https-user and --https-key together') + elif https_key != '': + raise RequiredArgumentMissingError( + 'Error! --http-key used without --http-user', + 'Try providing both --https-user and --https-key together') + + return protected_settings + + +def _get_cluster_type(cluster_type): + if cluster_type.lower() == 'connectedclusters': + return 'Microsoft.Kubernetes' + # Since cluster_type is an enum of only two values, if not connectedClusters, it will be managedClusters. + return 'Microsoft.ContainerService' + + +def _fix_compliance_state(config): + # If we get Compliant/NonCompliant as compliance_sate, change them before returning + if config.compliance_status.compliance_state.lower() == 'noncompliant': + config.compliance_status.compliance_state = 'Failed' + elif config.compliance_status.compliance_state.lower() == 'compliant': + config.compliance_status.compliance_state = 'Installed' + + return config + + +def _get_data_from_key_or_file(key, filepath): + if key != '' and filepath != '': + raise MutuallyExclusiveArgumentError( + 'Error! Both textual key and key filepath cannot be provided', + 'Try providing the file parameter without providing the plaintext parameter') + data = '' + if filepath != '': + data = _read_key_file(filepath) + elif key != '': + data = key + return data + + +def _read_key_file(path): + try: + with open(path, "r") as myfile: # user passed in filename + data_list = myfile.readlines() # keeps newline characters intact + data_list_len = len(data_list) + if (data_list_len) <= 0: + raise Exception("File provided does not contain any data") + raw_data = ''.join(data_list) + return _to_base64(raw_data) + except Exception as ex: + raise InvalidArgumentValueError( + 'Error! Unable to read key file specified with: {0}'.format(ex), + 'Verify that the filepath specified exists and contains valid utf-8 data') from ex + + +def _from_base64(base64_str): + return base64.b64decode(base64_str) + + +def _to_base64(raw_data): + bytes_data = raw_data.encode('utf-8') + return base64.b64encode(bytes_data).decode('utf-8') diff --git a/src/k8s-configuration/azext_k8s_configuration/_validators.py b/src/k8s-configuration/azext_k8s_configuration/_validators.py index 0862de43205..810f2e0faf1 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_validators.py +++ b/src/k8s-configuration/azext_k8s_configuration/_validators.py @@ -4,34 +4,44 @@ # -------------------------------------------------------------------------------------------- import re -from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.core.azclierror import InvalidArgumentValueError, MutuallyExclusiveArgumentError + +from knack.log import get_logger +from azext_k8s_configuration._client_factory import _resource_providers_client +from azext_k8s_configuration._utils import _from_base64 +import azext_k8s_configuration._consts as consts +from urllib.parse import urlparse +from paramiko.hostkeys import HostKeyEntry + + +logger = get_logger(__name__) # Parameter-Level Validation -def validate_configuration_type(configuration_type): +def _validate_configuration_type(configuration_type): if configuration_type.lower() != 'sourcecontrolconfiguration': raise InvalidArgumentValueError( 'Invalid configuration-type', 'Try specifying the valid value "sourceControlConfiguration"') -def validate_operator_namespace(namespace): +def _validate_operator_namespace(namespace): if namespace.operator_namespace: - __validate_k8s_name(namespace.operator_namespace, "--operator-namespace", 23) + _validate_k8s_name(namespace.operator_namespace, "--operator-namespace", 23) -def validate_operator_instance_name(namespace): +def _validate_operator_instance_name(namespace): if namespace.operator_instance_name: - __validate_k8s_name(namespace.operator_instance_name, "--operator-instance-name", 23) + _validate_k8s_name(namespace.operator_instance_name, "--operator-instance-name", 23) # Create Parameter Validation -def validate_configuration_name(configuration_name): - __validate_k8s_name(configuration_name, "--name", 63) +def _validate_configuration_name(configuration_name): + _validate_k8s_name(configuration_name, "--name", 63) # Helper -def __validate_k8s_name(param_value, param_name, max_len): +def _validate_k8s_name(param_value, param_name, max_len): if len(param_value) > max_len: raise InvalidArgumentValueError( 'Error! Invalid {0}'.format(param_name), @@ -44,3 +54,63 @@ def __validate_k8s_name(param_value, param_name, max_len): raise InvalidArgumentValueError( 'Error! Invalid {0}'.format(param_name), 'Parameter {0} can only contain lowercase alphanumeric characters and hyphens'.format(param_name)) + + +def _validate_url_with_params(repository_url, ssh_private_key_set, known_hosts_contents_set, https_auth_set): + scheme = urlparse(repository_url).scheme + + if scheme in ('http', 'https'): + if ssh_private_key_set: + raise MutuallyExclusiveArgumentError( + 'Error! An ssh private key cannot be used with an http(s) url', + 'Verify the url provided is a valid ssh url and not an http(s) url') + if known_hosts_contents_set: + raise MutuallyExclusiveArgumentError( + 'Error! ssh known_hosts cannot be used with an http(s) url', + 'Verify the url provided is a valid ssh url and not an http(s) url') + if not https_auth_set and scheme == 'https': + logger.warning('Warning! https url is being used without https auth params, ensure the repository ' + 'url provided is not a private repo') + else: + if https_auth_set: + raise MutuallyExclusiveArgumentError( + 'Error! https auth (--https-user and --https-key) cannot be used with a non-http(s) url', + 'Verify the url provided is a valid http(s) url and not an ssh url') + + +def _validate_known_hosts(knownhost_data): + try: + knownhost_str = _from_base64(knownhost_data).decode('utf-8') + except Exception as ex: + raise InvalidArgumentValueError( + 'Error! ssh known_hosts is not a valid utf-8 base64 encoded string', + 'Verify that the string provided safely decodes into a valid utf-8 format') from ex + lines = knownhost_str.split('\n') + for line in lines: + line = line.strip(' ') + line_len = len(line) + if (line_len == 0) or (line[0] == "#"): + continue + try: + host_key = HostKeyEntry.from_line(line) + if not host_key: + raise Exception('not enough fields found in known_hosts line') + except Exception as ex: + raise InvalidArgumentValueError( + 'Error! ssh known_hosts provided in wrong format', + 'Verify that all lines in the known_hosts contents are provided in a valid sshd(8) format') from ex + + +# pylint: disable=broad-except +def _validate_cc_registration(cmd): + try: + rp_client = _resource_providers_client(cmd.cli_ctx) + registration_state = rp_client.get(consts.PROVIDER_NAMESPACE).registration_state + + if registration_state != "Registered": + logger.warning("'Source Control Configuration' cannot be used because '%s' provider has not been " + "registered. More details for registering this provider can be found here - " + "https://aka.ms/EnableCustomLocations", consts.PROVIDER_NAMESPACE) + except Exception: + logger.warning("Unable to fetch registration state of '%s' provider. " + "Failed to enable 'source control configuration' feature...", consts.PROVIDER_NAMESPACE) diff --git a/src/k8s-configuration/azext_k8s_configuration/custom.py b/src/k8s-configuration/azext_k8s_configuration/custom.py index de6f0dfc5ca..a7ac497c21a 100644 --- a/src/k8s-configuration/azext_k8s_configuration/custom.py +++ b/src/k8s-configuration/azext_k8s_configuration/custom.py @@ -3,21 +3,16 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import base64 -import io -from urllib.parse import urlparse -from azure.cli.core.azclierror import InvalidArgumentValueError, ResourceNotFoundError, \ - RequiredArgumentMissingError, MutuallyExclusiveArgumentError, CommandNotFoundError +from azure.cli.core.azclierror import ResourceNotFoundError, CommandNotFoundError from knack.log import get_logger -from paramiko.ed25519key import Ed25519Key -from paramiko.hostkeys import HostKeyEntry -from paramiko.ssh_exception import SSHException -from Crypto.PublicKey import RSA, ECC, DSA +from azext_k8s_configuration._utils import _get_protected_settings, _get_cluster_type, \ + _fix_compliance_state, _get_data_from_key_or_file +from azext_k8s_configuration._validators import _validate_known_hosts, _validate_url_with_params, \ + _validate_configuration_name, _validate_cc_registration from azext_k8s_configuration.vendored_sdks.models import SourceControlConfiguration from azext_k8s_configuration.vendored_sdks.models import HelmOperatorProperties from azext_k8s_configuration.vendored_sdks.models import ErrorResponseException -from ._validators import validate_configuration_name logger = get_logger(__name__) @@ -27,11 +22,11 @@ def show_k8s_configuration(client, resource_group_name, cluster_name, name, clus """ # Determine ClusterRP - cluster_rp = __get_cluster_type(cluster_type) + cluster_rp = _get_cluster_type(cluster_type) try: config = client.get(resource_group_name, cluster_rp, cluster_type, cluster_name, name) - return __fix_compliance_state(config) + return _fix_compliance_state(config) except ErrorResponseException as ex: # Customize the error message for resources not found if ex.response.status_code == 404: @@ -55,7 +50,7 @@ def show_k8s_configuration(client, resource_group_name, cluster_name, name, clus # pylint: disable=too-many-locals -def create_k8s_configuration(client, resource_group_name, cluster_name, name, repository_url, scope, cluster_type, +def create_k8s_configuration(cmd, client, resource_group_name, cluster_name, name, repository_url, scope, cluster_type, operator_instance_name=None, operator_namespace='default', helm_operator_chart_version='1.2.0', operator_type='flux', operator_params='', ssh_private_key='', ssh_private_key_file='', https_user='', https_key='', @@ -65,10 +60,10 @@ def create_k8s_configuration(client, resource_group_name, cluster_name, name, re """ # Validate configuration name - validate_configuration_name(name) + _validate_configuration_name(name) # Determine ClusterRP - cluster_rp = __get_cluster_type(cluster_type) + cluster_rp = _get_cluster_type(cluster_type) # Determine operatorInstanceName if operator_instance_name is None: @@ -81,22 +76,25 @@ def create_k8s_configuration(client, resource_group_name, cluster_name, name, re helm_operator_properties.chart_version = helm_operator_chart_version.strip() helm_operator_properties.chart_values = helm_operator_params.strip() - protected_settings = validate_and_get_protected_settings(ssh_private_key, - ssh_private_key_file, - https_user, - https_key) - knownhost_data = get_data_from_key_or_file(ssh_known_hosts, ssh_known_hosts_file) + protected_settings = _get_protected_settings(ssh_private_key, + ssh_private_key_file, + https_user, + https_key) + knownhost_data = _get_data_from_key_or_file(ssh_known_hosts, ssh_known_hosts_file) if knownhost_data: - validate_known_hosts(knownhost_data) + _validate_known_hosts(knownhost_data) # Flag which parameters have been set and validate these settings against the set repository url ssh_private_key_set = ssh_private_key != '' or ssh_private_key_file != '' known_hosts_contents_set = knownhost_data != '' https_auth_set = https_user != '' and https_key != '' - validate_url_with_params(repository_url, - ssh_private_key_set=ssh_private_key_set, - known_hosts_contents_set=known_hosts_contents_set, - https_auth_set=https_auth_set) + _validate_url_with_params(repository_url, + ssh_private_key_set=ssh_private_key_set, + known_hosts_contents_set=known_hosts_contents_set, + https_auth_set=https_auth_set) + + # Validate that the subscription is registered to Microsoft.KubernetesConfiguration + _validate_cc_registration(cmd.cli_ctx) # Create sourceControlConfiguration object source_control_configuration = SourceControlConfiguration(repository_url=repository_url, @@ -114,7 +112,7 @@ def create_k8s_configuration(client, resource_group_name, cluster_name, name, re config = client.create_or_update(resource_group_name, cluster_rp, cluster_type, cluster_name, name, source_control_configuration) - return __fix_compliance_state(config) + return _fix_compliance_state(config) def update_k8s_configuration(client, resource_group_name, cluster_name, name, cluster_type, @@ -188,7 +186,7 @@ def update_k8s_configuration(client, resource_group_name, cluster_name, name, cl def list_k8s_configuration(client, resource_group_name, cluster_name, cluster_type): - cluster_rp = __get_cluster_type(cluster_type) + cluster_rp = _get_cluster_type(cluster_type) return client.list(resource_group_name, cluster_rp, cluster_type, cluster_name) @@ -197,155 +195,8 @@ def delete_k8s_configuration(client, resource_group_name, cluster_name, name, cl """ # Determine ClusterRP - cluster_rp = __get_cluster_type(cluster_type) + cluster_rp = _get_cluster_type(cluster_type) source_control_configuration_name = name return client.delete(resource_group_name, cluster_rp, cluster_type, cluster_name, source_control_configuration_name) - - -def validate_and_get_protected_settings(ssh_private_key, ssh_private_key_file, https_user, https_key): - protected_settings = {} - ssh_private_key_data = get_data_from_key_or_file(ssh_private_key, ssh_private_key_file) - - # Add gitops private key data to protected settings if exists - # Dry-run all key types to determine if the private key is in a valid format - invalid_rsa_key, invalid_ecc_key, invalid_dsa_key, invalid_ed25519_key = (False, False, False, False) - if ssh_private_key_data != '': - try: - RSA.import_key(from_base64(ssh_private_key_data)) - except ValueError: - invalid_rsa_key = True - try: - ECC.import_key(from_base64(ssh_private_key_data)) - except ValueError: - invalid_ecc_key = True - try: - DSA.import_key(from_base64(ssh_private_key_data)) - except ValueError: - invalid_dsa_key = True - try: - key_obj = io.StringIO(from_base64(ssh_private_key_data).decode('utf-8')) - Ed25519Key(file_obj=key_obj) - except SSHException: - invalid_ed25519_key = True - - if invalid_rsa_key and invalid_ecc_key and invalid_dsa_key and invalid_ed25519_key: - raise InvalidArgumentValueError( - 'Error! ssh private key provided in invalid format', - 'Verify the key provided is a valid PEM-formatted key of type RSA, ECC, DSA, or Ed25519') - protected_settings["sshPrivateKey"] = ssh_private_key_data - - # Check if both httpsUser and httpsKey exist, then add to protected settings - if https_user != '' and https_key != '': - protected_settings['httpsUser'] = to_base64(https_user) - protected_settings['httpsKey'] = to_base64(https_key) - elif https_user != '': - raise RequiredArgumentMissingError( - 'Error! --https-user used without --https-key', - 'Try providing both --https-user and --https-key together') - elif https_key != '': - raise RequiredArgumentMissingError( - 'Error! --http-key used without --http-user', - 'Try providing both --https-user and --https-key together') - - return protected_settings - - -def __get_cluster_type(cluster_type): - if cluster_type.lower() == 'connectedclusters': - return 'Microsoft.Kubernetes' - # Since cluster_type is an enum of only two values, if not connectedClusters, it will be managedClusters. - return 'Microsoft.ContainerService' - - -def __fix_compliance_state(config): - # If we get Compliant/NonCompliant as compliance_sate, change them before returning - if config.compliance_status.compliance_state.lower() == 'noncompliant': - config.compliance_status.compliance_state = 'Failed' - elif config.compliance_status.compliance_state.lower() == 'compliant': - config.compliance_status.compliance_state = 'Installed' - - return config - - -def validate_url_with_params(repository_url, ssh_private_key_set, known_hosts_contents_set, https_auth_set): - scheme = urlparse(repository_url).scheme - - if scheme in ('http', 'https'): - if ssh_private_key_set: - raise MutuallyExclusiveArgumentError( - 'Error! An ssh private key cannot be used with an http(s) url', - 'Verify the url provided is a valid ssh url and not an http(s) url') - if known_hosts_contents_set: - raise MutuallyExclusiveArgumentError( - 'Error! ssh known_hosts cannot be used with an http(s) url', - 'Verify the url provided is a valid ssh url and not an http(s) url') - if not https_auth_set and scheme == 'https': - logger.warning('Warning! https url is being used without https auth params, ensure the repository ' - 'url provided is not a private repo') - else: - if https_auth_set: - raise MutuallyExclusiveArgumentError( - 'Error! https auth (--https-user and --https-key) cannot be used with a non-http(s) url', - 'Verify the url provided is a valid http(s) url and not an ssh url') - - -def validate_known_hosts(knownhost_data): - try: - knownhost_str = from_base64(knownhost_data).decode('utf-8') - except Exception as ex: - raise InvalidArgumentValueError( - 'Error! ssh known_hosts is not a valid utf-8 base64 encoded string', - 'Verify that the string provided safely decodes into a valid utf-8 format') from ex - lines = knownhost_str.split('\n') - for line in lines: - line = line.strip(' ') - line_len = len(line) - if (line_len == 0) or (line[0] == "#"): - continue - try: - host_key = HostKeyEntry.from_line(line) - if not host_key: - raise Exception('not enough fields found in known_hosts line') - except Exception as ex: - raise InvalidArgumentValueError( - 'Error! ssh known_hosts provided in wrong format', - 'Verify that all lines in the known_hosts contents are provided in a valid sshd(8) format') from ex - - -def get_data_from_key_or_file(key, filepath): - if key != '' and filepath != '': - raise MutuallyExclusiveArgumentError( - 'Error! Both textual key and key filepath cannot be provided', - 'Try providing the file parameter without providing the plaintext parameter') - data = '' - if filepath != '': - data = read_key_file(filepath) - elif key != '': - data = key - return data - - -def read_key_file(path): - try: - with open(path, "r") as myfile: # user passed in filename - data_list = myfile.readlines() # keeps newline characters intact - data_list_len = len(data_list) - if (data_list_len) <= 0: - raise Exception("File provided does not contain any data") - raw_data = ''.join(data_list) - return to_base64(raw_data) - except Exception as ex: - raise InvalidArgumentValueError( - 'Error! Unable to read key file specified with: {0}'.format(ex), - 'Verify that the filepath specified exists and contains valid utf-8 data') from ex - - -def from_base64(base64_str): - return base64.b64decode(base64_str) - - -def to_base64(raw_data): - bytes_data = raw_data.encode('utf-8') - return base64.b64encode(bytes_data).decode('utf-8') diff --git a/src/k8s-configuration/azext_k8s_configuration/tests/latest/test_validators.py b/src/k8s-configuration/azext_k8s_configuration/tests/latest/test_validators.py index ce3db8b84f4..052b8a30164 100644 --- a/src/k8s-configuration/azext_k8s_configuration/tests/latest/test_validators.py +++ b/src/k8s-configuration/azext_k8s_configuration/tests/latest/test_validators.py @@ -6,7 +6,7 @@ import unittest import base64 from azure.cli.core.azclierror import InvalidArgumentValueError, MutuallyExclusiveArgumentError -from azext_k8s_configuration.custom import validate_and_get_protected_settings, validate_url_with_params, validate_known_hosts +from azext_k8s_configuration._utils import _get_protected_settings import azext_k8s_configuration._validators as validators from Crypto.PublicKey import RSA, ECC, DSA from paramiko.ed25519key import Ed25519Key @@ -17,31 +17,31 @@ def test_bad_private_key(self): private_key_encoded = base64.b64encode("this is not a valid private key".encode('utf-8')).decode('utf-8') err = "Error! ssh private key provided in invalid format" with self.assertRaises(InvalidArgumentValueError) as cm: - validate_and_get_protected_settings(private_key_encoded, '', '', '') + _get_protected_settings(private_key_encoded, '', '', '') self.assertEqual(str(cm.exception), err) def test_rsa_private_key(self): rsa_key = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUJsd0FBQUFkemMyZ3RjbgpOaEFBQUFBd0VBQVFBQUFZRUF1bVA5M09qRHdjdlEyZHZhRlJNNWYrMEhVSnFvOFJnbmdwaGN3NFZidnd1TVNoQTZFc2FyCjFsam1CNUNnT1NGNHJqNDIvcmdxMW1hWndoSUgvckdPSElNa0lIcjFrZmNKMnBrR3ZhK1NxVm4wWUhzMjBpUW02ay92ZXQKdXdVQ2J1QjlxSU5zL2h2b0ppQ21JMUVpVWZ4VGoxRFJCUG15OXR3Qm52bW5FS1kxZ2NhT2YrS2Y1aGhCc09pd00yZnBRTwp0aTlIcHVzM1JhNXpFeElWbjJzVitpRjVvV3ZZM1JQTTlKNXFPMXRObUtOWll6TjgzbDYxMlBzRmR1Vm1QM2NUUlJtK2pzCjdzZW5jY0U0RitzU0hQMlJpMk5DU0JvZ2RJOFR5VTlzeTM3Szl3bFJ5NGZkWWI1K1o3YUZjMjhTNDdDWlo5dTRFVXdWUEYKbjU4dTUzajU0empwdXNpei9ZWmx3MG5NeEQ5SXI0aHlJZ2s0NlUzVmdHR0NPUytZVTVZT2JURGhPRG5udk5VRkg2NVhCagpEM3l6WVJuRDA3b2swQ1JUR3RCOWMzTjBFNDBjUnlPeVpEQ0l5a0FPdHZXYnBUZzdnaXA2UDc4K2pLVlFnanFwRTVQdi9ICnl1dlB6cUJoUkpWcG5VR1dvWnFlcWJhd2N5RWZwdHFLaTNtWUdVMHBBQUFGa0U5cUs3SlBhaXV5QUFBQUIzTnphQzF5YzIKRUFBQUdCQUxwai9kem93OEhMME5uYjJoVVRPWC90QjFDYXFQRVlKNEtZWE1PRlc3OExqRW9RT2hMR3E5Wlk1Z2VRb0RraAplSzQrTnY2NEt0Wm1tY0lTQi82eGpoeURKQ0I2OVpIM0NkcVpCcjJ2a3FsWjlHQjdOdElrSnVwUDczcmJzRkFtN2dmYWlECmJQNGI2Q1lncGlOUklsSDhVNDlRMFFUNXN2YmNBWjc1cHhDbU5ZSEdqbi9pbitZWVFiRG9zRE5uNlVEcll2UjZick4wV3UKY3hNU0ZaOXJGZm9oZWFGcjJOMFR6UFNlYWp0YlRaaWpXV016Zk41ZXRkajdCWGJsWmo5M0UwVVp2bzdPN0hwM0hCT0JmcgpFaHo5a1l0alFrZ2FJSFNQRThsUGJNdCt5dmNKVWN1SDNXRytmbWUyaFhOdkV1T3dtV2ZidUJGTUZUeForZkx1ZDQrZU00CjZicklzLzJHWmNOSnpNUS9TSytJY2lJSk9PbE4xWUJoZ2prdm1GT1dEbTB3NFRnNTU3elZCUit1VndZdzk4czJFWnc5TzYKSk5Ba1V4clFmWE56ZEJPTkhFY2pzbVF3aU1wQURyYjFtNlU0TzRJcWVqKy9Qb3lsVUlJNnFST1Q3L3g4cnJ6ODZnWVVTVgphWjFCbHFHYW5xbTJzSE1oSDZiYWlvdDVtQmxOS1FBQUFBTUJBQUVBQUFHQkFMaElmSXFacUZKSFRXcllyN24rays4alR3ClFtcGJvWmc1YmZSWGdhdGljaEo4ZGlXOGlNblFFRVRBcFd0OU5FZ0tqbDRrSGRuSnoyUERkZzFIN0ExaHppbkNsdzZMTTAKYUkyMGxyR2NrWWpXNDRNd3ozYmRQNHlURTllSXRiM0pmN1pNSGpqek4rSy96bWN0eWdMeXFZSzVXYTljM1JnMXdIRWFNNAplakUvNDg4M25WUmJvSFJDcjFCVi8wQVVFTTZhNisrRHpVZW9WdWdWL3RsV3RVMlJuQlZ4eCtJS0FVSDZRTHJFU2JkUkRoCkVGUEFhRWtEb3crd3dDcFpqTXBhMHdRZXBDSkhwWkJLN1pBU25EU3R3Y2RKRE4yeHZzdVNOOGg0bkN0MlZWd0xRenJKeVAKU2VjcWM3M1hIc3E3VWx6ZU5veHlTVW9KZ2JjNTZoRzhWYS9ITlhsOUtkdkFlWUVzS1l1OW5NRUprVSt3VHo1KzUvM2wwVQpxSkErb0pTVTducjYydlVKQnljbXg0SFdBcjJ6QkR2QnFBUWMzRG9LWHczeVM1Z0c5Zkc0c25OUUkxOHVRSjdOSjdndHZHClpKRU56bTNJMmFTMzl5dndWZnFIMXpXVERxU2VNeWhYeWFnTkFEcGtCVEJIMVJQR2NtTFplclFmWWx1djVVUmFNTXdRQUEKQU1BdE9oNHFwUUhidm5tQ1RVakx4dXRrWnRaRlhNa0hmSTk5NS9Nd2RvWVY1eWRKV0pUVGsyKzB1QVBIcTZEejk2b3dWbQpjUkF2WDBDOVU5d3ZRMkpnR0Y1MDZzcmgzZkVpUzM2d1ArOFd0RjZ6ODd0enJwQnpQVHIxOGRONURCOEx5L3dXRk5BVTdqClBUbXM0dHlUY1VsRXR3eEt4TXJTNC9ROUZwMWozL3JNdnNZdGVaSVgycmN4YUhkWWJDVGJtTUpZS3lVTWVXTk56NXpub1EKcFcyd2NDSmpJc1MvS1F2WmR4cHZwNWd0RXE1WlEva3FvLzJVRWd1NHhwdDNWeUNma0FBQURCQVBOSHVEU1R0ZEpJWjdzcwpaQkVwcUE4TE54b1dMQ2RURnlpRERiUnpYOWVPTldkRFQ3NklaRE9HejczNXJhZUFSM2FiY0FhaUM0dDQwTFJTNGEyN29sCm9wK1dSak9wcjVNYUtOUnk4MCt6VWw3WUlSMjErKzVnMFVnNkRnQlBEdmFJSHFSTnRsZ2gyVXdTL0cva1lOaUlEY0JiS1EKOUcvdTI4ekRIRUtNL21YYS8wYnFtSm16ZUYvY1BLdHdScFE3clFoRnAwUkdFcnZtc0l4dDl6K0ZZZUdncjFBYUVTV0ZlTApmUmZsa0lnOVBWOEl0b09GN25qK2VtMkxkNTNCS1hSUUFBQU1FQXhDTFBueHFFVEsyMW5QOXFxQVYzMEZUUkhGNW9kRHg4ClpiYnZIbjgwdEgxQjYwZjRtTGJFRm56REZFR0NwS2Rwb3dyUXR6WUhnQzNBaGNJUE9BbXFXaDg0UEFPbisreHhFanNaQkwKRWhVWmNFUndkYTMzTnJDNTVEMzZxbDBMZEsrSGRuZUFzVGZhazh0bWVlOTJWb0RxdWovNGFSMjBmUTBJUFVzMU8rWHNRNQpGWVFYQzZndExHZGRzRVFoSDF6MTh6RGtWa1UwdEhlZkJaL2pFZXBiOEZScXoxR1hpT0hGK2xBZVE2b3crS0xlcWtCcXQ4CkZxMHhGdG90SlF4VnFWQUFBQUYycHZhVzV1YVhOQVJFVlRTMVJQVUMxUVRWVkdVRFpOQVFJRAotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K" - protected_settings = validate_and_get_protected_settings(rsa_key, '', '', '') + protected_settings = _get_protected_settings(rsa_key, '', '', '') self.assertEqual('sshPrivateKey' in protected_settings, True) self.assertEqual(protected_settings.get('sshPrivateKey'), rsa_key) def test_dsa_private_key(self): key = DSA.generate(2048) private_key_encoded = base64.b64encode(key.export_key()).decode('utf-8') - protected_settings = validate_and_get_protected_settings(private_key_encoded, '', '', '') + protected_settings = _get_protected_settings(private_key_encoded, '', '', '') self.assertEqual('sshPrivateKey' in protected_settings, True) self.assertEqual(protected_settings.get('sshPrivateKey'), private_key_encoded) def test_ecdsa_private_key(self): ecdsa_key = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFhQUFBQUJObFkyUnpZUwoxemFHRXlMVzVwYzNSd01qVTJBQUFBQ0c1cGMzUndNalUyQUFBQVFRUjBRc1BjWmJKeWZPaXE2a1M1d0VaeE5DbmR2YVJHCm1ETEUvVVBjakpDTDZQTVIyZmdPS2NnWlhzTEZkTUFzSnExS2d6TmNDN0ZXNGE0L0wrYTFWWUxDQUFBQXNIZ1RqTFY0RTQKeTFBQUFBRTJWalpITmhMWE5vWVRJdGJtbHpkSEF5TlRZQUFBQUlibWx6ZEhBeU5UWUFBQUJCQkhSQ3c5eGxzbko4NktycQpSTG5BUm5FMEtkMjlwRWFZTXNUOVE5eU1rSXZvOHhIWitBNHB5Qmxld3NWMHdDd21yVXFETTF3THNWYmhyajh2NXJWVmdzCklBQUFBZ0h1U3laU0NUZzJZbVNpOG9aY2c0cnVpODh0T1NUSm1aRVhkR09hdExySHNBQUFBWGFtOXBibTVwYzBCRVJWTkwKVkU5UUxWQk5WVVpRTmswQgotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K" - protected_settings = validate_and_get_protected_settings(ecdsa_key, '', '', '') + protected_settings = _get_protected_settings(ecdsa_key, '', '', '') self.assertEqual('sshPrivateKey' in protected_settings, True) self.assertEqual(protected_settings.get('sshPrivateKey'), ecdsa_key) def test_ed25519_private_key(self): ed25519_key = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNCNjF0RzkrNGFmOTZsWGoyUStjWjJMT2JpV1liMlRtWVR6N3NSV0JDM1hVZ0FBQUtCRzFWRWZSdFZSCkh3QUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQjYxdEc5KzRhZjk2bFhqMlErY1oyTE9iaVdZYjJUbVlUejdzUldCQzNYVWcKQUFBRURRTStLcCtOSWpJVUhSUklqRFE5VDZ0U0V0SG9Ic0w1QjlwbHpCNlZ2MnluclcwYjM3aHAvM3FWZVBaRDV4bllzNQp1SlpodlpPWmhQUHV4RllFTGRkU0FBQUFGMnB2YVc1dWFYTkFSRVZUUzFSUFVDMVFUVlZHVURaTkFRSURCQVVHCi0tLS0tRU5EIE9QRU5TU0ggUFJJVkFURSBLRVktLS0tLQo=" - protected_settings = validate_and_get_protected_settings(ed25519_key, '', '', '') + protected_settings = _get_protected_settings(ed25519_key, '', '', '') self.assertEqual('sshPrivateKey' in protected_settings, True) self.assertEqual(protected_settings.get('sshPrivateKey'), ed25519_key) @@ -52,7 +52,7 @@ def test_long_operator_namespace(self): namespace = OperatorNamespace(operator_namespace) err = 'Error! Invalid --operator-namespace' with self.assertRaises(InvalidArgumentValueError) as cm: - validators.validate_operator_namespace(namespace) + validators._validate_operator_namespace(namespace) self.assertEqual(str(cm.exception), err) def test_long_operator_instance_name(self): @@ -60,7 +60,7 @@ def test_long_operator_instance_name(self): namespace = OperatorInstanceName(operator_instance_name) err = 'Error! Invalid --operator-instance-name' with self.assertRaises(InvalidArgumentValueError) as cm: - validators.validate_operator_instance_name(namespace) + validators._validate_operator_instance_name(namespace) self.assertEqual(str(cm.exception), err) def test_caps_operator_namespace(self): @@ -68,7 +68,7 @@ def test_caps_operator_namespace(self): namespace = OperatorNamespace(operator_namespace) err = 'Error! Invalid --operator-namespace' with self.assertRaises(InvalidArgumentValueError) as cm: - validators.validate_operator_namespace(namespace) + validators._validate_operator_namespace(namespace) self.assertEqual(str(cm.exception), err) def test_caps_operator_instance_name(self): @@ -76,68 +76,68 @@ def test_caps_operator_instance_name(self): namespace = OperatorInstanceName(operator_instance_name) err = 'Error! Invalid --operator-instance-name' with self.assertRaises(InvalidArgumentValueError) as cm: - validators.validate_operator_instance_name(namespace) + validators._validate_operator_instance_name(namespace) self.assertEqual(str(cm.exception), err) def test_long_config_name(self): config_name = "thisisaverylongnamethatistoolongtobeusedthisisaverylongnamethatistoolongtobeused" err = 'Error! Invalid --name' with self.assertRaises(InvalidArgumentValueError) as cm: - validators.validate_configuration_name(config_name) + validators._validate_configuration_name(config_name) self.assertEqual(str(cm.exception), err) def test_valid_config_name(self): config_name = "this-is-a-valid-config" - validators.validate_configuration_name(config_name) + validators._validate_configuration_name(config_name) def test_caps_config_name(self): config_name = "ThisIsaCapsConfigName" err = 'Error! Invalid --name' with self.assertRaises(InvalidArgumentValueError) as cm: - validators.validate_configuration_name(config_name) + validators._validate_configuration_name(config_name) self.assertEqual(str(cm.exception), err) def test_dot_config_name(self): config_name = "a234567890b234567890c234567890d234567890e234567890f234567890.23" err = 'Error! Invalid --name' with self.assertRaises(InvalidArgumentValueError) as cm: - validators.validate_configuration_name(config_name) + validators._validate_configuration_name(config_name) self.assertEqual(str(cm.exception), err) def test_end_hyphen_config_name(self): config_name = "a234567890b234567890c234567890d234567890e234567890f23456789023-" err = 'Error! Invalid --name' with self.assertRaises(InvalidArgumentValueError) as cm: - validators.validate_configuration_name(config_name) + validators._validate_configuration_name(config_name) self.assertEqual(str(cm.exception), err) class TestValidateURLWithParams(unittest.TestCase): def test_ssh_private_key_with_ssh_url(self): - validate_url_with_params('git@github.com:jonathan-innis/helm-operator-get-started-private.git', True, False, False) + validators._validate_url_with_params('git@github.com:jonathan-innis/helm-operator-get-started-private.git', True, False, False) def test_ssh_known_hosts_with_ssh_url(self): - validate_url_with_params('git@github.com:jonathan-innis/helm-operator-get-started-private.git', False, True, False) + validators._validate_url_with_params('git@github.com:jonathan-innis/helm-operator-get-started-private.git', False, True, False) def test_https_auth_with_https_url(self): - validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', False, False, True) + validators._validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', False, False, True) def test_ssh_private_key_with_https_url(self): err = 'Error! An ssh private key cannot be used with an http(s) url' with self.assertRaises(MutuallyExclusiveArgumentError) as cm: - validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', True, False, False) + validators._validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', True, False, False) self.assertEqual(str(cm.exception), err) def test_ssh_known_hosts_with_https_url(self): err = 'Error! ssh known_hosts cannot be used with an http(s) url' with self.assertRaises(MutuallyExclusiveArgumentError) as cm: - validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', False, True, False) + validators._validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', False, True, False) self.assertEqual(str(cm.exception), err) def test_https_auth_with_ssh_url(self): err = 'Error! https auth (--https-user and --https-key) cannot be used with a non-http(s) url' with self.assertRaises(MutuallyExclusiveArgumentError) as cm: - validate_url_with_params('git@github.com:jonathan-innis/helm-operator-get-started-private.git', False, False, True) + validators._validate_url_with_params('git@github.com:jonathan-innis/helm-operator-get-started-private.git', False, False, True) self.assertEqual(str(cm.exception), err) @@ -145,24 +145,24 @@ class TestValidateKnownHosts(unittest.TestCase): def test_valid_known_hosts(self): known_hosts_raw = "ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H" known_hosts_encoded = base64.b64encode(known_hosts_raw.encode('utf-8')).decode('utf-8') - validate_known_hosts(known_hosts_encoded) + validators._validate_known_hosts(known_hosts_encoded) def test_valid_known_hosts_with_comment(self): known_hosts_raw = "ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H ThisIsAValidComment" known_hosts_encoded = base64.b64encode(known_hosts_raw.encode('utf-8')).decode('utf-8') - validate_known_hosts(known_hosts_encoded) + validators._validate_known_hosts(known_hosts_encoded) def test_valid_known_hosts_with_comment_own_line(self): known_hosts_raw = "#this is a comment on its own line\nssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H" known_hosts_encoded = base64.b64encode(known_hosts_raw.encode('utf-8')).decode('utf-8') - validate_known_hosts(known_hosts_encoded) + validators._validate_known_hosts(known_hosts_encoded) def test_invalid_known_hosts(self): known_hosts_raw = "thisisabadknownhostsfilethatisaninvalidformat" known_hosts_encoded = base64.b64encode(known_hosts_raw.encode('utf-8')).decode('utf-8') err = 'Error! ssh known_hosts provided in wrong format' with self.assertRaises(InvalidArgumentValueError) as cm: - validate_known_hosts(known_hosts_encoded) + validators._validate_known_hosts(known_hosts_encoded) self.assertEqual(str(cm.exception), err) From 77ffb3177827c370b20f52dba036300af77e1ba2 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 1 Apr 2021 11:03:01 -0700 Subject: [PATCH 2/9] Fix bug in checking registration --- src/k8s-configuration/azext_k8s_configuration/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k8s-configuration/azext_k8s_configuration/custom.py b/src/k8s-configuration/azext_k8s_configuration/custom.py index a7ac497c21a..75b5d3c6f6d 100644 --- a/src/k8s-configuration/azext_k8s_configuration/custom.py +++ b/src/k8s-configuration/azext_k8s_configuration/custom.py @@ -94,7 +94,7 @@ def create_k8s_configuration(cmd, client, resource_group_name, cluster_name, nam https_auth_set=https_auth_set) # Validate that the subscription is registered to Microsoft.KubernetesConfiguration - _validate_cc_registration(cmd.cli_ctx) + _validate_cc_registration(cmd) # Create sourceControlConfiguration object source_control_configuration = SourceControlConfiguration(repository_url=repository_url, From 44668006a0e80a15354476f780cd688aa783790a Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 1 Apr 2021 11:42:06 -0700 Subject: [PATCH 3/9] Add license header to utils --- src/k8s-configuration/azext_k8s_configuration/_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/k8s-configuration/azext_k8s_configuration/_utils.py b/src/k8s-configuration/azext_k8s_configuration/_utils.py index 8d3aecd86b4..2646fe4ef4d 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_utils.py +++ b/src/k8s-configuration/azext_k8s_configuration/_utils.py @@ -1,3 +1,8 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + import io import base64 from azure.cli.core.azclierror import InvalidArgumentValueError, \ From 4e5bbda59033f0b5d495f0ab2e2a2c70dab36a31 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 2 Apr 2021 12:12:26 -0700 Subject: [PATCH 4/9] Update private key check and error messaging --- .../azext_k8s_configuration/_utils.py | 29 ++-------------- .../azext_k8s_configuration/_validators.py | 34 +++++++++++++++++-- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/k8s-configuration/azext_k8s_configuration/_utils.py b/src/k8s-configuration/azext_k8s_configuration/_utils.py index 2646fe4ef4d..d51d8392b3f 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_utils.py +++ b/src/k8s-configuration/azext_k8s_configuration/_utils.py @@ -3,14 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import io import base64 from azure.cli.core.azclierror import InvalidArgumentValueError, \ RequiredArgumentMissingError, MutuallyExclusiveArgumentError -from paramiko.ed25519key import Ed25519Key -from paramiko.ssh_exception import SSHException -from Crypto.PublicKey import RSA, ECC, DSA +from ._validators import _validate_private_key def _get_protected_settings(ssh_private_key, ssh_private_key_file, https_user, https_key): @@ -19,30 +16,8 @@ def _get_protected_settings(ssh_private_key, ssh_private_key_file, https_user, h # Add gitops private key data to protected settings if exists # Dry-run all key types to determine if the private key is in a valid format - invalid_rsa_key, invalid_ecc_key, invalid_dsa_key, invalid_ed25519_key = (False, False, False, False) if ssh_private_key_data != '': - try: - RSA.import_key(_from_base64(ssh_private_key_data)) - except ValueError: - invalid_rsa_key = True - try: - ECC.import_key(_from_base64(ssh_private_key_data)) - except ValueError: - invalid_ecc_key = True - try: - DSA.import_key(_from_base64(ssh_private_key_data)) - except ValueError: - invalid_dsa_key = True - try: - key_obj = io.StringIO(_from_base64(ssh_private_key_data).decode('utf-8')) - Ed25519Key(file_obj=key_obj) - except SSHException: - invalid_ed25519_key = True - - if invalid_rsa_key and invalid_ecc_key and invalid_dsa_key and invalid_ed25519_key: - raise InvalidArgumentValueError( - 'Error! ssh private key provided in invalid format', - 'Verify the key provided is a valid PEM-formatted key of type RSA, ECC, DSA, or Ed25519') + _validate_private_key(ssh_private_key_data) protected_settings["sshPrivateKey"] = ssh_private_key_data # Check if both httpsUser and httpsKey exist, then add to protected settings diff --git a/src/k8s-configuration/azext_k8s_configuration/_validators.py b/src/k8s-configuration/azext_k8s_configuration/_validators.py index 810f2e0faf1..76a8320acbd 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_validators.py +++ b/src/k8s-configuration/azext_k8s_configuration/_validators.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import re +import io from azure.cli.core.azclierror import InvalidArgumentValueError, MutuallyExclusiveArgumentError from knack.log import get_logger @@ -12,6 +13,9 @@ import azext_k8s_configuration._consts as consts from urllib.parse import urlparse from paramiko.hostkeys import HostKeyEntry +from paramiko.ed25519key import Ed25519Key +from paramiko.ssh_exception import SSHException +from Crypto.PublicKey import RSA, ECC, DSA logger = get_logger(__name__) @@ -62,11 +66,11 @@ def _validate_url_with_params(repository_url, ssh_private_key_set, known_hosts_c if scheme in ('http', 'https'): if ssh_private_key_set: raise MutuallyExclusiveArgumentError( - 'Error! An ssh private key cannot be used with an http(s) url', + 'Error! An --ssh-private-key cannot be used with an http(s) url', 'Verify the url provided is a valid ssh url and not an http(s) url') if known_hosts_contents_set: raise MutuallyExclusiveArgumentError( - 'Error! ssh known_hosts cannot be used with an http(s) url', + 'Error! --ssh-known-hosts cannot be used with an http(s) url', 'Verify the url provided is a valid ssh url and not an http(s) url') if not https_auth_set and scheme == 'https': logger.warning('Warning! https url is being used without https auth params, ensure the repository ' @@ -101,6 +105,32 @@ def _validate_known_hosts(knownhost_data): 'Verify that all lines in the known_hosts contents are provided in a valid sshd(8) format') from ex +def _validate_private_key(ssh_private_key_data): + invalid_rsa_key, invalid_ecc_key, invalid_dsa_key, invalid_ed25519_key = (False, False, False, False) + try: + RSA.import_key(_from_base64(ssh_private_key_data)) + except ValueError: + invalid_rsa_key = True + try: + ECC.import_key(_from_base64(ssh_private_key_data)) + except ValueError: + invalid_ecc_key = True + try: + DSA.import_key(_from_base64(ssh_private_key_data)) + except ValueError: + invalid_dsa_key = True + try: + key_obj = io.StringIO(_from_base64(ssh_private_key_data).decode('utf-8')) + Ed25519Key(file_obj=key_obj) + except SSHException: + invalid_ed25519_key = True + + if invalid_rsa_key and invalid_ecc_key and invalid_dsa_key and invalid_ed25519_key: + raise InvalidArgumentValueError( + 'Error! --ssh-private-key provided in invalid format', + 'Verify the key provided is a valid PEM-formatted key of type RSA, ECC, DSA, or Ed25519') + + # pylint: disable=broad-except def _validate_cc_registration(cmd): try: From c284f7d31a831d598a7dfc0bbe88f31a20aab240 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 2 Apr 2021 12:20:10 -0700 Subject: [PATCH 5/9] Update based on refactoring --- .../azext_k8s_configuration/_utils.py | 31 +--------------- .../azext_k8s_configuration/custom.py | 35 ++++++++++++++++--- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/k8s-configuration/azext_k8s_configuration/_utils.py b/src/k8s-configuration/azext_k8s_configuration/_utils.py index d51d8392b3f..6ad8e600636 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_utils.py +++ b/src/k8s-configuration/azext_k8s_configuration/_utils.py @@ -4,36 +4,7 @@ # -------------------------------------------------------------------------------------------- import base64 -from azure.cli.core.azclierror import InvalidArgumentValueError, \ - RequiredArgumentMissingError, MutuallyExclusiveArgumentError - -from ._validators import _validate_private_key - - -def _get_protected_settings(ssh_private_key, ssh_private_key_file, https_user, https_key): - protected_settings = {} - ssh_private_key_data = _get_data_from_key_or_file(ssh_private_key, ssh_private_key_file) - - # Add gitops private key data to protected settings if exists - # Dry-run all key types to determine if the private key is in a valid format - if ssh_private_key_data != '': - _validate_private_key(ssh_private_key_data) - protected_settings["sshPrivateKey"] = ssh_private_key_data - - # Check if both httpsUser and httpsKey exist, then add to protected settings - if https_user != '' and https_key != '': - protected_settings['httpsUser'] = _to_base64(https_user) - protected_settings['httpsKey'] = _to_base64(https_key) - elif https_user != '': - raise RequiredArgumentMissingError( - 'Error! --https-user used without --https-key', - 'Try providing both --https-user and --https-key together') - elif https_key != '': - raise RequiredArgumentMissingError( - 'Error! --http-key used without --http-user', - 'Try providing both --https-user and --https-key together') - - return protected_settings +from azure.cli.core.azclierror import MutuallyExclusiveArgumentError, InvalidArgumentValueError def _get_cluster_type(cluster_type): diff --git a/src/k8s-configuration/azext_k8s_configuration/custom.py b/src/k8s-configuration/azext_k8s_configuration/custom.py index 75b5d3c6f6d..8882fd16cff 100644 --- a/src/k8s-configuration/azext_k8s_configuration/custom.py +++ b/src/k8s-configuration/azext_k8s_configuration/custom.py @@ -3,12 +3,13 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azure.cli.core.azclierror import ResourceNotFoundError, CommandNotFoundError +from azure.cli.core.azclierror import ResourceNotFoundError, CommandNotFoundError, \ + RequiredArgumentMissingError from knack.log import get_logger -from azext_k8s_configuration._utils import _get_protected_settings, _get_cluster_type, \ - _fix_compliance_state, _get_data_from_key_or_file +from azext_k8s_configuration._utils import _get_cluster_type, \ + _fix_compliance_state, _get_data_from_key_or_file, _to_base64 from azext_k8s_configuration._validators import _validate_known_hosts, _validate_url_with_params, \ - _validate_configuration_name, _validate_cc_registration + _validate_configuration_name, _validate_cc_registration, _validate_private_key from azext_k8s_configuration.vendored_sdks.models import SourceControlConfiguration from azext_k8s_configuration.vendored_sdks.models import HelmOperatorProperties @@ -200,3 +201,29 @@ def delete_k8s_configuration(client, resource_group_name, cluster_name, name, cl source_control_configuration_name = name return client.delete(resource_group_name, cluster_rp, cluster_type, cluster_name, source_control_configuration_name) + + +def _get_protected_settings(ssh_private_key, ssh_private_key_file, https_user, https_key): + protected_settings = {} + ssh_private_key_data = _get_data_from_key_or_file(ssh_private_key, ssh_private_key_file) + + # Add gitops private key data to protected settings if exists + # Dry-run all key types to determine if the private key is in a valid format + if ssh_private_key_data != '': + _validate_private_key(ssh_private_key_data) + protected_settings["sshPrivateKey"] = ssh_private_key_data + + # Check if both httpsUser and httpsKey exist, then add to protected settings + if https_user != '' and https_key != '': + protected_settings['httpsUser'] = _to_base64(https_user) + protected_settings['httpsKey'] = _to_base64(https_key) + elif https_user != '': + raise RequiredArgumentMissingError( + 'Error! --https-user used without --https-key', + 'Try providing both --https-user and --https-key together') + elif https_key != '': + raise RequiredArgumentMissingError( + 'Error! --http-key used without --http-user', + 'Try providing both --https-user and --https-key together') + + return protected_settings From 30b4fc0c1a6d3cb6bd474572484fba816402864f Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 2 Apr 2021 12:49:48 -0700 Subject: [PATCH 6/9] Fix failing tests --- .../tests/latest/test_validators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/k8s-configuration/azext_k8s_configuration/tests/latest/test_validators.py b/src/k8s-configuration/azext_k8s_configuration/tests/latest/test_validators.py index 052b8a30164..29a796e05c5 100644 --- a/src/k8s-configuration/azext_k8s_configuration/tests/latest/test_validators.py +++ b/src/k8s-configuration/azext_k8s_configuration/tests/latest/test_validators.py @@ -6,7 +6,7 @@ import unittest import base64 from azure.cli.core.azclierror import InvalidArgumentValueError, MutuallyExclusiveArgumentError -from azext_k8s_configuration._utils import _get_protected_settings +from azext_k8s_configuration.custom import _get_protected_settings import azext_k8s_configuration._validators as validators from Crypto.PublicKey import RSA, ECC, DSA from paramiko.ed25519key import Ed25519Key @@ -15,7 +15,7 @@ class TestValidateKeyTypes(unittest.TestCase): def test_bad_private_key(self): private_key_encoded = base64.b64encode("this is not a valid private key".encode('utf-8')).decode('utf-8') - err = "Error! ssh private key provided in invalid format" + err = "Error! --ssh-private-key provided in invalid format" with self.assertRaises(InvalidArgumentValueError) as cm: _get_protected_settings(private_key_encoded, '', '', '') self.assertEqual(str(cm.exception), err) @@ -123,13 +123,13 @@ def test_https_auth_with_https_url(self): validators._validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', False, False, True) def test_ssh_private_key_with_https_url(self): - err = 'Error! An ssh private key cannot be used with an http(s) url' + err = 'Error! An --ssh-private-key cannot be used with an http(s) url' with self.assertRaises(MutuallyExclusiveArgumentError) as cm: validators._validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', True, False, False) self.assertEqual(str(cm.exception), err) def test_ssh_known_hosts_with_https_url(self): - err = 'Error! ssh known_hosts cannot be used with an http(s) url' + err = 'Error! --ssh-known-hosts cannot be used with an http(s) url' with self.assertRaises(MutuallyExclusiveArgumentError) as cm: validators._validate_url_with_params('https://github.com/jonathan-innis/helm-operator-get-started-private.git', False, True, False) self.assertEqual(str(cm.exception), err) From f63ed4f194a90a576dc4c88ee03835ef2188671b Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Mon, 3 May 2021 09:48:40 -0700 Subject: [PATCH 7/9] Add provider registration check --- src/k8s-configuration/HISTORY.rst | 4 ++++ src/k8s-configuration/azext_k8s_configuration/_validators.py | 2 +- src/k8s-configuration/setup.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/k8s-configuration/HISTORY.rst b/src/k8s-configuration/HISTORY.rst index 7de17458634..918019a6174 100644 --- a/src/k8s-configuration/HISTORY.rst +++ b/src/k8s-configuration/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +1.0.1 +++++++++++++++++++ +* Add provider registration check + 1.0.0 ++++++++++++++++++ * Support api-version 2021-03-01 diff --git a/src/k8s-configuration/azext_k8s_configuration/_validators.py b/src/k8s-configuration/azext_k8s_configuration/_validators.py index 76a8320acbd..8d922d1f41a 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_validators.py +++ b/src/k8s-configuration/azext_k8s_configuration/_validators.py @@ -140,7 +140,7 @@ def _validate_cc_registration(cmd): if registration_state != "Registered": logger.warning("'Source Control Configuration' cannot be used because '%s' provider has not been " "registered. More details for registering this provider can be found here - " - "https://aka.ms/EnableCustomLocations", consts.PROVIDER_NAMESPACE) + "https://aka.ms/RegisterKubernetesConfigurationProvider", consts.PROVIDER_NAMESPACE) except Exception: logger.warning("Unable to fetch registration state of '%s' provider. " "Failed to enable 'source control configuration' feature...", consts.PROVIDER_NAMESPACE) diff --git a/src/k8s-configuration/setup.py b/src/k8s-configuration/setup.py index d6aa1b2377d..2701f5bee81 100644 --- a/src/k8s-configuration/setup.py +++ b/src/k8s-configuration/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '1.0.0' +VERSION = '1.0.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From 9f2e11e3881293a89368013a94f6b1269b277ee4 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 7 May 2021 15:20:07 -0700 Subject: [PATCH 8/9] Create a test for uppercase url, address comments --- .../azext_k8s_configuration/_consts.py | 1 + .../azext_k8s_configuration/_validators.py | 21 ++++++++----------- .../configurations/Configuration.Tests.ps1 | 5 +++++ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/k8s-configuration/azext_k8s_configuration/_consts.py b/src/k8s-configuration/azext_k8s_configuration/_consts.py index a4b36021903..b459a138457 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_consts.py +++ b/src/k8s-configuration/azext_k8s_configuration/_consts.py @@ -4,3 +4,4 @@ # -------------------------------------------------------------------------------------------- PROVIDER_NAMESPACE = 'Microsoft.KubernetesConfiguration' +REGISTERED = "Registered" \ No newline at end of file diff --git a/src/k8s-configuration/azext_k8s_configuration/_validators.py b/src/k8s-configuration/azext_k8s_configuration/_validators.py index 8d922d1f41a..47c6b97ca3a 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_validators.py +++ b/src/k8s-configuration/azext_k8s_configuration/_validators.py @@ -63,7 +63,7 @@ def _validate_k8s_name(param_value, param_name, max_len): def _validate_url_with_params(repository_url, ssh_private_key_set, known_hosts_contents_set, https_auth_set): scheme = urlparse(repository_url).scheme - if scheme in ('http', 'https'): + if scheme.lower() in ('http', 'https'): if ssh_private_key_set: raise MutuallyExclusiveArgumentError( 'Error! An --ssh-private-key cannot be used with an http(s) url', @@ -106,29 +106,26 @@ def _validate_known_hosts(knownhost_data): def _validate_private_key(ssh_private_key_data): - invalid_rsa_key, invalid_ecc_key, invalid_dsa_key, invalid_ed25519_key = (False, False, False, False) try: RSA.import_key(_from_base64(ssh_private_key_data)) + return except ValueError: - invalid_rsa_key = True try: ECC.import_key(_from_base64(ssh_private_key_data)) + return except ValueError: - invalid_ecc_key = True try: DSA.import_key(_from_base64(ssh_private_key_data)) + return except ValueError: - invalid_dsa_key = True try: key_obj = io.StringIO(_from_base64(ssh_private_key_data).decode('utf-8')) Ed25519Key(file_obj=key_obj) + return except SSHException: - invalid_ed25519_key = True - - if invalid_rsa_key and invalid_ecc_key and invalid_dsa_key and invalid_ed25519_key: - raise InvalidArgumentValueError( - 'Error! --ssh-private-key provided in invalid format', - 'Verify the key provided is a valid PEM-formatted key of type RSA, ECC, DSA, or Ed25519') + raise InvalidArgumentValueError( + 'Error! --ssh-private-key provided in invalid format', + 'Verify the key provided is a valid PEM-formatted key of type RSA, ECC, DSA, or Ed25519') # pylint: disable=broad-except @@ -137,7 +134,7 @@ def _validate_cc_registration(cmd): rp_client = _resource_providers_client(cmd.cli_ctx) registration_state = rp_client.get(consts.PROVIDER_NAMESPACE).registration_state - if registration_state != "Registered": + if registration_state.lower() != consts.REGISTERED.lower(): logger.warning("'Source Control Configuration' cannot be used because '%s' provider has not been " "registered. More details for registering this provider can be found here - " "https://aka.ms/RegisterKubernetesConfigurationProvider", consts.PROVIDER_NAMESPACE) diff --git a/testing/test/configurations/Configuration.Tests.ps1 b/testing/test/configurations/Configuration.Tests.ps1 index a85df42ed2e..23a8170fd44 100644 --- a/testing/test/configurations/Configuration.Tests.ps1 +++ b/testing/test/configurations/Configuration.Tests.ps1 @@ -55,6 +55,11 @@ Describe 'Basic Source Control Configuration Testing' { $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS } + It "Performs a re-PUT of the configuration on the cluster, with HTTPS in caps" { + az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "HTTPS://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator=false --operator-namespace $configurationName + $? | Should -BeTrue + } + It "Lists the configurations on the cluster" { $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters $? | Should -BeTrue From a55fa28d1ab8faa90e321421a327d8e5f4ad9d15 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 7 May 2021 15:56:07 -0700 Subject: [PATCH 9/9] Add blank line to fix style check --- src/k8s-configuration/azext_k8s_configuration/_consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k8s-configuration/azext_k8s_configuration/_consts.py b/src/k8s-configuration/azext_k8s_configuration/_consts.py index b459a138457..e58470e7e78 100644 --- a/src/k8s-configuration/azext_k8s_configuration/_consts.py +++ b/src/k8s-configuration/azext_k8s_configuration/_consts.py @@ -4,4 +4,4 @@ # -------------------------------------------------------------------------------------------- PROVIDER_NAMESPACE = 'Microsoft.KubernetesConfiguration' -REGISTERED = "Registered" \ No newline at end of file +REGISTERED = "Registered"