diff --git a/python/az/aro/azext_aro/tests/latest/custom_preparers.py b/python/az/aro/azext_aro/tests/latest/custom_preparers.py new file mode 100644 index 00000000000..3a7281b08c5 --- /dev/null +++ b/python/az/aro/azext_aro/tests/latest/custom_preparers.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the Apache License 2.0. + +from azure.cli.testsdk.preparers import RoleBasedServicePrincipalPreparer +from azure.cli.testsdk.scenario_tests.utilities import is_text_payload +from azure.cli.testsdk.utilities import GraphClientPasswordReplacer +from azure.mgmt.core.tools import resource_id + +MOCK_GUID = '00000000-0000-0000-0000-000000000001' +MOCK_SECRET = 'fake-secret' + + +class AROClusterServicePrincipalPreparer(RoleBasedServicePrincipalPreparer): + def __init__( + self, + name_prefix="clitest", + skip_assignment=True, + parameter_name="client_id", + parameter_password="client_secret", + dev_setting_sp_name="AZURE_CLI_TEST_DEV_SP_NAME", + dev_setting_sp_password="AZURE_CLI_TEST_DEV_SP_PASSWORD", + key="aro_csp", + ): + super(AROClusterServicePrincipalPreparer, self).__init__( + name_prefix, + skip_assignment, + parameter_name, + parameter_password, + dev_setting_sp_name, + dev_setting_sp_password, + key, + ) + self.client_id_to_replace = None + self.client_secret_to_replace = None + + def create_resource(self, name, **kwargs): + client_id, client_secret = self._get_csp_credentials(name) + + self.test_class_instance.kwargs[self.key] = client_id + self.test_class_instance.kwargs["{}_pass".format(self.key)] = client_secret + + return { + self.parameter_name: client_id, + self.parameter_password: client_secret, + } + + # Overriden because RoleBasedServicePrincipal.remove_resource does not delete + # the underlying AAD application generated when creating the service principal + def remove_resource(self, name, **kwargs): + super().remove_resource(name, **kwargs) + + if not self.dev_setting_sp_name: + self.live_only_execute(self.cli_ctx, 'az ad app delete --id {}'.format(self.result.get('appId'))) + + def process_request(self, request): + if self.client_id_to_replace in request.uri: + request.uri = request.uri.replace(self.client_id_to_replace, MOCK_GUID) + + if is_text_payload(request) and isinstance(request.body, bytes): + request.body = self._replace_byte_keys(request.body) + elif is_text_payload(request) and isinstance(request.body, str): + request.body = self._replace_string_keys(request.body) + + return request + + def process_response(self, response): + if is_text_payload(response) and response['body']['string']: + response['body']['string'] = self._replace_string_keys(response['body']['string']) + + return response + + def _get_csp_credentials(self, name): + if not self.live_test and not self.test_class_instance.in_recording: + return MOCK_GUID, MOCK_SECRET + + client_id, client_secret = self._generate_csp(name) + + # call AbstractPreparer.moniker to make resource counts and self.resource_moniker consistent between live + # and play-back. see SingleValueReplacer.process_request, AbstractPreparer.__call__._preparer_wrapper + # and ScenarioTest.create_random_name. This is so that when self.create_random_name is called for the + # first time during live or playback, it would have the same value. + # In short, the default sp preparer in live mode does not call moniker, which leads to inconsistent counts. + _ = self.moniker + + self.client_id_to_replace = client_id + self.client_secret_to_replace = client_secret + + return client_id, client_secret + + def _generate_csp(self, name): + if self.dev_setting_sp_name: + client_id = self.dev_setting_sp_name + client_secret = self.dev_setting_sp_password + + return client_id, client_secret + + subscription = self.test_class_instance.get_subscription_id() + resource_group = self.test_class_instance.kwargs.get('rg') + command = 'az ad sp create-for-rbac -n {} --role contributor --scopes "{}"'\ + .format(name, resource_id(subscription=subscription, resource_group=resource_group)) + + try: + self.result = self.live_only_execute(self.cli_ctx, command).get_output_in_json() + except AttributeError: + pass + + client_id = self.result['appId'] + client_secret = self.result.get('password') or GraphClientPasswordReplacer.PWD_REPLACEMENT + + return client_id, client_secret + + def _replace_string_keys(self, val): + if self.client_id_to_replace is None: + return val + + return val.replace(self.client_id_to_replace, MOCK_GUID).replace(self.client_secret_to_replace, MOCK_SECRET) + + def _replace_byte_keys(self, val): + return self._replace_string_keys(val.decode('utf-8')).encode('utf-8') diff --git a/python/az/aro/azext_aro/tests/latest/integration/test_aro_scenario.py b/python/az/aro/azext_aro/tests/latest/integration/test_aro_scenario.py index 91c93b12a91..adb164c7211 100644 --- a/python/az/aro/azext_aro/tests/latest/integration/test_aro_scenario.py +++ b/python/az/aro/azext_aro/tests/latest/integration/test_aro_scenario.py @@ -2,42 +2,93 @@ # Licensed under the Apache License 2.0. import os +from random import randint from unittest import mock -from azure_devtools.scenario_tests import AllowLargeResponse -from azure.cli.testsdk import ResourceGroupPreparer -from azure.cli.testsdk import ScenarioTest +from knack.log import get_logger +from azext_aro.tests.latest.custom_preparers import AROClusterServicePrincipalPreparer +from azure.cli.testsdk import ScenarioTest, ResourceGroupPreparer +from azure.cli.testsdk.checkers import StringContainCheck +from azure.cli.testsdk.scenario_tests import AllowLargeResponse -TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) +logger = get_logger(__name__) -class AroScenarioTest(ScenarioTest): - @ResourceGroupPreparer(name_prefix='cli_test_aro') - def test_aro(self, resource_group): +class AroScenarioTests(ScenarioTest): + @AllowLargeResponse() + @ResourceGroupPreparer(random_name_length=28, name_prefix='cli_test_aro', location='eastus') + @AROClusterServicePrincipalPreparer(name_prefix='cli_test_aro') + def test_aro_public_cluster(self, resource_group): + from azure.mgmt.core.tools import resource_id + + subscription = self.get_subscription_id() + + master_subnet = self.create_random_name('dev_master', 14) + worker_subnet = self.create_random_name('dev_worker', 14) + name = self.create_random_name('aro', 14) + + temp_kubeconfig_path = self.create_random_name('kubeconfig', 24) + '.tmp' + self.kwargs.update({ - 'name': 'test1' + 'name': name, + 'resource_group': resource_group, + 'subscription': subscription, + 'master_subnet': master_subnet, + 'worker_subnet': worker_subnet, + 'master_ip_range': '10.{}.{}.0/24'.format(randint(0, 127), randint(0, 255)), + 'worker_ip_range': '10.{}.{}.0/24'.format(randint(0, 127), randint(0, 255)), + 'master_subnet_resource': resource_id(subscription=subscription, resource_group=resource_group, namespace='Microsoft.Network', type='virtualNetworks', child_type_1='subnets', name='dev-vnet', child_name_1=master_subnet), + 'worker_subnet_resource': resource_id(subscription=subscription, resource_group=resource_group, namespace='Microsoft.Network', type='virtualNetworks', child_type_1='subnets', name='dev-vnet', child_name_1=worker_subnet), + 'temp_kubeconfig_path': temp_kubeconfig_path, }) - # test aro create + self.cmd('network vnet create -g {rg} -n dev-vnet --address-prefixes 10.0.0.0/9') + self.cmd('network vnet subnet create -g {rg} --vnet-name dev-vnet -n {master_subnet} --address-prefixes {master_ip_range} --service-endpoints Microsoft.ContainerRegistry --default-outbound false') + self.cmd('network vnet subnet create -g {rg} --vnet-name dev-vnet -n {worker_subnet} --address-prefixes {worker_ip_range} --service-endpoints Microsoft.ContainerRegistry --default-outbound false') + self.cmd('network vnet subnet update -g {rg} --vnet-name dev-vnet -n {master_subnet} --private-link-service-network-policies Disabled') + + # aro validate with mock.patch('azure.cli.command_modules.aro._rbac._gen_uuid', side_effect=self.create_guid): - self.cmd('aro create -g {rg} -n {name} --tags foo=doo', checks=[ - self.check('tags.foo', 'doo'), - self.check('name', '{name}') + self.cmd('aro validate -g {rg} -n {name} --client-id {aro_csp} --client-secret {aro_csp_pass} --master-subnet {master_subnet_resource} --worker-subnet {worker_subnet_resource} --subscription {subscription}') + + # aro create + with mock.patch('azure.cli.command_modules.aro._rbac._gen_uuid', side_effect=self.create_guid): + self.cmd('aro create -g {rg} -n {name} --client-id {aro_csp} --client-secret {aro_csp_pass} --master-subnet {master_subnet_resource} --worker-subnet {worker_subnet_resource} --subscription {subscription} --tags test=create', checks=[ + self.check('tags.test', 'create'), + self.check('name', '{name}'), + self.check('masterProfile.subnetId', '{master_subnet_resource}'), + self.check('workerProfiles[0].subnetId', '{worker_subnet_resource}'), + self.check('provisioningState', 'Succeeded') ]) - self.cmd('aro update -g {rg} -n {name} --tags foo=boo', checks=[ - self.check('tags.foo', 'boo') + # aro list-credentials + self.cmd('aro list-credentials -g {rg} -n {name} --subscription {subscription}', checks=[self.check('kubeadminUsername', 'kubeadmin')]) + + # aro get-admin-kubeconfig + try: + self.cmd('aro get-admin-kubeconfig -g {rg} -n {name} --subscription {subscription} -f {temp_kubeconfig_path}') + self.assertGreater(os.path.getsize(temp_kubeconfig_path), 0) + finally: + os.remove(temp_kubeconfig_path) + + # aro show + self.cmd('aro show -g {rg} -n {name} --subscription {subscription} --output table', checks=[ + StringContainCheck(name), + StringContainCheck(resource_group), + StringContainCheck('eastus'), + StringContainCheck('Succeeded'), ]) - count = len(self.cmd('aro list').get_output_in_json()) - self.cmd('aro show -g {rg} -n {name}', checks=[ - self.check('name', '{name}'), - self.check('resourceGroup', '{rg}'), - self.check('tags.foo', 'boo') + # aro list + self.cmd('aro list -g {rg} --subscription {subscription}', checks=[ + self.check('[0].name', '{name}'), + self.check('[0].provisioningState', 'Succeeded'), + self.check_pattern('[0].id', '.*{name}') ]) - self.cmd('aro delete -g {rg} -n {name}') + # aro update + self.cmd('aro update -g {rg} -n {name} --subscription {subscription}', expect_failure=False) - final_count = len(self.cmd('aro list').get_output_in_json()) - self.assertTrue(final_count, count - 1) + # aro delete + self.cmd('aro delete -y -g {rg} -n {name} --subscription {subscription}', expect_failure=False)