Skip to content

Commit ac2ef3e

Browse files
new: Support specifying explicitly empty lists (#530)
1 parent 2132cfb commit ac2ef3e

File tree

6 files changed

+168
-10
lines changed

6 files changed

+168
-10
lines changed

linodecli/arg_helpers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,10 @@ def action_help(cli, command, action):
380380
if op.method in {"post", "put"} and arg.required
381381
else ""
382382
)
383-
print(f" --{arg.path}: {is_required}{arg.description}")
383+
nullable_fmt = " (nullable)" if arg.nullable else ""
384+
print(
385+
f" --{arg.path}: {is_required}{arg.description}{nullable_fmt}"
386+
)
384387

385388

386389
def bake_command(cli, spec_loc):

linodecli/baked/operation.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ def type_func(value):
7676
return type_func
7777

7878

79+
class ArrayAction(argparse.Action):
80+
"""
81+
This action is intended to be used only with array arguments.
82+
This purpose of this action is to allow users to specify explicitly
83+
empty lists using a singular "[]" argument value.
84+
"""
85+
86+
def __call__(self, parser, namespace, values, option_string=None):
87+
if getattr(namespace, self.dest) is None:
88+
setattr(namespace, self.dest, [])
89+
90+
output_list = getattr(namespace, self.dest)
91+
92+
# If the output list is empty and the user specifies a []
93+
# argument, keep the list empty
94+
if values == "[]" and len(output_list) < 1:
95+
return
96+
97+
output_list.append(values)
98+
99+
79100
class ListArgumentAction(argparse.Action):
80101
"""
81102
This action is intended to be used only with list arguments.
@@ -380,7 +401,7 @@ def _add_args_post_put(self, parser) -> List[Tuple[str, str]]:
380401
parser.add_argument(
381402
"--" + arg.path,
382403
metavar=arg.name,
383-
action="append",
404+
action=ArrayAction,
384405
type=arg_type_handler,
385406
)
386407
elif arg.list_item:

tests/integration/linodes/helpers_linodes.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ def wait_until(linode_id: "str", timeout, status: "str", period=5):
7171
return False
7272

7373

74-
def create_linode():
75-
region = "us-east"
76-
74+
def create_linode(test_region=DEFAULT_REGION):
7775
# create linode
7876
linode_id = (
7977
exec_test_command(
@@ -84,7 +82,7 @@ def create_linode():
8482
"--type",
8583
DEFAULT_LINODE_TYPE,
8684
"--region",
87-
region,
85+
test_region,
8886
"--image",
8987
DEFAULT_TEST_IMAGE,
9088
"--root_pass",
@@ -142,7 +140,10 @@ def remove_linodes():
142140

143141

144142
def create_linode_and_wait(
145-
test_plan=DEFAULT_LINODE_TYPE, test_image=DEFAULT_TEST_IMAGE, ssh_key=""
143+
test_plan=DEFAULT_LINODE_TYPE,
144+
test_image=DEFAULT_TEST_IMAGE,
145+
ssh_key="",
146+
test_region=DEFAULT_REGION,
146147
):
147148
linode_type = test_plan
148149

@@ -160,7 +161,7 @@ def create_linode_and_wait(
160161
"--type",
161162
linode_type,
162163
"--region",
163-
"us-east",
164+
test_region,
164165
"--image",
165166
test_image,
166167
"--root_pass",

tests/integration/networking/test_networking.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import json
12
import re
23

34
import pytest
45

56
from tests.integration.helpers import delete_target_id, exec_test_command
6-
from tests.integration.linodes.helpers_linodes import create_linode_and_wait
7+
from tests.integration.linodes.helpers_linodes import (
8+
create_linode,
9+
create_linode_and_wait,
10+
)
711

812
BASE_CMD = ["linode-cli", "networking"]
913

@@ -17,6 +21,32 @@ def setup_test_networking():
1721
delete_target_id(target="linodes", id=linode_id)
1822

1923

24+
@pytest.fixture(scope="package")
25+
def setup_test_networking_shared_ipv4():
26+
target_region = "us-southeast"
27+
28+
linode_ids = (
29+
create_linode(test_region=target_region),
30+
create_linode(test_region=target_region),
31+
)
32+
33+
yield linode_ids
34+
35+
for id in linode_ids:
36+
delete_target_id(target="linodes", id=id)
37+
38+
39+
def has_shared_ip(linode_id: int, ip: str) -> bool:
40+
shared_ips = json.loads(
41+
exec_test_command(
42+
["linode-cli", "linodes", "ips-list", "--json", linode_id]
43+
).stdout.decode()
44+
)[0]["ipv4"]["shared"]
45+
46+
# Ensure there is a matching shared IP
47+
return len([v for v in shared_ips if v["address"] == ip]) > 0
48+
49+
2050
def test_display_ips_for_available_linodes(setup_test_networking):
2151
result = exec_test_command(
2252
BASE_CMD + ["ips-list", "--text", "--no-headers", "--delimiter", ","]
@@ -89,3 +119,54 @@ def test_allocate_additional_private_ipv4_address(setup_test_networking):
89119
assert re.search(
90120
"ipv4,False,.*,[0-9][0-9][0-9][0-9][0-9][0-9][0-9]*", result
91121
)
122+
123+
124+
def test_share_ipv4_address(setup_test_networking_shared_ipv4):
125+
target_linode, parent_linode = setup_test_networking_shared_ipv4
126+
127+
# Allocate an IPv4 address on the parent Linode
128+
ip_address = json.loads(
129+
exec_test_command(
130+
BASE_CMD
131+
+ [
132+
"ip-add",
133+
"--type",
134+
"ipv4",
135+
"--linode_id",
136+
parent_linode,
137+
"--json",
138+
"--public",
139+
"true",
140+
]
141+
).stdout.decode()
142+
)[0]["address"]
143+
144+
# Share the IP address to the target Linode
145+
exec_test_command(
146+
BASE_CMD
147+
+ [
148+
"ip-share",
149+
"--ips",
150+
ip_address,
151+
"--linode_id",
152+
target_linode,
153+
"--json",
154+
]
155+
)
156+
157+
assert has_shared_ip(target_linode, ip_address)
158+
159+
# Remove the IP shares
160+
exec_test_command(
161+
BASE_CMD
162+
+ [
163+
"ip-share",
164+
"--ips",
165+
"[]",
166+
"--linode_id",
167+
target_linode,
168+
"--json",
169+
]
170+
)
171+
172+
assert not has_shared_ip(target_linode, ip_address)

tests/unit/test_operation.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,35 @@ def test_parse_args_nullable_float(self, create_operation):
158158

159159
result = create_operation.parse_args(["--nullable_float", "456.123"])
160160
assert result.nullable_float == 456.123
161+
162+
def test_array_arg_action_basic(self):
163+
"""
164+
Tests a basic array argument condition..
165+
"""
166+
167+
parser = argparse.ArgumentParser(
168+
prog=f"foo",
169+
)
170+
171+
parser.add_argument(
172+
"--foo",
173+
metavar="foo",
174+
action=operation.ArrayAction,
175+
type=str,
176+
)
177+
178+
# User specifies a normal list
179+
result = parser.parse_args(["--foo", "foo", "--foo", "bar"])
180+
assert getattr(result, "foo") == ["foo", "bar"]
181+
182+
# User wants an explicitly empty list
183+
result = parser.parse_args(["--foo", "[]"])
184+
assert getattr(result, "foo") == []
185+
186+
# User doesn't specify the list
187+
result = parser.parse_args([])
188+
assert getattr(result, "foo") is None
189+
190+
# User specifies a normal value and an empty list value
191+
result = parser.parse_args(["--foo", "foo", "--foo", "[]"])
192+
assert getattr(result, "foo") == ["foo", "[]"]

wiki/Usage.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,31 @@ linode-cli linodes create \
108108
When running certain commands, you may need to specify an argument that is nested
109109
in another field. These arguments can be specified using a `.` delimited path to
110110
the argument. For example, to create a firewall with an inbound policy of `DROP`
111-
and an outbound policy of `ACCEPT`, you can execute the following::
111+
and an outbound policy of `ACCEPT`, you can execute the following:
112112
```bash
113113
linode-cli firewalls create --label example-firewall --rules.outbound_policy ACCEPT --rules.inbound_policy DROP
114114
```
115115

116+
## Special Arguments
117+
118+
In some cases, certain values for arguments may have unique functionality.
119+
120+
### Null Values
121+
122+
Arguments marked as nullable can be passed the value `null` to send an explicit null value to the Linode API:
123+
124+
```bash
125+
linode-cli networking ip-update --rdns null 127.0.0.1
126+
```
127+
128+
### Empty Lists
129+
130+
List arguments can be passed the value `[]` to send an explicit empty list value to the Linode API:
131+
132+
```bash
133+
linode-cli networking ip-share --linode_id 12345 --ips []
134+
```
135+
116136
## Suppressing Defaults
117137

118138
If you configured default values for `image`, `authorized_users`, `region`,

0 commit comments

Comments
 (0)