Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ This can be done using the following environment variables to override certain s

* ``LINODE_CLI_API_SCHEME`` - The request scheme to use (e.g. ``https``)

Alternatively, these values can be configured per-user using the ``linode-cli configure`` command.

Multiple Users
^^^^^^^^^^^^^^

Expand Down
11 changes: 9 additions & 2 deletions linodecli/api_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,19 @@ def _build_filter_header(


def _build_request_url(ctx, operation, parsed_args) -> str:
result = operation.url.format(**vars(parsed_args))
target_server = handle_url_overrides(
operation.url_base,
host=ctx.config.get_value("api_host"),
version=ctx.config.get_value("api_version"),
scheme=ctx.config.get_value("api_scheme"),
)

result = f"{target_server}{operation.url_path}".format(**vars(parsed_args))

if operation.method == "get":
result += f"?page={ctx.page}&page_size={ctx.page_size}"

return handle_url_overrides(result)
return result


def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
Expand Down
10 changes: 8 additions & 2 deletions linodecli/baked/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,18 @@ def __init__(self, command, operation, method, params):
self.description = operation.description.split(".")[0]
self.params = [OpenAPIOperationParameter(c) for c in params]

server = (
# These fields must be stored separately
# to allow them to be easily modified
# at runtime.
self.url_base = (
operation.servers[0].url
if operation.servers
else operation._root.servers[0].url
)
self.url = server + operation.path[-2]

self.url_path = operation.path[-2]

self.url = self.url_base + self.url_path

docs_url = None
tags = operation.tags
Expand Down
39 changes: 30 additions & 9 deletions linodecli/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import argparse
import os
import sys
from typing import Dict

from .auth import (
_check_full_access,
Expand All @@ -14,8 +15,10 @@
_get_token_web,
)
from .helpers import (
_bool_input,
_check_browsers,
_config_get_with_default,
_default_text_input,
_default_thing_input,
_get_config,
_get_config_path,
Expand Down Expand Up @@ -442,21 +445,19 @@ def configure(
),
)

if _bool_input("Configure a custom API target?", default=False):
self._configure_api_target(config)

# save off the new configuration
if username != "DEFAULT" and not self.config.has_section(username):
self.config.add_section(username)

if not is_default:
if username != self.default_username():
while True:
value = input(
"Make this user the default when using the CLI? [y/N]: "
)
if value.lower() in "yn":
is_default = value.lower() == "y"
break
if not value.strip():
break
is_default = _bool_input(
"Make this user the default when using the CLI?"
)

if not is_default: # they didn't change the default user
print(
f"Active user will remain {self.config.get('DEFAULT', 'default-user')}"
Expand All @@ -479,3 +480,23 @@ def configure(
self.write_config()
os.chmod(_get_config_path(), 0o600)
self._configured = True

@staticmethod
def _configure_api_target(config: Dict[str, str]):
config["api_host"] = _default_text_input(
"NOTE: Skipping this field will use the default Linode API host.\n"
'API host override (e.g. "api.dev.linode.com")',
optional=True,
)

config["api_version"] = _default_text_input(
"NOTE: Skipping this field will use the default Linode API version.\n"
'API version override (e.g. "v4beta")',
optional=True,
)

config["api_scheme"] = _default_text_input(
"NOTE: Skipping this field will use the HTTPS scheme.\n"
'API scheme override (e.g. "https")',
optional=True,
)
118 changes: 81 additions & 37 deletions linodecli/configuration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import configparser
import os
import webbrowser
from typing import Any, Optional
from typing import Any, Callable, Optional

from .auth import _do_get_request

Expand All @@ -17,7 +17,6 @@
"XDG_CONFIG_HOME", f"{os.path.expanduser('~')}/.config"
)


# this is a list of browser that _should_ work for web-based auth. This is mostly
# intended to exclude lynx and other terminal browsers which could be opened, but
# won't work.
Expand Down Expand Up @@ -139,6 +138,69 @@ def _default_thing_input(
return things[choice_idx]


def _default_text_input(
ask: str,
default: str = None,
optional: bool = False,
validator: Callable[[str], Optional[str]] = None,
) -> Optional[str]: # pylint: disable=too-many-arguments
"""
Requests the user to enter a certain string of text with the given prompt.
If optional, the user may hit enter to not configure this option.
"""

prompt_text = f"\n{ask} "

if default is not None:
prompt_text += f"(Default {default})"
elif optional:
prompt_text += "(Optional)"

while True:
user_input = input(prompt_text + ": ")

# If the user skips on an optional value, return None
if user_input == "":
if default is not None:
return default

if optional:
return None

print("Please enter a valid value.")
continue

# Validate the user's input using the
# passed in validator.
if validator is not None:
validation_result = validator(user_input)

if validation_result is not None:
print(validation_result)
continue

return user_input


def _bool_input(
prompt: str, default: bool = True
): # pylint: disable=too-many-arguments
"""
Requests the user to enter either `y` or `n` given a prompt.
"""
while True:
user_input = input(f"\n{prompt} [y/N]: ").strip().lower()

if user_input == "":
return default

if user_input not in ("y", "n"):
print("Invalid input. Please input either y or n.")
continue

return user_input == "y"


def _config_get_with_default(
config: configparser.ConfigParser,
user: str,
Expand Down Expand Up @@ -187,41 +249,23 @@ def _handle_no_default_user(self): # pylint: disable=too-many-branches
self.config.add_section(username)
self.config.set(username, "token", token)

if self.config.has_option("DEFAULT", "region"):
self.config.set(
username, "region", self.config.get("DEFAULT", "region")
)

if self.config.has_option("DEFAULT", "type"):
self.config.set(
username, "type", self.config.get("DEFAULT", "type")
)

if self.config.has_option("DEFAULT", "image"):
self.config.set(
username, "image", self.config.get("DEFAULT", "image")
)

if self.config.has_option("DEFAULT", "mysql_engine"):
self.config.set(
username,
"mysql_engine",
self.config.get("DEFAULT", "mysql_engine"),
)

if self.config.has_option("DEFAULT", "postgresql_engine"):
self.config.set(
username,
"postgresql_engine",
self.config.get("DEFAULT", "postgresql_engine"),
)

if self.config.has_option("DEFAULT", "authorized_keys"):
self.config.set(
username,
"authorized_keys",
self.config.get("DEFAULT", "authorized_keys"),
)
config_keys = (
"region",
"type",
"image",
"mysql_engine",
"postgresql_engine",
"authorized_keys",
"api_host",
"api_version",
"api_scheme",
)

for key in config_keys:
if not self.config.has_option("DEFAULT", key):
continue

self.config.set(username, key, self.config.get("DEFAULT", key))

self.write_config()
else:
Expand Down
10 changes: 6 additions & 4 deletions linodecli/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@
API_CA_PATH = os.getenv("LINODE_CLI_CA", None) or True


def handle_url_overrides(url):
def handle_url_overrides(
url: str, host: str = None, version: str = None, scheme: str = None
):
"""
Returns the URL with the API URL environment overrides applied.
"""

parsed_url = urlparse(url)

overrides = {
"netloc": API_HOST_OVERRIDE,
"path": API_VERSION_OVERRIDE,
"scheme": API_SCHEME_OVERRIDE,
"netloc": API_HOST_OVERRIDE or host,
"path": API_VERSION_OVERRIDE or version,
"scheme": API_SCHEME_OVERRIDE or scheme,
}

# Apply overrides
Expand Down
8 changes: 7 additions & 1 deletion tests/fixtures/api_request_test_foobar_get.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ info:
title: API Specification
version: 1.0.0
servers:
- url: http://localhost
- url: http://localhost/v4

paths:
/foo/bar:
Expand Down Expand Up @@ -39,6 +39,12 @@ components:
x-linode-filterable: true
type: string
description: Filterable result value
filterable_list_result:
x-linode-filterable: true
type: array
items:
type: string
description: Filterable result value
PaginationEnvelope:
type: object
properties:
Expand Down
1 change: 1 addition & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def list_operation():

list_operation = make_test_operation(
command, operation, method, path.parameters
)

return list_operation

Expand Down
21 changes: 19 additions & 2 deletions tests/unit/test_api_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,24 @@ def test_build_request_url_get(self, mock_cli, list_operation):
mock_cli, list_operation, SimpleNamespace()
)

assert "http://localhost/foo/bar?page=1&page_size=100" == result
assert "http://localhost/v4/foo/bar?page=1&page_size=100" == result

def test_build_request_url_with_overrides(self, mock_cli, list_operation):
def mock_getvalue(key: str):
return {
"api_host": "foobar.local",
"api_scheme": "https",
"api_version": "v4beta",
}[key]

mock_cli.config.get_value = mock_getvalue

result = api_request._build_request_url(
mock_cli, list_operation, SimpleNamespace()
)
assert (
result == "https://foobar.local/v4beta/foo/bar?page=1&page_size=100"
)

def test_build_request_url_post(self, mock_cli, create_operation):
result = api_request._build_request_url(
Expand Down Expand Up @@ -112,7 +129,7 @@ def test_do_request_get(self, mock_cli, list_operation):
mock_response = Mock(status_code=200, reason="OK")

def validate_http_request(url, headers=None, data=None, **kwargs):
assert url == "http://localhost/foo/bar?page=1&page_size=100"
assert url == "http://localhost/v4/foo/bar?page=1&page_size=100"
assert headers["X-Filter"] == json.dumps(
{
"+and": [
Expand Down
Loading