playground-ls-cli 4.14.1.dev8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- localstack_cli/__init__.py +0 -0
- localstack_cli/cli/__init__.py +10 -0
- localstack_cli/cli/console.py +11 -0
- localstack_cli/cli/core_plugin.py +12 -0
- localstack_cli/cli/exceptions.py +19 -0
- localstack_cli/cli/localstack.py +951 -0
- localstack_cli/cli/lpm.py +138 -0
- localstack_cli/cli/main.py +22 -0
- localstack_cli/cli/plugin.py +39 -0
- localstack_cli/cli/plugins.py +134 -0
- localstack_cli/cli/profiles.py +65 -0
- localstack_cli/config.py +1689 -0
- localstack_cli/constants.py +165 -0
- localstack_cli/logging/__init__.py +0 -0
- localstack_cli/logging/format.py +194 -0
- localstack_cli/logging/setup.py +142 -0
- localstack_cli/packages/__init__.py +25 -0
- localstack_cli/packages/api.py +418 -0
- localstack_cli/packages/core.py +416 -0
- localstack_cli/pro/__init__.py +0 -0
- localstack_cli/pro/core/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/__init__.py +1 -0
- localstack_cli/pro/core/bootstrap/auth.py +213 -0
- localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
- localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
- localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
- localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
- localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
- localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
- localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
- localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
- localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
- localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
- localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
- localstack_cli/pro/core/cli/__init__.py +0 -0
- localstack_cli/pro/core/cli/auth.py +226 -0
- localstack_cli/pro/core/cli/aws.py +16 -0
- localstack_cli/pro/core/cli/cli.py +99 -0
- localstack_cli/pro/core/cli/click_utils.py +21 -0
- localstack_cli/pro/core/cli/cloud_pods.py +465 -0
- localstack_cli/pro/core/cli/diff_view.py +41 -0
- localstack_cli/pro/core/cli/ephemeral.py +199 -0
- localstack_cli/pro/core/cli/extensions.py +492 -0
- localstack_cli/pro/core/cli/iam.py +180 -0
- localstack_cli/pro/core/cli/license.py +90 -0
- localstack_cli/pro/core/cli/localstack.py +118 -0
- localstack_cli/pro/core/cli/replicator.py +378 -0
- localstack_cli/pro/core/cli/state.py +183 -0
- localstack_cli/pro/core/cli/tree_view.py +235 -0
- localstack_cli/pro/core/config.py +556 -0
- localstack_cli/pro/core/constants.py +54 -0
- localstack_cli/pro/core/plugins.py +169 -0
- localstack_cli/runtime/__init__.py +6 -0
- localstack_cli/runtime/exceptions.py +7 -0
- localstack_cli/runtime/hooks.py +73 -0
- localstack_cli/testing/__init__.py +1 -0
- localstack_cli/testing/config.py +4 -0
- localstack_cli/utils/__init__.py +0 -0
- localstack_cli/utils/analytics/__init__.py +12 -0
- localstack_cli/utils/analytics/cli.py +67 -0
- localstack_cli/utils/analytics/client.py +111 -0
- localstack_cli/utils/analytics/events.py +30 -0
- localstack_cli/utils/analytics/logger.py +48 -0
- localstack_cli/utils/analytics/metadata.py +250 -0
- localstack_cli/utils/analytics/publisher.py +160 -0
- localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
- localstack_cli/utils/archives.py +271 -0
- localstack_cli/utils/batching.py +258 -0
- localstack_cli/utils/bootstrap.py +1418 -0
- localstack_cli/utils/checksum.py +313 -0
- localstack_cli/utils/collections.py +554 -0
- localstack_cli/utils/common.py +229 -0
- localstack_cli/utils/container_networking.py +142 -0
- localstack_cli/utils/container_utils/__init__.py +0 -0
- localstack_cli/utils/container_utils/container_client.py +1585 -0
- localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
- localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
- localstack_cli/utils/crypto.py +294 -0
- localstack_cli/utils/docker_utils.py +272 -0
- localstack_cli/utils/files.py +327 -0
- localstack_cli/utils/functions.py +92 -0
- localstack_cli/utils/http.py +326 -0
- localstack_cli/utils/json.py +219 -0
- localstack_cli/utils/net.py +516 -0
- localstack_cli/utils/no_exit_argument_parser.py +19 -0
- localstack_cli/utils/numbers.py +49 -0
- localstack_cli/utils/objects.py +235 -0
- localstack_cli/utils/patch.py +260 -0
- localstack_cli/utils/platform.py +77 -0
- localstack_cli/utils/run.py +514 -0
- localstack_cli/utils/server/__init__.py +0 -0
- localstack_cli/utils/server/tcp_proxy.py +108 -0
- localstack_cli/utils/serving.py +187 -0
- localstack_cli/utils/ssl.py +71 -0
- localstack_cli/utils/strings.py +245 -0
- localstack_cli/utils/sync.py +267 -0
- localstack_cli/utils/threads.py +163 -0
- localstack_cli/utils/time.py +81 -0
- localstack_cli/utils/urls.py +21 -0
- localstack_cli/utils/venv.py +100 -0
- localstack_cli/utils/xml.py +41 -0
- localstack_cli/version.py +34 -0
- playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
- playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
- playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
- playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
- playground_ls_cli-4.14.1.dev8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from localstack_cli.pro.core.bootstrap.licensingv2 import (
|
|
5
|
+
AuthToken,
|
|
6
|
+
DevLocalstackEnvironment,
|
|
7
|
+
LicensingError,
|
|
8
|
+
get_credentials_from_environment,
|
|
9
|
+
get_licensed_environment,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(
|
|
14
|
+
name="license",
|
|
15
|
+
short_help="(Preview) Manage and verify your LocalStack license",
|
|
16
|
+
help="""
|
|
17
|
+
(Preview) Manage and verify your LocalStack license.
|
|
18
|
+
|
|
19
|
+
Your LocalStack license allows you to use advanced features of LocalStack.
|
|
20
|
+
""",
|
|
21
|
+
)
|
|
22
|
+
def license() -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@license.command("info")
|
|
27
|
+
def cmd_info():
|
|
28
|
+
try:
|
|
29
|
+
credentials = get_credentials_from_environment()
|
|
30
|
+
except Exception as e:
|
|
31
|
+
click.echo(f"credentials: {e}")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if credentials:
|
|
35
|
+
if isinstance(credentials, AuthToken):
|
|
36
|
+
if credentials.encoded() == "test":
|
|
37
|
+
credential_validity = "test"
|
|
38
|
+
else:
|
|
39
|
+
credential_validity = "valid syntax" if credentials.is_valid() else "invalid"
|
|
40
|
+
else:
|
|
41
|
+
credential_validity = ""
|
|
42
|
+
|
|
43
|
+
click.echo(
|
|
44
|
+
f"credentials: {type(credentials).__name__}({credentials.encoded()}) {credential_validity}"
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
click.echo("credentials: none found in environment")
|
|
48
|
+
|
|
49
|
+
env = get_licensed_environment()
|
|
50
|
+
if isinstance(env, DevLocalstackEnvironment):
|
|
51
|
+
click.echo("test credentials used, using unlicensed dev environment")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
license_file_path = None
|
|
55
|
+
for path in env.get_license_file_read_locations():
|
|
56
|
+
if os.path.isfile(path):
|
|
57
|
+
license_file_path = path
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
license_ = None
|
|
61
|
+
if license_file_path:
|
|
62
|
+
try:
|
|
63
|
+
with open(license_file_path, "rb") as fd:
|
|
64
|
+
content = fd.read()
|
|
65
|
+
license_ = env.parser.parse(content)
|
|
66
|
+
text_content = content.decode("utf-8")
|
|
67
|
+
click.echo(f"license location: {license_file_path}")
|
|
68
|
+
click.echo(f"license: {text_content}")
|
|
69
|
+
except LicensingError as e:
|
|
70
|
+
click.echo(f"license location: {license_file_path}")
|
|
71
|
+
click.echo(f"license: error reading license file {e}")
|
|
72
|
+
|
|
73
|
+
if license_:
|
|
74
|
+
try:
|
|
75
|
+
env.client.validate_license(env.require_valid_credentials(), license_)
|
|
76
|
+
click.echo("license validity: valid")
|
|
77
|
+
except LicensingError as e:
|
|
78
|
+
click.echo(f"license validity: invalid ({e})")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@license.command("activate")
|
|
82
|
+
def cmd_activate():
|
|
83
|
+
try:
|
|
84
|
+
env = get_licensed_environment()
|
|
85
|
+
env.activate()
|
|
86
|
+
text_license = env.serializer.serialize(env.license).decode("utf-8")
|
|
87
|
+
click.echo("license activation completed")
|
|
88
|
+
click.echo(f"license: {text_license}")
|
|
89
|
+
except LicensingError as e:
|
|
90
|
+
click.echo(e.get_user_friendly())
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from localstack_cli.cli import LocalstackCli, LocalstackCliPlugin, console
|
|
6
|
+
from localstack_cli.utils.analytics.cli import publish_invocation
|
|
7
|
+
|
|
8
|
+
from .auth import auth
|
|
9
|
+
from .aws import aws
|
|
10
|
+
from .cli import RequiresLicenseGroup
|
|
11
|
+
from .cloud_pods import pod
|
|
12
|
+
from .ephemeral import ephemeral
|
|
13
|
+
from .extensions import extensions
|
|
14
|
+
from .iam import iam # noqa
|
|
15
|
+
from .license import license
|
|
16
|
+
from .replicator import replicator
|
|
17
|
+
from .state import state
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProCliPlugins(LocalstackCliPlugin):
|
|
21
|
+
"""Plugin that adds more CLI commands to the localstack CLI. Some commands are subject to a license
|
|
22
|
+
activation, but all of them are shown by default."""
|
|
23
|
+
|
|
24
|
+
name = "pro"
|
|
25
|
+
|
|
26
|
+
def attach(self, cli: LocalstackCli) -> None:
|
|
27
|
+
group: click.Group = cli.group
|
|
28
|
+
group.add_command(dns)
|
|
29
|
+
group.add_command(aws) # noqa
|
|
30
|
+
group.add_command(extensions) # noqa
|
|
31
|
+
group.add_command(license) # noqa
|
|
32
|
+
group.add_command(state) # noqa
|
|
33
|
+
group.add_command(auth) # noqa
|
|
34
|
+
group.add_command(pod) # noqa
|
|
35
|
+
group.add_command(ephemeral) # noqa
|
|
36
|
+
group.add_command(replicator) # noqa
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@click.group(
|
|
40
|
+
name="dns",
|
|
41
|
+
short_help="Manage LocalStack DNS host config",
|
|
42
|
+
help="""
|
|
43
|
+
Manage the usage of the LocalStack DNS on your host.
|
|
44
|
+
|
|
45
|
+
This command provides tools to configure your the DNS on your host machine to use the LocalStack DNS
|
|
46
|
+
on your host machine.
|
|
47
|
+
The LocalStack DNS is used for certain Pro features (like the transparent endpoint injection).
|
|
48
|
+
|
|
49
|
+
\b
|
|
50
|
+
Visit https://docs.localstack.cloud/user-guide/tools/transparent-endpoint-injection/dns-server/
|
|
51
|
+
for more information on the LocalStack DNS and how it is used.
|
|
52
|
+
""",
|
|
53
|
+
cls=RequiresLicenseGroup,
|
|
54
|
+
)
|
|
55
|
+
def dns() -> None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dns.command(
|
|
60
|
+
name="systemd-resolved",
|
|
61
|
+
short_help="Manage LocalStack DNS in systemd-resolved",
|
|
62
|
+
help="""
|
|
63
|
+
Manage the LocalStack DNS configuration using systemd-resolved (Ubuntu, Debian, etc.).
|
|
64
|
+
|
|
65
|
+
This command sets (or reverts) the LocalStack DNS, running in the current LocalStack runtime, in
|
|
66
|
+
systemd-resolved for the docker network interface.
|
|
67
|
+
Most current Linux systems - like Ubuntu, Debian, or Fedora - use systemd-resolved for the network name
|
|
68
|
+
resolution.
|
|
69
|
+
""",
|
|
70
|
+
)
|
|
71
|
+
@click.option("--set/--revert", "-s/-r", "set_", default=True, help="Set or revert DNS settings")
|
|
72
|
+
@publish_invocation
|
|
73
|
+
def cmd_dns_systemd(set_: bool) -> None:
|
|
74
|
+
import localstack_cli.pro.core.bootstrap.dns_utils
|
|
75
|
+
from localstack_cli.pro.core.bootstrap.dns_utils import configure_systemd
|
|
76
|
+
|
|
77
|
+
console.print("Configuring systemd-resolved...")
|
|
78
|
+
logger_name = localstack_cli.pro.core.bootstrap.dns_utils.LOG.name
|
|
79
|
+
localstack_cli.pro.core.bootstrap.dns_utils.LOG = ConsoleLogger(logger_name)
|
|
80
|
+
configure_systemd(not set_)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ConsoleLogger(logging.Logger):
|
|
84
|
+
def __init__(self, name):
|
|
85
|
+
super().__init__(name)
|
|
86
|
+
|
|
87
|
+
def info(
|
|
88
|
+
self,
|
|
89
|
+
msg: Any,
|
|
90
|
+
*args: Any,
|
|
91
|
+
**kwargs: Any,
|
|
92
|
+
) -> None:
|
|
93
|
+
console.print(msg % args)
|
|
94
|
+
|
|
95
|
+
def warning(
|
|
96
|
+
self,
|
|
97
|
+
msg: Any,
|
|
98
|
+
*args: Any,
|
|
99
|
+
**kwargs: Any,
|
|
100
|
+
) -> None:
|
|
101
|
+
console.print("[red]Warning:[/red] ", msg % args)
|
|
102
|
+
|
|
103
|
+
def error(
|
|
104
|
+
self,
|
|
105
|
+
msg: Any,
|
|
106
|
+
*args: Any,
|
|
107
|
+
**kwargs: Any,
|
|
108
|
+
) -> None:
|
|
109
|
+
console.print("[red]Error:[/red] ", msg % args)
|
|
110
|
+
|
|
111
|
+
def exception(
|
|
112
|
+
self,
|
|
113
|
+
msg: Any,
|
|
114
|
+
*args: Any,
|
|
115
|
+
**kwargs: Any,
|
|
116
|
+
) -> None:
|
|
117
|
+
console.print("[red]Error:[/red] ", msg % args)
|
|
118
|
+
console.print_exception()
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess as sp
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from configparser import ConfigParser
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TypedDict
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from localstack_cli import config
|
|
16
|
+
from localstack_cli.cli import console
|
|
17
|
+
from localstack_cli.cli.exceptions import CLIError
|
|
18
|
+
|
|
19
|
+
from .cli import RequiresPlatformLicenseGroup, _assert_host_reachable
|
|
20
|
+
|
|
21
|
+
AWS_CONFIG_ENV_VARS = {
|
|
22
|
+
"aws_access_key_id": "{}_ACCESS_KEY_ID",
|
|
23
|
+
"aws_secret_access_key": "{}_SECRET_ACCESS_KEY",
|
|
24
|
+
"aws_session_token": "{}_SESSION_TOKEN",
|
|
25
|
+
"region_name": "{}_DEFAULT_REGION",
|
|
26
|
+
"endpoint_url": "{}_ENDPOINT_URL",
|
|
27
|
+
"profile_name": "{}_PROFILE",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
PREVIEW_BANNER = """
|
|
31
|
+
*** Preview Feature ***
|
|
32
|
+
|
|
33
|
+
This feature is currently in preview mode in our Teams offering and it's availability may change in future releases.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
REPLICATOR_HELP = (
|
|
37
|
+
PREVIEW_BANNER
|
|
38
|
+
+ """
|
|
39
|
+
|
|
40
|
+
The replicator command group allows you to replicate AWS resources into LocalStack.
|
|
41
|
+
"""
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_check_output(cmd) -> str:
|
|
46
|
+
return sp.check_output(cmd, stderr=sp.PIPE, env=os.environ).decode("utf-8").strip()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ProfileLoadError(RuntimeError):
|
|
50
|
+
def __init__(self, profile_name: str):
|
|
51
|
+
super().__init__(f"Could not find profile '{profile_name}'")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ReplicatorCliGroup(RequiresPlatformLicenseGroup):
|
|
55
|
+
name = "replicator"
|
|
56
|
+
tier = "Ultimate"
|
|
57
|
+
|
|
58
|
+
def invoke(self, ctx: click.Context):
|
|
59
|
+
print(PREVIEW_BANNER, file=sys.stderr)
|
|
60
|
+
super().invoke(ctx)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AWSConfig(TypedDict, total=False):
|
|
64
|
+
aws_access_key_id: str
|
|
65
|
+
aws_secret_access_key: str
|
|
66
|
+
aws_session_token: str | None
|
|
67
|
+
region_name: str
|
|
68
|
+
endpoint_url: str | None
|
|
69
|
+
profile_name: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_aws_env_config(prefix: str) -> AWSConfig:
|
|
73
|
+
aws_config = {
|
|
74
|
+
key: os.getenv(value.format(prefix)) for key, value in AWS_CONFIG_ENV_VARS.items()
|
|
75
|
+
}
|
|
76
|
+
return AWSConfig(**{k: v for k, v in aws_config.items() if v})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_config_from_profile(profile_name: str, profile_dir: Path | None = None) -> AWSConfig:
|
|
80
|
+
profile_dir = profile_dir or Path.home() / ".aws"
|
|
81
|
+
|
|
82
|
+
def load_config_for_profile(path: Path, profile_prefix: str = "") -> Mapping:
|
|
83
|
+
parser = ConfigParser()
|
|
84
|
+
parser.read(path)
|
|
85
|
+
try:
|
|
86
|
+
return parser[f"{profile_prefix}{profile_name}"]
|
|
87
|
+
except KeyError:
|
|
88
|
+
raise ProfileLoadError(profile_name)
|
|
89
|
+
|
|
90
|
+
config = load_config_for_profile(profile_dir / "config", profile_prefix="profile ")
|
|
91
|
+
credentials = load_config_for_profile(profile_dir / "credentials")
|
|
92
|
+
|
|
93
|
+
return AWSConfig(
|
|
94
|
+
aws_access_key_id=credentials["aws_access_key_id"],
|
|
95
|
+
aws_secret_access_key=credentials["aws_secret_access_key"],
|
|
96
|
+
region_name=config["region"],
|
|
97
|
+
profile_name=profile_name,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_awscli_config() -> AWSConfig | None:
|
|
102
|
+
try:
|
|
103
|
+
# get credentials values
|
|
104
|
+
cmd = ["aws", "configure", "export-credentials"]
|
|
105
|
+
credentials = json.loads(run_check_output(cmd))
|
|
106
|
+
except sp.CalledProcessError as exc:
|
|
107
|
+
if b"AWS CLI version 2" in exc.stderr:
|
|
108
|
+
print(
|
|
109
|
+
"Warning: awscli v1 installed. Please use v2 for auto detection of credentials",
|
|
110
|
+
file=sys.stderr,
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
# try to get the endpoint url
|
|
116
|
+
cmd = ["aws", "configure", "get", "endpoint_url"]
|
|
117
|
+
endpoint_url = run_check_output(cmd)
|
|
118
|
+
except sp.CalledProcessError:
|
|
119
|
+
# If there are no endpoint configured an exception is raised we do a last
|
|
120
|
+
# check in the environment to find the endpoint url
|
|
121
|
+
endpoint_url = os.getenv("AWS_ENDPOINT_URL")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# try to get the region from configure
|
|
125
|
+
cmd = ["aws", "configure", "get", "region"]
|
|
126
|
+
region_name = run_check_output(cmd)
|
|
127
|
+
except sp.CalledProcessError:
|
|
128
|
+
# If there are no default configured an exception is raised we do a last
|
|
129
|
+
# check in the environment to find the region
|
|
130
|
+
region_name = os.getenv("AWS_DEFAULT_REGION")
|
|
131
|
+
|
|
132
|
+
if not region_name:
|
|
133
|
+
try:
|
|
134
|
+
# older awscli versions do not return the region to the command `aws configure get region`.
|
|
135
|
+
# We need to rely on the configure list in this case
|
|
136
|
+
cmd = ["aws", "configure", "list"]
|
|
137
|
+
for line in run_check_output(cmd).splitlines():
|
|
138
|
+
if "region" not in line:
|
|
139
|
+
continue
|
|
140
|
+
# aws changed the format of configure and added `:` in delimiters
|
|
141
|
+
words = re.split(r"[:\s]+", line)
|
|
142
|
+
try:
|
|
143
|
+
region_name = words[1]
|
|
144
|
+
break
|
|
145
|
+
except IndexError:
|
|
146
|
+
return None
|
|
147
|
+
except (sp.CalledProcessError, FileNotFoundError):
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
return AWSConfig(
|
|
151
|
+
aws_access_key_id=credentials["AccessKeyId"],
|
|
152
|
+
aws_secret_access_key=credentials["SecretAccessKey"],
|
|
153
|
+
aws_session_token=credentials.get("SessionToken"),
|
|
154
|
+
region_name=region_name,
|
|
155
|
+
endpoint_url=endpoint_url,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_source_config(profile_dir: Path | None = None) -> AWSConfig:
|
|
160
|
+
awscli_source_config = get_awscli_config()
|
|
161
|
+
if awscli_source_config:
|
|
162
|
+
print("Configured credentials from the AWS CLI", file=sys.stderr)
|
|
163
|
+
return awscli_source_config
|
|
164
|
+
|
|
165
|
+
source_config = get_aws_env_config("AWS")
|
|
166
|
+
profile_name = source_config.get("profile_name")
|
|
167
|
+
|
|
168
|
+
if source_config.get("aws_access_key_id") and not source_config.get("aws_secret_access_key"):
|
|
169
|
+
raise CLIError(
|
|
170
|
+
"Unable to retrieve credentials: Partial credentials found in env."
|
|
171
|
+
" Need both 'AWS_ACCESS_KEY_ID' and 'AWS_SECRET_ACCESS_KEY'"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if profile_name:
|
|
175
|
+
config_file_source_config = get_config_from_profile(
|
|
176
|
+
profile_name=profile_name, profile_dir=profile_dir
|
|
177
|
+
)
|
|
178
|
+
if source_config.get("aws_secret_access_key"):
|
|
179
|
+
# before merging, if the secret access key is in the env vars we need to ensure the session token from the
|
|
180
|
+
# config file isn't carried along
|
|
181
|
+
config_file_source_config.pop("aws_session_token", None)
|
|
182
|
+
config_file_source_config.update(source_config)
|
|
183
|
+
source_config = config_file_source_config
|
|
184
|
+
|
|
185
|
+
errors = []
|
|
186
|
+
|
|
187
|
+
if not source_config.get("region_name"):
|
|
188
|
+
errors.append("'AWS_DEFAULT_REGION' must bet set in environment or in profile.")
|
|
189
|
+
if not source_config.get("aws_access_key_id"):
|
|
190
|
+
errors.append("'AWS_ACCESS_KEY_ID' must bet set in environment or in profile.")
|
|
191
|
+
if not source_config.get("aws_secret_access_key"):
|
|
192
|
+
errors.append("'AWS_SECRET_ACCESS_KEY' must bet set in environment or in profile.")
|
|
193
|
+
|
|
194
|
+
if errors:
|
|
195
|
+
raise CLIError("\n".join(errors))
|
|
196
|
+
|
|
197
|
+
return source_config
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_target_config(access_key: str = "", region_name: str = "") -> AWSConfig:
|
|
201
|
+
target_config = get_aws_env_config("TARGET")
|
|
202
|
+
if access_key:
|
|
203
|
+
target_config["aws_access_key_id"] = access_key
|
|
204
|
+
if region_name:
|
|
205
|
+
target_config["region_name"] = region_name
|
|
206
|
+
|
|
207
|
+
return target_config
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_replicator_url():
|
|
211
|
+
_assert_host_reachable()
|
|
212
|
+
return f"{config.external_service_url()}/_localstack/replicator"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@click.group(
|
|
216
|
+
name="replicator",
|
|
217
|
+
short_help="(Preview) Start a replication job or check its status",
|
|
218
|
+
help=REPLICATOR_HELP,
|
|
219
|
+
cls=ReplicatorCliGroup,
|
|
220
|
+
)
|
|
221
|
+
def replicator() -> None:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def validate_start_command(
|
|
226
|
+
replication_type: str,
|
|
227
|
+
resource_arn: str | None = None,
|
|
228
|
+
resource_type: str | None = None,
|
|
229
|
+
resource_identifier: str | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
if replication_type == "SINGLE_RESOURCE":
|
|
232
|
+
if not (resource_arn or resource_type):
|
|
233
|
+
raise CLIError("You must specify either --resource-arn or --resource_type")
|
|
234
|
+
|
|
235
|
+
if resource_arn and resource_type:
|
|
236
|
+
raise CLIError("You must specify either --resource-arn or --resource_type")
|
|
237
|
+
|
|
238
|
+
if resource_type and not resource_identifier:
|
|
239
|
+
raise CLIError("You must specify --resource-id when using --resource-type")
|
|
240
|
+
|
|
241
|
+
if resource_arn and not resource_arn.startswith("arn:aws:"):
|
|
242
|
+
raise CLIError("--resource-arn must start with arn:aws:")
|
|
243
|
+
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@replicator.command(
|
|
248
|
+
name="start",
|
|
249
|
+
short_help="Replicate an AWS resource",
|
|
250
|
+
help="""
|
|
251
|
+
Starts a job to replicate an AWS resource into localstack.
|
|
252
|
+
You must have credentials with sufficient read access to the resource trying to replicate.
|
|
253
|
+
At the moment only environment variables are recognized.
|
|
254
|
+
`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_DEFAULT_REGION` must be set. `AWS_ENDPOINT_URL` and `AWS_SESSION_TOKEN` are optional.
|
|
255
|
+
""",
|
|
256
|
+
)
|
|
257
|
+
@click.option(
|
|
258
|
+
"--replication-type",
|
|
259
|
+
type=click.Choice(["SINGLE_RESOURCE", "BATCH"]),
|
|
260
|
+
default="SINGLE_RESOURCE",
|
|
261
|
+
show_default=True,
|
|
262
|
+
help="Type of replication job: SINGLE_RESOURCE, BATCH",
|
|
263
|
+
)
|
|
264
|
+
@click.option(
|
|
265
|
+
"--explore-strategy",
|
|
266
|
+
type=click.Choice(["SIMPLE", "TREE"]),
|
|
267
|
+
default="SIMPLE",
|
|
268
|
+
show_default=True,
|
|
269
|
+
help="How we explore the resource tree. SIMPLE only replicates the resource requested.",
|
|
270
|
+
)
|
|
271
|
+
@click.option(
|
|
272
|
+
"--resource-arn",
|
|
273
|
+
help="ARN of the resource to recreate. Optional for SINGLE_RESOURCE replication",
|
|
274
|
+
)
|
|
275
|
+
@click.option(
|
|
276
|
+
"--resource-type",
|
|
277
|
+
help="CloudControl type of the resource to recreate. Optional for SINGLE_RESOURCE replication",
|
|
278
|
+
)
|
|
279
|
+
@click.option(
|
|
280
|
+
"--resource-identifier",
|
|
281
|
+
help="CloudControl identifier of the resource to recreate. Mandatory if --resource-type is used",
|
|
282
|
+
)
|
|
283
|
+
@click.option(
|
|
284
|
+
"--target-account-id",
|
|
285
|
+
# TODO add link to the docs
|
|
286
|
+
help="Localstack account ID where the resources will be replicated. Defaults to 000000000000. See <docs> to enable same account replication",
|
|
287
|
+
)
|
|
288
|
+
@click.option(
|
|
289
|
+
"--target-region-name",
|
|
290
|
+
# TODO add link to the docs
|
|
291
|
+
help="Localstack region where the resources will be replicated. Only provide if different than source AWS account.",
|
|
292
|
+
)
|
|
293
|
+
@click.option("--delay", help="Delay for the MOCK replication work")
|
|
294
|
+
def start(
|
|
295
|
+
replication_type: str,
|
|
296
|
+
explore_strategy: str,
|
|
297
|
+
resource_arn: str | None = None,
|
|
298
|
+
resource_type: str | None = None,
|
|
299
|
+
resource_identifier: str | None = None,
|
|
300
|
+
delay: str | None = None,
|
|
301
|
+
target_account_id: str | None = None,
|
|
302
|
+
target_region_name: str | None = None,
|
|
303
|
+
) -> None:
|
|
304
|
+
validate_start_command(replication_type, resource_arn, resource_type, resource_identifier)
|
|
305
|
+
|
|
306
|
+
source_config = get_source_config()
|
|
307
|
+
# TODO Do we want to keep those in the cli? there may be some Most Common Settings
|
|
308
|
+
# that we can allow from the cli for simplicity, and the rest through env var?
|
|
309
|
+
target_config = get_target_config(access_key=target_account_id, region_name=target_region_name)
|
|
310
|
+
|
|
311
|
+
replication_config = {}
|
|
312
|
+
if resource_arn:
|
|
313
|
+
replication_config["resource_arn"] = resource_arn
|
|
314
|
+
if resource_type:
|
|
315
|
+
replication_config["resource_type"] = resource_type
|
|
316
|
+
if resource_identifier:
|
|
317
|
+
replication_config["resource_identifier"] = resource_identifier
|
|
318
|
+
|
|
319
|
+
if replication_type == "MOCK":
|
|
320
|
+
replication_config["delay"] = float(delay) if delay else 1
|
|
321
|
+
|
|
322
|
+
replicator_url = f"{get_replicator_url()}/jobs"
|
|
323
|
+
|
|
324
|
+
payload = {
|
|
325
|
+
"replication_type": replication_type,
|
|
326
|
+
"explore_strategy": explore_strategy,
|
|
327
|
+
"replication_job_config": replication_config,
|
|
328
|
+
"source_aws_config": source_config,
|
|
329
|
+
"target_aws_config": target_config,
|
|
330
|
+
}
|
|
331
|
+
response = requests.post(replicator_url, json=payload)
|
|
332
|
+
|
|
333
|
+
if response.status_code == 200:
|
|
334
|
+
console.print_json(json=response.text)
|
|
335
|
+
else:
|
|
336
|
+
raise CLIError(f"Failed to create replication job: {response.status_code}, {response.text}")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@replicator.command(
|
|
340
|
+
name="status",
|
|
341
|
+
short_help="Check replication status",
|
|
342
|
+
help="""
|
|
343
|
+
Check the status of a replication job using its Job ID.
|
|
344
|
+
Use the --follow flag to continuously check the status until the job is completed.
|
|
345
|
+
""",
|
|
346
|
+
)
|
|
347
|
+
@click.argument("job_id")
|
|
348
|
+
@click.option("--follow", is_flag=True, help="Follow the status until completed")
|
|
349
|
+
@click.option("--delay", help="Delay between calls", default=5, type=int)
|
|
350
|
+
def status(job_id, follow: bool, delay: int) -> None:
|
|
351
|
+
url = f"{get_replicator_url()}/jobs/{job_id}"
|
|
352
|
+
while True:
|
|
353
|
+
response = requests.get(url)
|
|
354
|
+
|
|
355
|
+
if response.status_code == 200:
|
|
356
|
+
job = response.json()
|
|
357
|
+
console.print_json(data=job)
|
|
358
|
+
job_state = job.get("state")
|
|
359
|
+
if job_state == "ERROR":
|
|
360
|
+
raise CLIError(job.get("error_message"))
|
|
361
|
+
elif job_state == "SUCCEEDED":
|
|
362
|
+
return
|
|
363
|
+
else:
|
|
364
|
+
raise CLIError(f"Failed to replicate resource: {response.status_code}, {response.text}")
|
|
365
|
+
|
|
366
|
+
if not follow:
|
|
367
|
+
return
|
|
368
|
+
time.sleep(float(delay))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@replicator.command(name="resources", short_help="List supported resources")
|
|
372
|
+
def resources():
|
|
373
|
+
url = f"{get_replicator_url()}/resources"
|
|
374
|
+
response = requests.get(url)
|
|
375
|
+
if response.status_code != 200:
|
|
376
|
+
raise CLIError(f"Failed to get list of resources: {response.status_code}, {response.text}")
|
|
377
|
+
|
|
378
|
+
console.print_json(json=response.text)
|