Skip to content

Commit 5114107

Browse files
authored
Merge branch 'dev' into feature/TPT-4457-add-integration-tests-for-rdma-interfaces
2 parents 7167690 + dc02f0c commit 5114107

7 files changed

Lines changed: 108 additions & 18 deletions

File tree

.github/workflows/publish-wiki.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- uses: actions/checkout@v7
18-
- uses: Andrew-Chen-Wang/github-wiki-action@6448478bd55f1f3f752c93af8ac03207eccc3213 # pin@v5.0.3
18+
- uses: Andrew-Chen-Wang/github-wiki-action@1bbb4280446f9630e8e21a18012cbacf3b0f992e # pin@v5.0.6

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@ Visit the [Wiki](../../wiki) for more information.
1212

1313
## Install
1414

15-
Install via PyPI:
15+
We recommend installing `linode-cli` with `pipx`, which installs each Python CLI tool into its own isolated environment and works on distributions where `pip install` fails because the system Python is marked as externally managed ([PEP 668](https://peps.python.org/pep-0668/)). If pipx isn't installed yet, follow the [pipx installation guide](https://pipx.pypa.io/latest/how-to/install-pipx.html).
16+
17+
To install:
18+
19+
```bash
20+
pipx install linode-cli
21+
```
22+
23+
To upgrade:
24+
1625
```bash
17-
pip3 install linode-cli
26+
pipx upgrade linode-cli
1827
```
1928

20-
Visit the [Wiki](../../wiki/Installation) for more information.
29+
The [Wiki](https://github.com/linode/linode-cli/wiki/Installation) covers other installation methods, including the Docker image, the GitHub Action, and building from source.
2130

2231
## Contributing
2332

linodecli/configuration/auth.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,12 @@ def _get_token_terminal(base_url: str) -> Tuple[str, str]:
214214
:returns: A tuple containing the user's username and token.
215215
:rtype: Tuple[str, str]
216216
"""
217-
print(f"""
218-
First, we need a Personal Access Token. To get one, please visit
219-
{TOKEN_GENERATION_URL} and click
220-
"Create a Personal Access Token". The CLI needs access to everything
221-
on your account to work correctly.""")
217+
print(
218+
"First, we need a Personal Access Token. To get one, please visit\n"
219+
f"{TOKEN_GENERATION_URL} and click\n"
220+
'"Create a Personal Access Token". The CLI needs access to everything\n'
221+
"on your account to work correctly."
222+
)
222223

223224
while True:
224225
token = input("Personal Access Token: ")

linodecli/configuration/helpers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ def _check_browsers() -> bool:
9797

9898
# pylint: disable-next=protected-access
9999
if not KNOWN_GOOD_BROWSERS.intersection(webbrowser._tryorder):
100-
print("""
101-
This tool defaults to web-based authentication,
102-
however no known-working browsers were found.""")
100+
print(
101+
"This tool defaults to web-based authentication,\n"
102+
"however no known-working browsers were found."
103+
)
103104
while True:
104105
r = input("Try it anyway? [y/N]: ")
105106
if r.lower() in "yn ":

linodecli/plugins/get-kubeconfig.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import argparse
1010
import base64
11+
import os
1112
import sys
1213
from pathlib import Path
1314

@@ -19,6 +20,11 @@
1920

2021
PLUGIN_BASE = "linode-cli get-kubeconfig"
2122

23+
# Kubeconfigs contain credentials, so they should only be
24+
# accessible by the user that created them.
25+
KUBECONFIG_FILE_MODE = 0o600
26+
KUBECONFIG_DIR_MODE = 0o700
27+
2228

2329
def call(args, context):
2430
"""
@@ -147,8 +153,27 @@ def _load_config(filepath):
147153

148154
# Dumps data to a yaml file
149155
def _dump_config(filepath, data):
150-
Path.mkdir(filepath.parent, exist_ok=True)
151-
with open(filepath, "w", encoding="utf-8") as file_descriptor:
156+
filepath.parent.mkdir(mode=KUBECONFIG_DIR_MODE, parents=True, exist_ok=True)
157+
158+
# Create the file with restrictive permissions rather than chmod-ing it
159+
# afterwards, so its contents are never briefly readable by other users.
160+
# NOTE: The mode is only applied when the file is created.
161+
def opener(path, flags):
162+
return os.open(path, flags, mode=KUBECONFIG_FILE_MODE)
163+
164+
with open(
165+
filepath, "w", encoding="utf-8", opener=opener
166+
) as file_descriptor:
167+
# Tighten the permissions of pre-existing files that are readable or
168+
# writable by users other than the owner.
169+
# NOTE: os.fchmod is not available on Windows, where POSIX file modes
170+
# are not meaningful anyway.
171+
if (
172+
hasattr(os, "fchmod")
173+
and os.fstat(file_descriptor.fileno()).st_mode & 0o077
174+
):
175+
os.fchmod(file_descriptor.fileno(), KUBECONFIG_FILE_MODE)
176+
152177
yaml.dump(data, file_descriptor)
153178

154179

tests/integration/linodes/test_linode_interfaces.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,6 @@ def test_interface_settings_update(
146146
interface_id,
147147
"--default_route.ipv6_interface_id",
148148
interface_id,
149-
"--default_route.ipv4_eligible_interface_ids",
150-
interface_id,
151-
"--default_route.ipv6_eligible_interface_ids",
152-
interface_id,
153149
"--json",
154150
]
155151
)

tests/unit/test_plugin_kubeconfig.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,64 @@ def test_merge(mock_cli, fake_kubeconfig_file):
204204
assert result["dictionary"] == yaml_a["dictionary"]
205205

206206

207+
# Ensure newly created kubeconfig files are not world/group-readable
208+
@pytest.mark.skipif(
209+
os.name == "nt", reason="POSIX file modes are not supported on Windows"
210+
)
211+
def test_written_config_permissions(mock_cli):
212+
mock_cli.call_operation = mock_call_operation
213+
214+
with tempfile.TemporaryDirectory() as temp_dir:
215+
file_path = os.path.join(temp_dir, "new_dir", "nested", "config")
216+
217+
try:
218+
plugin.call(
219+
[
220+
"--label",
221+
"nonempty_data",
222+
"--kubeconfig",
223+
file_path,
224+
],
225+
PluginContext("REALTOKEN", mock_cli),
226+
)
227+
except SystemExit as err:
228+
assert err.code == 0
229+
230+
assert os.path.exists(file_path)
231+
assert os.stat(file_path).st_mode & 0o777 == 0o600
232+
assert os.stat(os.path.dirname(file_path)).st_mode & 0o777 == 0o700
233+
234+
235+
# Ensure pre-existing world-readable kubeconfig files get tightened
236+
@pytest.mark.skipif(
237+
os.name == "nt", reason="POSIX file modes are not supported on Windows"
238+
)
239+
def test_existing_config_permissions_tightened(mock_cli):
240+
mock_cli.call_operation = mock_call_operation
241+
242+
with tempfile.TemporaryDirectory() as temp_dir:
243+
file_path = os.path.join(temp_dir, "config")
244+
245+
with open(file_path, "w", encoding="utf-8") as file:
246+
file.write(TEST_YAML_CONTENT_A)
247+
os.chmod(file_path, 0o644)
248+
249+
try:
250+
plugin.call(
251+
[
252+
"--label",
253+
"nonempty_data",
254+
"--kubeconfig",
255+
file_path,
256+
],
257+
PluginContext("REALTOKEN", mock_cli),
258+
)
259+
except SystemExit as err:
260+
assert err.code == 0
261+
262+
assert os.stat(file_path).st_mode & 0o777 == 0o600
263+
264+
207265
def test_merge_to_empty_config(mock_cli, fake_kubeconfig_file_without_entries):
208266
stdout_buf = io.StringIO()
209267
mock_cli.call_operation = mock_call_operation

0 commit comments

Comments
 (0)