Skip to content

Commit e83d0fe

Browse files
authored
Merge pull request #557 from linode/dev
Release v5.46.0
2 parents 9b43cf8 + 90edae0 commit e83d0fe

File tree

14 files changed

+529
-53
lines changed

14 files changed

+529
-53
lines changed

.github/workflows/e2e-suite.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,11 @@ jobs:
4747
env:
4848
LINODE_CLI_TOKEN: ${{ secrets.LINODE_TOKEN }}
4949

50-
- name: Set release version env
51-
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
52-
5350
- name: Add additional information to XML report
5451
run: |
55-
echo $RELEASE_VERSION
56-
echo ${{ env.RELEASE_VERSION }}
5752
filename=$(ls | grep -E '^[0-9]{12}_cli_test_report\.xml$')
5853
python scripts/add_to_xml_test_report.py \
59-
--branch_name "${{ env.RELEASE_VERSION }}" \
54+
--branch_name "${GITHUB_REF#refs/*/}" \
6055
--gha_run_id "$GITHUB_RUN_ID" \
6156
--gha_run_number "$GITHUB_RUN_NUMBER" \
6257
--xmlfile "${filename}"

.github/workflows/oci-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jobs:
1616
with:
1717
python-version: '3.x'
1818

19+
- name: Install deps
20+
run: make requirements
21+
1922
- name: Set up QEMU
2023
uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # pin@v2.2.0
2124

linodecli/plugins/obj/__init__.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@
7878
INVALID_PAGE_MSG = "No result to show in this page."
7979

8080

81+
def get_available_cluster(cli: CLI):
82+
"""Get list of possible clusters for the account"""
83+
return [
84+
c["id"]
85+
for c in _do_get_request( # pylint: disable=protected-access
86+
cli.config.base_url,
87+
"/object-storage/clusters",
88+
token=cli.config.get_token(),
89+
)["data"]
90+
]
91+
92+
8193
def flip_to_page(iterable: Iterable, page: int = 1):
8294
"""Given a iterable object and return a specific iteration (page)"""
8395
iterable = iter(iterable)
@@ -451,7 +463,7 @@ def print_help(parser: ArgumentParser):
451463
print("See --help for individual commands for more information")
452464

453465

454-
def get_obj_args_parser():
466+
def get_obj_args_parser(clusters: List[str]):
455467
"""
456468
Initialize and return the argument parser for the obj plug-in.
457469
"""
@@ -468,6 +480,7 @@ def get_obj_args_parser():
468480
"--cluster",
469481
metavar="CLUSTER",
470482
type=str,
483+
choices=clusters,
471484
help="The cluster to use for the operation",
472485
)
473486

@@ -527,7 +540,8 @@ def call(
527540

528541
sys.exit(2) # requirements not met - we can't go on
529542

530-
parser = get_obj_args_parser()
543+
clusters = get_available_cluster(context.client)
544+
parser = get_obj_args_parser(clusters)
531545
parsed, args = parser.parse_known_args(args)
532546

533547
# don't mind --no-defaults if it's there; the top-level parser already took care of it
@@ -710,14 +724,7 @@ def _configure_plugin(client: CLI):
710724
"""
711725
Configures a default cluster value.
712726
"""
713-
clusters = [
714-
c["id"]
715-
for c in _do_get_request( # pylint: disable=protected-access
716-
client.config.base_url,
717-
"/object-storage/clusters",
718-
token=client.config.get_value("token"),
719-
)["data"]
720-
]
727+
clusters = get_available_cluster(client)
721728

722729
cluster = _default_thing_input( # pylint: disable=protected-access
723730
"Configure a default Cluster for operations.",

linodecli/plugins/obj/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
The config of the object storage plugin.
33
"""
4+
import shutil
45

56
ENV_ACCESS_KEY_NAME = "LINODE_CLI_OBJ_ACCESS_KEY"
67
ENV_SECRET_KEY_NAME = "LINODE_CLI_OBJ_SECRET_KEY"
@@ -15,7 +16,8 @@
1516
# for help commands
1617
PLUGIN_BASE = "linode-cli obj"
1718

18-
PROGRESS_BAR_WIDTH = 100
19+
columns = shutil.get_terminal_size(fallback=(80, 24)).columns
20+
PROGRESS_BAR_WIDTH = columns - 20 if columns > 30 else columns
1921

2022
# constant error messages
2123
NO_SCOPES_ERROR = """Your OAuth token isn't authorized to create Object Storage keys.

linodecli/plugins/obj/helpers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ def __call__(self, bytes_amount: int):
2626
if not self.size:
2727
return
2828
self.uploaded += bytes_amount
29-
percentage = self.bar_width * (self.uploaded / self.size)
30-
progress = int(percentage)
29+
percentage = 100 * (self.uploaded / self.size)
30+
uploaded = self.bar_width * (self.uploaded / self.size)
31+
progress = int(uploaded)
3132
progress_bar = ("#" * progress) + ("-" * (self.bar_width - progress))
3233
print(f"\r |{progress_bar}| {percentage:.1f}%", end="\r")
3334
if self.uploaded == self.size:

linodecli/plugins/obj/objects.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,14 @@ def upload_object(
8585

8686
chunk_size = 1024 * 1024 * parsed.chunk_size
8787

88+
prefix = None
89+
bucket = parsed.bucket
90+
if "/" in parsed.bucket:
91+
bucket = parsed.bucket.split("/")[0]
92+
prefix = parsed.bucket.lstrip(f"{bucket}/")
93+
8894
upload_options = {
89-
"Bucket": parsed.bucket,
95+
"Bucket": bucket,
9096
"Config": TransferConfig(multipart_chunksize=chunk_size * MB),
9197
}
9298

@@ -95,8 +101,10 @@ def upload_object(
95101

96102
for file_path in to_upload:
97103
print(f"Uploading {file_path.name}:")
104+
upload_options["Key"] = (
105+
file_path.name if not prefix else f"{prefix}/{file_path.name}"
106+
)
98107
upload_options["Filename"] = str(file_path.resolve())
99-
upload_options["Key"] = file_path.name
100108
upload_options["Callback"] = ProgressPercentage(
101109
file_path.stat().st_size, PROGRESS_BAR_WIDTH
102110
)

scripts/add_to_xml_test_report.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
11
import argparse
22
import xml.etree.ElementTree as ET
3+
import requests
4+
5+
latest_release_url = "https://api.github.com/repos/linode/linode-cli/releases/latest"
6+
7+
8+
def get_release_version():
9+
url = latest_release_url
10+
11+
try:
12+
response = requests.get(url)
13+
response.raise_for_status() # Check for HTTP errors
14+
15+
release_info = response.json()
16+
version = release_info["tag_name"]
17+
18+
# Remove 'v' prefix if it exists
19+
if version.startswith("v"):
20+
version = version[1:]
21+
22+
return str(version)
23+
24+
except requests.exceptions.RequestException as e:
25+
print("Error:", e)
26+
except KeyError:
27+
print("Error: Unable to fetch release information from GitHub API.")
28+
329

430
# Parse command-line arguments
531
parser = argparse.ArgumentParser(description='Modify XML with workflow information')
632
parser.add_argument('--branch_name', required=True)
733
parser.add_argument('--gha_run_id', required=True)
834
parser.add_argument('--gha_run_number', required=True)
35+
parser.add_argument('--release_tag', required=False)
936
parser.add_argument('--xmlfile', required=True) # Added argument for XML file path
1037

1138
args = parser.parse_args()
@@ -25,10 +52,14 @@
2552
gha_run_number_element = ET.Element('gha_run_number')
2653
gha_run_number_element.text = args.gha_run_number
2754

55+
gha_release_tag_element = ET.Element('release_tag')
56+
gha_release_tag_element.text = get_release_version()
57+
2858
# Add the new elements to the root of the XML
2959
root.append(branch_name_element)
3060
root.append(gha_run_id_element)
3161
root.append(gha_run_number_element)
62+
root.append(gha_release_tag_element)
3263

3364
# Save the modified XML
3465
modified_xml_file_path = xml_file_path # Overwrite it

tests/integration/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,31 @@ def nodebalancer_with_default_conf():
374374
res_arr = result.split(",")
375375
nodebalancer_id = res_arr[0]
376376
delete_target_id(target="nodebalancers", id=nodebalancer_id)
377+
378+
379+
def get_regions_with_capabilities(capabilities):
380+
regions = (
381+
exec_test_command(
382+
[
383+
"linode-cli",
384+
"regions",
385+
"ls",
386+
"--text",
387+
"--no-headers",
388+
"--format=id,capabilities",
389+
]
390+
)
391+
.stdout.decode()
392+
.rstrip()
393+
)
394+
395+
regions = regions.split("\n")
396+
397+
regions_with_all_caps = []
398+
399+
for region in regions:
400+
region_name = region.split()[0]
401+
if all(capability in region for capability in capabilities):
402+
regions_with_all_caps.append(region_name)
403+
404+
return regions_with_all_caps

tests/integration/obj/test_obj_plugin.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from dataclasses import dataclass
34
from typing import Callable, Optional
@@ -6,16 +7,16 @@
67
import requests
78
from pytest import MonkeyPatch
89

9-
from linodecli.configuration.auth import _do_request
1010
from linodecli.plugins.obj import (
1111
ENV_ACCESS_KEY_NAME,
1212
ENV_SECRET_KEY_NAME,
1313
TRUNCATED_MSG,
1414
)
1515
from tests.integration.fixture_types import GetTestFilesType, GetTestFileType
16-
from tests.integration.helpers import BASE_URL, count_lines, exec_test_command
16+
from tests.integration.helpers import count_lines, exec_test_command
1717

1818
REGION = "us-southeast-1"
19+
CLI_CMD = ["linode-cli", "object-storage"]
1920
BASE_CMD = ["linode-cli", "obj", "--cluster", REGION]
2021

2122

@@ -50,27 +51,24 @@ def static_site_error():
5051

5152

5253
@pytest.fixture(scope="session")
53-
def keys(token: str):
54-
response = _do_request(
55-
BASE_URL,
56-
requests.post,
57-
"object-storage/keys",
58-
token,
59-
False,
60-
{"label": "cli-integration-test-obj-key"},
61-
)
62-
54+
def keys():
55+
response = json.loads(
56+
exec_test_command(
57+
CLI_CMD
58+
+ [
59+
"keys-create",
60+
"--label",
61+
"cli-integration-test-obj-key",
62+
"--json",
63+
],
64+
).stdout.decode()
65+
)[0]
6366
_keys = Keys(
6467
access_key=response.get("access_key"),
6568
secret_key=response.get("secret_key"),
6669
)
6770
yield _keys
68-
_do_request(
69-
BASE_URL,
70-
requests.delete,
71-
f"object-storage/keys/{response['id']}",
72-
token,
73-
)
71+
exec_test_command(CLI_CMD + ["keys-delete", str(response.get("id"))])
7472

7573

7674
def patch_keys(keys: Keys, monkeypatch: MonkeyPatch):
@@ -150,6 +148,51 @@ def test_obj_single_file_single_bucket(
150148
assert f1.read() == f2.read()
151149

152150

151+
def test_obj_single_file_single_bucket_with_prefix(
152+
create_bucket: Callable[[Optional[str]], str],
153+
generate_test_files: GetTestFilesType,
154+
keys: Keys,
155+
monkeypatch: MonkeyPatch,
156+
):
157+
patch_keys(keys, monkeypatch)
158+
file_path = generate_test_files()[0]
159+
bucket_name = create_bucket()
160+
exec_test_command(
161+
BASE_CMD + ["put", str(file_path), f"{bucket_name}/prefix"]
162+
)
163+
process = exec_test_command(BASE_CMD + ["la"])
164+
output = process.stdout.decode()
165+
166+
assert f"{bucket_name}/prefix/{file_path.name}" in output
167+
168+
file_size = file_path.stat().st_size
169+
assert str(file_size) in output
170+
171+
process = exec_test_command(BASE_CMD + ["ls"])
172+
output = process.stdout.decode()
173+
assert bucket_name in output
174+
assert file_path.name not in output
175+
176+
process = exec_test_command(BASE_CMD + ["ls", bucket_name])
177+
output = process.stdout.decode()
178+
assert bucket_name not in output
179+
assert "prefix" in output
180+
181+
downloaded_file_path = file_path.parent / f"downloaded_{file_path.name}"
182+
process = exec_test_command(
183+
BASE_CMD
184+
+ [
185+
"get",
186+
bucket_name,
187+
"prefix/" + file_path.name,
188+
str(downloaded_file_path),
189+
]
190+
)
191+
output = process.stdout.decode()
192+
with open(downloaded_file_path) as f2, open(file_path) as f1:
193+
assert f1.read() == f2.read()
194+
195+
153196
def test_multi_files_multi_bucket(
154197
create_bucket: Callable[[Optional[str]], str],
155198
generate_test_files: GetTestFilesType,

tests/integration/vpc/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)