-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[Config] az config: Add new config command module
#14436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d0cb029
cd0dc0f
920c262
919a891
cca1ba0
9793e22
ba13f99
c686e44
9749ec0
803f2b6
8b7c4c0
d621f94
3b5eb40
ed41fa7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| from azure.cli.core import AzCommandsLoader | ||
|
|
||
| import azure.cli.command_modules.config._help # pylint: disable=unused-import | ||
|
|
||
|
|
||
| class ConfigCommandsLoader(AzCommandsLoader): | ||
|
|
||
| def __init__(self, cli_ctx=None): | ||
| super(ConfigCommandsLoader, self).__init__(cli_ctx=cli_ctx) | ||
|
|
||
| def load_command_table(self, args): | ||
| from azure.cli.command_modules.config.commands import load_command_table | ||
| load_command_table(self, args) | ||
| return self.command_table | ||
|
|
||
| def load_arguments(self, command): | ||
| from azure.cli.command_modules.config._params import load_arguments | ||
| load_arguments(self, command) | ||
|
|
||
|
|
||
| COMMAND_LOADER_CLS = ConfigCommandsLoader |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| from knack.help_files import helps # pylint: disable=unused-import | ||
| # pylint: disable=line-too-long, too-many-lines | ||
|
|
||
| helps['config'] = """ | ||
| type: group | ||
| short-summary: Manage Azure CLI configuration. | ||
| """ | ||
|
|
||
| helps['config set'] = """ | ||
| type: command | ||
| short-summary: Set a configuration. | ||
| long-summary: | | ||
| For available configuration options, see https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration. | ||
| By default without specifying --local, the configuration will be saved to `~/.azure/config`. | ||
| examples: | ||
| - name: Disable color with `core.no_color`. | ||
| text: az config set core.no_color=true | ||
| - name: Hide warnings and only show errors with `core.only_show_errors`. | ||
| text: az config set core.only_show_errors=true | ||
| - name: Turn on client-side telemetry. | ||
| text: az config set core.collect_telemetry=true | ||
| - name: Turn on file logging and set its location. | ||
| text: |- | ||
| az config set logging.enable_log_file=true | ||
| az config set logging.log_dir=~/az-logs | ||
| - name: Set the default location to `westus2` and default resource group to `myRG`. | ||
| text: az config set defaults.location=westus2 defaults.group=MyResourceGroup | ||
| - name: Set the default resource group to `myRG` on a local scope. | ||
| text: az config set defaults.group=myRG --local | ||
| """ | ||
|
|
||
| helps['config get'] = """ | ||
| type: command | ||
| short-summary: Get a configuration. | ||
| examples: | ||
| - name: Get all configurations. | ||
| text: az config get | ||
| - name: Get configurations in `core` section. | ||
| text: az config get core | ||
| - name: Get the configuration of key `core.no_color`. | ||
| text: az config get core.no_color | ||
| """ | ||
|
|
||
| helps['config unset'] = """ | ||
| type: command | ||
| short-summary: Unset a configuration. | ||
| examples: | ||
| - name: Unset the configuration of key `core.no_color`. | ||
| text: az config unset core.no_color | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| # pylint: disable=line-too-long | ||
|
|
||
|
|
||
| def load_arguments(self, _): | ||
| with self.argument_context('config') as c: | ||
| c.ignore('_subscription') # ignore the global subscription param | ||
|
|
||
| with self.argument_context('config set') as c: | ||
| c.positional('key_value', nargs='+', help="Space-separated configurations in the form of <section>.<key>=<value>.") | ||
| c.argument('local', action='store_true', help='Set as a local configuration in the working directory.') | ||
|
|
||
| with self.argument_context('config get') as c: | ||
| c.positional('key', nargs='?', help='The configuration to get. ' | ||
| 'If not provided, all sections and configurations will be listed. ' | ||
| 'If `section` is provided, all configurations under the specified section will be listed. ' | ||
| 'If `<section>.<key>` is provided, only the corresponding configuration is shown.') | ||
| c.argument('local', action='store_true', | ||
| help='Include local configuration. Scan from the working directory up to the root drive, then the global configuration ' | ||
| 'and return the first occurrence.') | ||
|
|
||
| with self.argument_context('config unset') as c: | ||
| c.positional('key', nargs='+', help='The configuration to unset, in the form of <section>.<key>.') | ||
| c.argument('local', action='store_true', | ||
| help='Include local configuration. Scan from the working directory up to the root drive, then the global configuration ' | ||
| 'and unset the first occurrence.') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| from azure.cli.core.commands import CliCommandType | ||
|
|
||
|
|
||
| def load_command_table(self, _): | ||
|
|
||
| config_custom = CliCommandType(operations_tmpl='azure.cli.command_modules.config.custom#{}') | ||
|
|
||
| with self.command_group('config', config_custom, is_experimental=True) as g: | ||
| g.command('set', 'config_set') | ||
| g.command('get', 'config_get') | ||
| g.command('unset', 'config_unset') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| from knack.log import get_logger | ||
| from knack.util import CLIError | ||
|
|
||
| from azure.cli.core.util import ScopedConfig | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
| def _normalize_config_value(value): | ||
| if value: | ||
| value = '' if value in ["''", '""'] else value | ||
| return value | ||
|
|
||
|
|
||
| def config_set(cmd, key_value=None, local=False): | ||
| if key_value: | ||
| with ScopedConfig(cmd.cli_ctx.config, local): | ||
| for kv in key_value: | ||
| # core.no_color=true | ||
| parts = kv.split('=', 1) | ||
| if len(parts) == 1: | ||
| raise CLIError('usage error: [section].[name]=[value] ...') | ||
| key = parts[0] | ||
| value = parts[1] | ||
|
|
||
| # core.no_color | ||
| parts = key.split('.', 1) | ||
| if len(parts) == 1: | ||
| raise CLIError('usage error: [section].[name]=[value] ...') | ||
| section = parts[0] | ||
| name = parts[1] | ||
|
|
||
| cmd.cli_ctx.config.set_value(section, name, _normalize_config_value(value)) | ||
|
|
||
|
|
||
| def config_get(cmd, key=None, local=False): | ||
| # No arg. List all sections and all items | ||
| if not key: | ||
| with ScopedConfig(cmd.cli_ctx.config, local): | ||
| sections = cmd.cli_ctx.config.sections() | ||
| result = {} | ||
| for section in sections: | ||
| items = cmd.cli_ctx.config.items(section) | ||
| result[section] = items | ||
| return result | ||
|
|
||
| parts = key.split('.', 1) | ||
| if len(parts) == 1: | ||
| # Only section is provided | ||
| section = key | ||
| name = None | ||
| else: | ||
| # section.name | ||
| section = parts[0] | ||
| name = parts[1] | ||
|
|
||
| with ScopedConfig(cmd.cli_ctx.config, local): | ||
| items = cmd.cli_ctx.config.items(section) | ||
|
|
||
| if not name: | ||
| # Only section | ||
| return items | ||
|
|
||
| # section.option | ||
| try: | ||
| return next(x for x in items if x['name'] == name) | ||
| except StopIteration: | ||
| raise CLIError("Configuration '{}' is not set.".format(key)) | ||
|
|
||
|
|
||
| def config_unset(cmd, key=None, local=False): | ||
| for k in key: | ||
| # section.name | ||
| parts = k.split('.', 1) | ||
|
|
||
| if len(parts) == 1: | ||
| raise CLIError("usage error: [section].[name]") | ||
|
|
||
| section = parts[0] | ||
| name = parts[1] | ||
|
|
||
| with ScopedConfig(cmd.cli_ctx.config, local): | ||
| cmd.cli_ctx.config.remove_option(section, name) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What will be returned if
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will ignore the operation and return
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, try:
existed = self.config_parser.remove_option(section, option)
self.set(self.config_parser)
except configparser.NoSectionError:
passWe'd better confirm with PM about the behavior later.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree with your suggestion. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
| import os | ||
| import tempfile | ||
| import unittest | ||
| from unittest.mock import MagicMock | ||
|
|
||
| from azure.cli.testsdk import ScenarioTest, LocalContextScenarioTest | ||
| from knack.util import CLIError | ||
|
|
||
|
|
||
| class ConfigTest(ScenarioTest): | ||
|
|
||
| def test_config(self): | ||
|
|
||
| # [test_section1] | ||
| # test_option1 = test_value1 | ||
| # | ||
| # [test_section2] | ||
| # test_option21 = test_value21 | ||
| # test_option22 = test_value22 | ||
|
|
||
| # C:\Users\{username}\AppData\Local\Temp | ||
| tempdir = tempfile.gettempdir() | ||
| original_path = os.getcwd() | ||
| os.chdir(tempdir) | ||
| print("Using temp dir: {}".format(tempdir)) | ||
|
|
||
| global_test_args = {"source": os.path.expanduser(os.path.join('~', '.azure', 'config')), "flag": ""} | ||
| local_test_args = {"source": os.path.join(tempdir, '.azure', 'config'), "flag": " --local"} | ||
|
|
||
| for args in (global_test_args, local_test_args): | ||
| test_option1_expected = {'name': 'test_option1', 'source': args["source"], 'value': 'test_value1'} | ||
| test_option21_expected = {'name': 'test_option21', 'source': args["source"], 'value': 'test_value21'} | ||
| test_option22_expected = {'name': 'test_option22', 'source': args["source"], 'value': 'test_value22'} | ||
|
|
||
| test_section1_expected = [test_option1_expected] | ||
| test_section2_expected = [test_option21_expected, test_option22_expected] | ||
|
|
||
| # 1. set | ||
| # Test setting one option | ||
| self.cmd('config set test_section1.test_option1=test_value1' + args['flag']) | ||
| # Test setting multiple options | ||
| self.cmd('config set test_section2.test_option21=test_value21 test_section2.test_option22=test_value22' + args['flag']) | ||
|
|
||
| # 2. get | ||
| # 2.1 Test get all sections | ||
| output = self.cmd('config get' + args['flag']).get_output_in_json() | ||
| self.assertListEqual(output['test_section1'], test_section1_expected) | ||
| self.assertListEqual(output['test_section2'], test_section2_expected) | ||
|
|
||
| # 2.2 Test get one section | ||
| output = self.cmd('config get test_section1' + args['flag']).get_output_in_json() | ||
| self.assertListEqual(output, test_section1_expected) | ||
| output = self.cmd('config get test_section2' + args['flag']).get_output_in_json() | ||
| self.assertListEqual(output, test_section2_expected) | ||
|
|
||
| # 2.3 Test get one item | ||
| output = self.cmd('config get test_section1.test_option1' + args['flag']).get_output_in_json() | ||
| self.assertDictEqual(output, test_option1_expected) | ||
| output = self.cmd('config get test_section2.test_option21' + args['flag']).get_output_in_json() | ||
| self.assertDictEqual(output, test_option21_expected) | ||
| output = self.cmd('config get test_section2.test_option22' + args['flag']).get_output_in_json() | ||
| self.assertDictEqual(output, test_option22_expected) | ||
|
|
||
| with self.assertRaises(CLIError): | ||
| self.cmd('config get test_section1.test_option22' + args['flag']) | ||
|
|
||
| # 3. unset | ||
| # Test unsetting one option | ||
| self.cmd('config unset test_section1.test_option1' + args['flag']) | ||
| # Test unsetting multiple options | ||
| self.cmd('config unset test_section2.test_option21 test_section2.test_option22' + args['flag']) | ||
|
|
||
| os.chdir(original_path) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option for this case is to return empty string. Personally, I prefer empty string for this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty string means the config is actually set to
'', which is different from the concept of "not set".There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense to me. Maybe we should also confirm this with PM.