diff --git a/awxkit/awxkit/cli/client.py b/awxkit/awxkit/cli/client.py index ab033474cb..ff1328fbd3 100755 --- a/awxkit/awxkit/cli/client.py +++ b/awxkit/awxkit/cli/client.py @@ -251,7 +251,13 @@ class CLI(object): if self.resource != 'settings': for method in ('list', 'modify', 'create'): if method in parser.parser.choices: - parser.build_query_arguments(method, 'GET' if method == 'list' else 'POST') + if method == 'list': + http_method = 'GET' + elif method == 'modify' and 'PUT' in parser.options: + http_method = 'PUT' + else: + http_method = 'POST' + parser.build_query_arguments(method, http_method) if from_sphinx: parsed, extra = self.parser.parse_known_args(self.argv) else: diff --git a/awxkit/awxkit/cli/options.py b/awxkit/awxkit/cli/options.py index aa55cc9df1..a1b3ed45a8 100644 --- a/awxkit/awxkit/cli/options.py +++ b/awxkit/awxkit/cli/options.py @@ -102,6 +102,18 @@ class ResourceOptionsParser(object): if '299' in warning and 'deprecated' in warning: self.deprecated = True self.allowed_options = options.headers.get('Allow', '').split(', ') + # If the user can PUT on the detail endpoint but doesn't have + # POST on the list endpoint, use the detail endpoint's + # action schema so that 'modify' fields are populated. + if 'POST' not in self.options and 'PUT' in self.allowed_options: + try: + detail_actions = options.json().get('actions', {}) + except Exception: + detail_actions = {} + if 'PUT' in detail_actions: + self.options['PUT'] = detail_actions['PUT'] + elif 'GET' in detail_actions: + self.options['PUT'] = detail_actions['GET'] def build_list_actions(self): action_map = { @@ -109,6 +121,10 @@ class ResourceOptionsParser(object): 'POST': 'create', } for method, action in self.options.items(): + # Skip 'PUT', which may be added by get_allowed_options + # and is handled separately by build_detail_actions + if method not in action_map: + continue method = action_map[method] parser = self.parser.add_parser(method, help='') if method == 'list': diff --git a/awxkit/test/cli/test_options.py b/awxkit/test/cli/test_options.py index 2830674712..01fc2a4169 100644 --- a/awxkit/test/cli/test_options.py +++ b/awxkit/test/cli/test_options.py @@ -11,6 +11,24 @@ class ResourceOptionsParser(ResourceOptionsParser): self.allowed_options = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] +class NoPostResourceOptionsParser(ResourceOptionsParser): + """Simulates a user with object-level PUT but no list-level POST.""" + + detail_put_actions = {} + + def get_allowed_options(self): + self.allowed_options = ['GET', 'PUT', 'PATCH', 'DELETE'] + # Simulate the logic from the real get_allowed_options that + # falls back to the detail endpoint's PUT schema when POST + # is not available on the list endpoint. + if 'POST' not in self.options and 'PUT' in self.allowed_options: + if self.detail_put_actions: + self.options['PUT'] = self.detail_put_actions + + def handle_custom_actions(self): + pass + + class OptionsPage(Page): def options(self): return self @@ -185,6 +203,30 @@ class TestOptions(unittest.TestCase): self.parser.choices[method].print_help(out) assert 'positional arguments:\n id' in out.getvalue() + def test_modify_without_list_post(self): + """User with object-level PUT but no list-level POST can still modify.""" + page = OptionsPage.from_json( + { + 'actions': { + 'GET': {}, + } + } + ) + NoPostResourceOptionsParser.detail_put_actions = { + 'scm_branch': {'type': 'string', 'help_text': 'SCM branch'}, + 'description': {'type': 'string', 'help_text': 'Description'}, + } + options = NoPostResourceOptionsParser(None, page, 'projects', self.parser) + + assert 'modify' in self.parser.choices + assert 'create' not in self.parser.choices + + options.build_query_arguments('modify', 'PUT') + out = StringIO() + self.parser.choices['modify'].print_help(out) + assert '--scm_branch TEXT' in out.getvalue() + assert '--description TEXT' in out.getvalue() + class TestSettingsOptions(unittest.TestCase): def setUp(self):