diff --git a/resotocore/resotocore/cli/command.py b/resotocore/resotocore/cli/command.py index 20e53b0ef3..da0e631c06 100644 --- a/resotocore/resotocore/cli/command.py +++ b/resotocore/resotocore/cli/command.py @@ -2535,6 +2535,12 @@ class ListCommand(CLICommand, OutputTransformer): (["change"], "change"), (["changed_at"], "changed_at"), ] + default_properties_to_ignore = { + "ancestors.cloud.reported.id", + "ancestors.account.reported.id", + "ancestors.region.reported.id", + "ancestors.zone.reported.id", + } all_default_props = { ".".join(path) for path, _ in default_properties_to_show @@ -2585,7 +2591,9 @@ def default_props_to_show() -> List[Tuple[List[str], str]]: # add all predicates the user has queried if ctx.query: # add all predicates the user has queried - predicate_names = (p.name for p in ctx.query.visible_predicates) + predicate_names = ( + p.name for p in ctx.query.visible_predicates if p.name not in self.default_properties_to_ignore + ) # add sort keys of the last part the user has defined sort_names = (s.name for s in ctx.query.current_part.sort) for name in chain(predicate_names, sort_names): @@ -2623,7 +2631,39 @@ def parse_props_to_show(props_arg: str) -> List[Tuple[List[str], str]]: props.append((path, as_name)) return props + def create_unique_names(all_props: List[Tuple[List[str], str]]) -> List[Tuple[List[str], str]]: + result = [] + names: Set[str] = set() + + def unique_name(path: List[str], current: str) -> str: + # compute the name by parameter path + current = "_".join(path) if path else current + if current not in names: + return current + # compute the name by parameter path and current name + count = 0 + attempt = current + while attempt in names: + count += 1 + attempt = f"{current}_{count}" + return attempt + + for path, name in all_props: + if name in names: + if len(path) <= 1: + name = unique_name(path, name) + elif path[0] in ("ancestors", "descendants"): + name = unique_name([path[1]] + path[3:], name) + elif path[0] in Section.all: + name = unique_name(path[1:], name) + else: + name = unique_name(path, name) + names.add(name) + result.append((path, name)) + return result + props_to_show = parse_props_to_show(properties) if properties is not None else default_props_to_show() + props_to_show = create_unique_names(props_to_show) def fmt_json(elem: Json) -> JsonElement: if is_node(elem): diff --git a/resotocore/tests/resotocore/cli/command_test.py b/resotocore/tests/resotocore/cli/command_test.py index 11ea085804..70dab88810 100644 --- a/resotocore/tests/resotocore/cli/command_test.py +++ b/resotocore/tests/resotocore/cli/command_test.py @@ -583,14 +583,12 @@ async def test_list_command(cli: CLI) -> None: assert result[0] == ["some_string=hello, some_int=0, node_id=sub_root"] # List supports csv output - props = dict(id="test", a="a", b=True, c=False, d=None, e=12, f=1.234, reported={}) result = await cli.execute_cli_command( f"json {json.dumps(props)} | list --csv a,b,c,d,e,f,non_existent", stream.list ) assert result[0] == ['"a","b","c","d","e","f","non_existent"', '"a",True,False,"",12,1.234,""'] # List supports markdown output - props = dict(id="test", a="a", b=True, c=False, d=None, e=12, f=1.234, reported={}) result = await cli.execute_cli_command( f"json {json.dumps(props)} | list --markdown a,b,c,d,e,f,non_existent", stream.list ) @@ -615,10 +613,19 @@ async def test_list_command(cli: CLI) -> None: ] # List supports only markdown or csv, but not both at the same time - props = dict(id="test", a="a", b=True, c=False, d=None, e=12, f=1.234, reported={}) with pytest.raises(CLIParseError): await cli.execute_cli_command(f"json {json.dumps(props)}" " | list --csv --markdown", stream.list) + # List command will make sure to make the column name unique + props = dict(id="123", reported=props, ancestors={"account": {"reported": props}}) + result = await cli.execute_cli_command( + f"json {json.dumps(props)} | list reported.a, reported.b as a, reported.c as a, reported.c, " + f"ancestors.account.reported.a, ancestors.account.reported.a, ancestors.account.reported.a as foo", + stream.list, + ) + # b as a ==> b, c as a ==> c, c ==> c_1, ancestors.account.reported.a ==> account_a, again ==> _1 + assert result[0][0] == "a=a, b=true, c=false, c_1=false, account_a=a, account_a_1=a, foo=a" + @pytest.mark.asyncio async def test_jq_command(cli: CLI) -> None: