Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions linodecli/plugins/obj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from math import ceil
from typing import List, Optional

from packaging import version
from pytimeparse import parse as parse_time
from rich import print as rprint
from rich.table import Table
Expand All @@ -32,6 +33,7 @@
ENV_ACCESS_KEY_NAME,
ENV_SECRET_KEY_NAME,
INCOMING_DATE_FORMAT,
MIN_BOTOCORE_VERSION,
MULTIPART_UPLOAD_CHUNK_SIZE_DEFAULT,
NO_ACCESS_ERROR,
NO_SCOPES_ERROR,
Expand Down Expand Up @@ -59,6 +61,7 @@

try:
import boto3
import botocore
from botocore.config import Config
from botocore.exceptions import ClientError

Expand All @@ -68,6 +71,29 @@


CLUSTER_KEY = "cluster"


def _check_botocore_version():
"""
Exits with an upgrade hint if the installed botocore is too old for the
options the S3 client is configured with.
"""
if version.parse(botocore.__version__) >= version.parse(
MIN_BOTOCORE_VERSION
):
return

print(
f"This plugin requires boto3 {MIN_BOTOCORE_VERSION} or later, but "
f"botocore {botocore.__version__} is installed. Please upgrade it by "
"running 'pip3 install --upgrade boto3', or reinstall the CLI with "
"its Object Storage dependencies: "
"'pip3 install --upgrade \"linode-cli[obj]\"'",
file=sys.stderr,
)
sys.exit(ExitCodes.REQUEST_FAILED)


KEY_CLEANUP_ENABLED_KEY = "key-cleanup-enabled"
KEY_LIFESPAN_KEY = "key-lifespan"
KEY_ROTATION_PERIOD_KEY = "key-rotation-period"
Expand Down Expand Up @@ -430,6 +456,8 @@ def call(
ExitCodes.REQUEST_FAILED
) # requirements not met - we can't go on

_check_botocore_version()

parser = get_obj_args_parser()
parsed, args = parser.parse_known_args(args)

Expand Down
4 changes: 4 additions & 0 deletions linodecli/plugins/obj/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
# for help commands
PLUGIN_BASE = "linode-cli obj"

# the oldest botocore that accepts the checksum options the S3 client is
# configured with; boto3 releases use the matching botocore version
MIN_BOTOCORE_VERSION = "1.36.0"

columns = shutil.get_terminal_size(fallback=(80, 24)).columns
PROGRESS_BAR_WIDTH = columns - 20 if columns > 30 else columns

Expand Down
41 changes: 40 additions & 1 deletion tests/unit/test_plugin_obj.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import time
from unittest.mock import patch
from unittest.mock import Mock, patch

import pytest
from pytest import CaptureFixture

from linodecli import CLI, plugins
from linodecli.exit_codes import ExitCodes
from linodecli.plugins import obj
from linodecli.plugins.obj import get_obj_args_parser, helpers, print_help

Expand Down Expand Up @@ -336,3 +338,40 @@ def call_operation_side_effect(resource, action, *args, **kwargs):
assert (
not delete_calls
), "Cleanup should not be performed when key-cleanup-enabled is False"


@pytest.mark.parametrize("botocore_version", ["1.36.0", "1.43.1"])
def test_check_botocore_version_accepts_supported(botocore_version: str):
with patch.object(obj.botocore, "__version__", botocore_version):
obj._check_botocore_version()


def test_check_botocore_version_rejects_outdated(capsys: CaptureFixture):
with (
patch.object(obj.botocore, "__version__", "1.35.99"),
pytest.raises(SystemExit) as exc,
):
obj._check_botocore_version()

assert exc.value.code == ExitCodes.REQUEST_FAILED
err = capsys.readouterr().err
assert "requires boto3 1.36.0 or later" in err
assert "botocore 1.35.99 is installed" in err
assert "pip3 install --upgrade boto3" in err


def test_obj_action_with_outdated_botocore_exits_before_client():
command = Mock()
with (
patch.object(obj.botocore, "__version__", "1.35.99"),
patch("linodecli.plugins.obj.CLI") as MockCLI,
patch.dict(obj.COMMAND_MAP, {"ls": command}),
pytest.raises(SystemExit) as exc,
):
obj.call(
["ls", "bucket"],
plugins.PluginContext("12345", MockCLI.return_value),
)

assert exc.value.code == ExitCodes.REQUEST_FAILED
command.assert_not_called()