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,834 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import zipfile
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from functools import singledispatch
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import IO, Any, Optional, TypedDict
|
|
11
|
+
from urllib import parse
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
import requests
|
|
16
|
+
import yaml
|
|
17
|
+
from click import ClickException
|
|
18
|
+
from localstack_cli import config, constants
|
|
19
|
+
from localstack_cli.cli import console
|
|
20
|
+
from localstack_cli.constants import APPLICATION_JSON, HEADER_CONTENT_TYPE, LOCALHOST_HOSTNAME
|
|
21
|
+
from localstack_cli.pro.core.bootstrap import auth
|
|
22
|
+
from localstack_cli.pro.core.bootstrap.auth import get_platform_auth_headers
|
|
23
|
+
from localstack_cli.pro.core.bootstrap.pods.constants import INTERNAL_REQUEST_PARAMS_HEADER
|
|
24
|
+
from localstack_cli.pro.core.bootstrap.pods.remotes.api import CloudPodsRemotesClient
|
|
25
|
+
from localstack_cli.pro.core.bootstrap.pods.remotes.configs import (
|
|
26
|
+
DEFAULT_REMOTE_SCHEME,
|
|
27
|
+
RemoteConfig,
|
|
28
|
+
RemoteConfigParams,
|
|
29
|
+
)
|
|
30
|
+
from localstack_cli.pro.core.bootstrap.pods.remotes.params import get_remote_params_callable
|
|
31
|
+
from localstack_cli.pro.core.config import CLI_INJECT_POD_IDENTITY, POD_LOAD_CLI_TIMEOUT
|
|
32
|
+
from localstack_cli.pro.core.constants import (
|
|
33
|
+
API_PATH_PODS,
|
|
34
|
+
CLOUDPODS_METADATA_FILE,
|
|
35
|
+
HEADER_POD_SECRET,
|
|
36
|
+
)
|
|
37
|
+
from localstack_cli.utils.bootstrap import in_ci
|
|
38
|
+
from localstack_cli.utils.http import safe_requests
|
|
39
|
+
from localstack_cli.utils.strings import to_str
|
|
40
|
+
from packaging import version
|
|
41
|
+
from requests.structures import CaseInsensitiveDict
|
|
42
|
+
from rich.progress import Progress
|
|
43
|
+
|
|
44
|
+
LOG = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
# header constants
|
|
47
|
+
HEADER_LS_API_KEY = "ls-api-key"
|
|
48
|
+
HEADER_LS_VERSION = "ls-version"
|
|
49
|
+
HEADER_AUTHORIZATION = "Authorization"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
DiffResult = dict[str, list[dict[str, Any]]]
|
|
53
|
+
"""Maps a service to a list of localstack.pro.core.persistence.pods.diff.models.Operation"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CloudPodNotFound(Exception):
|
|
57
|
+
def __init__(self, pod_name: str) -> None:
|
|
58
|
+
super().__init__(f"Cloud pod '{pod_name}' not found")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PodInfo(TypedDict, total=False):
|
|
62
|
+
"""A set of info about a pod. Mostly used to display information on the CLI."""
|
|
63
|
+
|
|
64
|
+
name: str
|
|
65
|
+
pod_id: str
|
|
66
|
+
version: int
|
|
67
|
+
services: list[str]
|
|
68
|
+
description: str
|
|
69
|
+
size: int
|
|
70
|
+
remote: str
|
|
71
|
+
localstack_version: str
|
|
72
|
+
encrypted: bool
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def fetch_state_response_from_instance(
|
|
76
|
+
services: list[str] | None = None,
|
|
77
|
+
) -> tuple[requests.Response, PodInfo]:
|
|
78
|
+
url = f"{get_runtime_pods_endpoint()}/state"
|
|
79
|
+
params = ",".join(services) if services else ""
|
|
80
|
+
|
|
81
|
+
# TODO: temporary fix to bypass Gateway 503 responses on shutdown
|
|
82
|
+
headers = {INTERNAL_REQUEST_PARAMS_HEADER: "{}"}
|
|
83
|
+
|
|
84
|
+
is_ci: bool = in_ci()
|
|
85
|
+
result = requests.get(url, params={"services": params}, headers=headers, stream=not is_ci)
|
|
86
|
+
if not result.ok:
|
|
87
|
+
raise Exception(
|
|
88
|
+
f"An error occurred while retrieving the LocalStack state (code {result.status_code})"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
metadata = PodInfo(
|
|
92
|
+
services=result.headers.get("x-localstack-pod-services", "").split(","),
|
|
93
|
+
size=int(result.headers.get("x-localstack-pod-size", 0)),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return result, metadata
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def write_state_zip_to_file(
|
|
100
|
+
open_file: IO[bytes], services: list[str] | None = None, chunk_size: int = 100_000
|
|
101
|
+
) -> PodInfo:
|
|
102
|
+
"""
|
|
103
|
+
Retrieves the state from LocalStack by calling the container endpoint (/_localstack/pods/state).
|
|
104
|
+
|
|
105
|
+
Extract this state from LocalStack but write straight to a file object rather than buffering
|
|
106
|
+
in memory.
|
|
107
|
+
|
|
108
|
+
This specifically helps the state export command which writes to a local file, and enables it
|
|
109
|
+
to handle arbitrary file sizes.
|
|
110
|
+
|
|
111
|
+
:param open_file: open file object to write the bytes to
|
|
112
|
+
:param services: specify a list of services for which we want to extract the in-memory state;
|
|
113
|
+
if None, it extracts all
|
|
114
|
+
:param chunk_size: optional chunk size for the download (bytes)
|
|
115
|
+
:return: a PodInfo object containing the metadata for the pod
|
|
116
|
+
"""
|
|
117
|
+
result, metadata = fetch_state_response_from_instance(services)
|
|
118
|
+
content_length = int(result.headers.get("Content-Length", 0))
|
|
119
|
+
|
|
120
|
+
with Progress() as progress:
|
|
121
|
+
load_task = progress.add_task("Retrieving state from the container", total=content_length)
|
|
122
|
+
for chunk in result.iter_content(chunk_size=chunk_size):
|
|
123
|
+
open_file.write(chunk)
|
|
124
|
+
progress.update(load_task, advance=len(chunk))
|
|
125
|
+
|
|
126
|
+
return metadata
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_state_zip_from_instance(services: list[str] | None = None) -> tuple[bytes, PodInfo]:
|
|
130
|
+
"""
|
|
131
|
+
Retrieves the state from LocalStack by calling the container endpoint (/_localstack/pods/state).
|
|
132
|
+
|
|
133
|
+
:param services: specify a list of services for which we want to extract the in-memory state; if None, it extracts
|
|
134
|
+
all.
|
|
135
|
+
|
|
136
|
+
:return: a tuple with the bytes content and a PodInfo object containing a set of metadata.
|
|
137
|
+
"""
|
|
138
|
+
result, metadata = fetch_state_response_from_instance(services)
|
|
139
|
+
content_length = int(result.headers.get("Content-Length", 0))
|
|
140
|
+
is_ci: bool = in_ci()
|
|
141
|
+
content = result.content if is_ci else b""
|
|
142
|
+
|
|
143
|
+
with Progress() as progress:
|
|
144
|
+
load_task = progress.add_task("Retrieving state from the container", total=content_length)
|
|
145
|
+
for chunk in result.iter_content(chunk_size=100_000):
|
|
146
|
+
content += chunk
|
|
147
|
+
progress.update(load_task, advance=len(chunk))
|
|
148
|
+
|
|
149
|
+
return content, metadata
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class CloudPodRemoteAttributes(TypedDict, total=False):
|
|
153
|
+
"""A set of attributes for a Cloud Pod stored in a remote storage."""
|
|
154
|
+
|
|
155
|
+
is_public: bool
|
|
156
|
+
description: str | None
|
|
157
|
+
services: list[str] | None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class PodSaveRequest(TypedDict, total=False):
|
|
161
|
+
"""The payload for a pod save request"""
|
|
162
|
+
|
|
163
|
+
remote: dict[str, str | dict] | None
|
|
164
|
+
attributes: CloudPodRemoteAttributes | None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class CloudPodsService(ABC):
|
|
168
|
+
"""Service Interface for the Cloud Pods Operations."""
|
|
169
|
+
|
|
170
|
+
@abstractmethod
|
|
171
|
+
def save(
|
|
172
|
+
self,
|
|
173
|
+
pod_name: str,
|
|
174
|
+
attributes: CloudPodRemoteAttributes | None = None,
|
|
175
|
+
remote: RemoteConfigParams | None = None,
|
|
176
|
+
local: bool = False,
|
|
177
|
+
# TODO: consider defining version as str rather than int
|
|
178
|
+
version: int | None = None,
|
|
179
|
+
secret: str | None = None,
|
|
180
|
+
) -> PodInfo:
|
|
181
|
+
"""
|
|
182
|
+
Saves a new version of a Cloud Pod to a given storage (by default, our platform). In the process, it also
|
|
183
|
+
creates a version on the pods' fyle system.
|
|
184
|
+
:param pod_name: the name of the Cloud Pod
|
|
185
|
+
:param attributes: a set of attributes for the Cloud Pod
|
|
186
|
+
:param remote: the name/parameters of the remote storage. The default None equals to our platform
|
|
187
|
+
:param local: flag that prevent the registration of the pod with the remote (it only creates the version
|
|
188
|
+
locally). Mostly used for testing purposes.
|
|
189
|
+
:param version: the version of the pod to save (creates a new version if not specified)
|
|
190
|
+
:param secret: secret for encryption of the cloud pod content.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
def delete(
|
|
195
|
+
self,
|
|
196
|
+
pod_name: str,
|
|
197
|
+
remote: RemoteConfigParams | None = None,
|
|
198
|
+
delete_from_remote: bool = True,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Delete a Cloud Pod from the pods' file system and the remote storage.
|
|
202
|
+
:param pod_name: the name of the Cloud Pod
|
|
203
|
+
:param remote: the name/parameters of the remote storage. The default None equals to our platform
|
|
204
|
+
:param delete_from_remote: delete the pod from the remote storage. `False` would only delete the pod cache from
|
|
205
|
+
the local filesystem
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
@abstractmethod
|
|
209
|
+
def load(
|
|
210
|
+
self,
|
|
211
|
+
pod_name: str,
|
|
212
|
+
remote: RemoteConfigParams | None = None,
|
|
213
|
+
version: int | None = None,
|
|
214
|
+
merge_strategy: str | None = None,
|
|
215
|
+
ignore_version_mismatches: bool = True,
|
|
216
|
+
secret: str | None = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Loads a Cloud Pod into the LocalStack running instance. Depending on whether the latest pod's version is
|
|
220
|
+
available locally or not, it will also download it from the remote storage.
|
|
221
|
+
:param pod_name: the name of the Cloud Pod
|
|
222
|
+
:param remote: the name/parameters of the remote storage. The default None equals to our platform
|
|
223
|
+
:param version: the version of the Cloud Pod to load
|
|
224
|
+
:param merge_strategy: the merge strategy to use when loading the pod
|
|
225
|
+
:param ignore_version_mismatches: automatically approve the pod loading without any check or user confirmation
|
|
226
|
+
:param secret: eventual encryption key (for enterprise users)
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
@abstractmethod
|
|
230
|
+
def list(self, remote: RemoteConfigParams | None = None) -> list:
|
|
231
|
+
"""
|
|
232
|
+
List all the Cloud Pods available in the remote storage and in the pods' file system.
|
|
233
|
+
:param remote: the name/parameters of the remote
|
|
234
|
+
:return: a list of Cloud Pods
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
@abstractmethod
|
|
238
|
+
def get_versions(
|
|
239
|
+
self, pod_name: str, remote: RemoteConfigParams | None = None
|
|
240
|
+
) -> builtins.list:
|
|
241
|
+
"""
|
|
242
|
+
List all the versions of a Cloud Pod available in the remote storage.
|
|
243
|
+
:param pod_name: the name of the Cloud Pod
|
|
244
|
+
:param remote: the name/parameters of the remote storage. The default None equals to our platform
|
|
245
|
+
:return: a list of version for the given pod
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def diff(
|
|
249
|
+
self,
|
|
250
|
+
pod_name: str,
|
|
251
|
+
remote: RemoteConfigParams | None = None,
|
|
252
|
+
version: int | None = None,
|
|
253
|
+
) -> DiffResult:
|
|
254
|
+
"""
|
|
255
|
+
Return a list of operations that represent a diff between a state stored in a Cloud Pod and the one on the
|
|
256
|
+
runtime.
|
|
257
|
+
:param pod_name: the name of the Cloud Pod to run the diff with.
|
|
258
|
+
:param version: the version of the Cloud Pod to run the diff with.
|
|
259
|
+
:param remote: the name/parameters of the remote storage.
|
|
260
|
+
:return: a map between service and the list of operations representing a diff
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def _get_cloud_pods_info(self, pod_name: str) -> dict:
|
|
264
|
+
"""
|
|
265
|
+
This utility function make a call to platform and return the model object, i.e., a Cloud Pod entity.
|
|
266
|
+
todo: start exporting models from the platform to handle these cases
|
|
267
|
+
"""
|
|
268
|
+
response = requests.get(create_platform_url(pod_name), headers=get_platform_auth_headers())
|
|
269
|
+
if response.status_code == 404:
|
|
270
|
+
raise CloudPodNotFound(pod_name)
|
|
271
|
+
if not response.ok:
|
|
272
|
+
_raise_exception_with_formatted_message(
|
|
273
|
+
f"Unable to get info for pod: {pod_name}", response
|
|
274
|
+
)
|
|
275
|
+
return json.loads(response.content)
|
|
276
|
+
|
|
277
|
+
def _get_localstack_pod_version(
|
|
278
|
+
self,
|
|
279
|
+
pod_name: str,
|
|
280
|
+
cloud_pods_dict: dict,
|
|
281
|
+
version: int | None = None,
|
|
282
|
+
) -> str | None:
|
|
283
|
+
"""Returns the LocalStack version originating the Cloud Pod"""
|
|
284
|
+
versions: list[dict] = cloud_pods_dict["versions"]
|
|
285
|
+
max_version = int(cloud_pods_dict["max_version"])
|
|
286
|
+
if version and version > max_version:
|
|
287
|
+
raise Exception(
|
|
288
|
+
f"Unable to load pod {pod_name} with version {version}. The maximum version available in the"
|
|
289
|
+
f" remote storage is {max_version}"
|
|
290
|
+
)
|
|
291
|
+
# todo: generate some model to handle platform responses?
|
|
292
|
+
matching_versions = list(filter(lambda v: v["version"] == version, versions))
|
|
293
|
+
my_version = matching_versions[0] if matching_versions else versions[-1]
|
|
294
|
+
ls_pod_version: str | None = my_version["localstack_version"]
|
|
295
|
+
return ls_pod_version
|
|
296
|
+
|
|
297
|
+
def get_state_data(self) -> dict:
|
|
298
|
+
"""
|
|
299
|
+
Gets a representation of the service data in the running LocalStack instance.
|
|
300
|
+
"""
|
|
301
|
+
response = requests.get(
|
|
302
|
+
url=f"{get_runtime_pods_endpoint()}/state/metamodel", headers=_get_headers()
|
|
303
|
+
)
|
|
304
|
+
if not response.ok:
|
|
305
|
+
_raise_exception_with_formatted_message(
|
|
306
|
+
"Unable to get state data from the LocalStack instance", response
|
|
307
|
+
)
|
|
308
|
+
return json.loads(response.content)
|
|
309
|
+
|
|
310
|
+
def set_remote_attributes( # noqa
|
|
311
|
+
self,
|
|
312
|
+
pod_name: str,
|
|
313
|
+
attributes: CloudPodRemoteAttributes,
|
|
314
|
+
remote: RemoteConfigParams | None = None,
|
|
315
|
+
) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Updates a set of attributes for a Cloud Pod stored in a remote storage.
|
|
318
|
+
:param pod_name: the name of the Cloud Pod
|
|
319
|
+
:param attributes: the attributes to update
|
|
320
|
+
:param remote: the name/parameters of the remote storage. The default None equals to our platform
|
|
321
|
+
"""
|
|
322
|
+
if remote:
|
|
323
|
+
LOG.debug(
|
|
324
|
+
"Trying to set attributes for remote '%s'. Currently we support attributes "
|
|
325
|
+
"only for the default remote",
|
|
326
|
+
remote,
|
|
327
|
+
)
|
|
328
|
+
return
|
|
329
|
+
url = create_platform_url(pod_name)
|
|
330
|
+
auth_header = auth.get_platform_auth_headers()
|
|
331
|
+
response = safe_requests.patch(
|
|
332
|
+
url, headers=auth_header, json={"is_public": attributes["is_public"]}
|
|
333
|
+
)
|
|
334
|
+
if not response.ok:
|
|
335
|
+
raise Exception(
|
|
336
|
+
f"Error setting remote attributes for Cloud Pod {pod_name} (code {response.status_code}): "
|
|
337
|
+
f"{response.text}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _get_headers() -> dict[str, str]:
|
|
342
|
+
"""Returns the headers for the HTTP request with the content type and the bearer token if available."""
|
|
343
|
+
headers = {HEADER_CONTENT_TYPE: APPLICATION_JSON}
|
|
344
|
+
# If this flag is enabled, we inject the authorization header via the API.
|
|
345
|
+
# This header will be used to authenticate platform calls to the platform from within the container.
|
|
346
|
+
# When they are not provided, the header will be constructed from the credentials used to start the pro
|
|
347
|
+
# container. This mechanism allows to detach, if needed, container identity from pod identity.
|
|
348
|
+
if CLI_INJECT_POD_IDENTITY:
|
|
349
|
+
headers.update(CaseInsensitiveDict(auth.get_platform_auth_headers()))
|
|
350
|
+
return headers
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _raise_exception_with_formatted_message(message: str, response: requests.Response):
|
|
354
|
+
"""Raises an exception with a formatted message that includes the response status code and text."""
|
|
355
|
+
raise Exception(f"{message}: {response.text}")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _get_remote_params_payload(remote: RemoteConfigParams | None) -> PodSaveRequest:
|
|
359
|
+
"""Uses the remote plugins in the client to get the remote parameters."""
|
|
360
|
+
if not remote:
|
|
361
|
+
return {}
|
|
362
|
+
|
|
363
|
+
remote_config = _get_remote_configuration(remote, render_params=False)
|
|
364
|
+
remote_params_callable = get_remote_params_callable(remote_config.remote_url)
|
|
365
|
+
if not remote_params_callable:
|
|
366
|
+
return {}
|
|
367
|
+
|
|
368
|
+
remote.remote_params = remote_params_callable()
|
|
369
|
+
return {"remote": remote.to_dict()}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class CloudPodsClient(CloudPodsService):
|
|
373
|
+
"""
|
|
374
|
+
Client implementation of the Cloud Pods Service. It is a thin client that makes HTTP calls to the Cloud Pods
|
|
375
|
+
endpoints in the container.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
def __init__(self, interactive: bool = False) -> None:
|
|
379
|
+
self.interactive = interactive
|
|
380
|
+
|
|
381
|
+
def _process_response(self, response, message: str):
|
|
382
|
+
status = console.status(message)
|
|
383
|
+
status.start()
|
|
384
|
+
|
|
385
|
+
for event in response.iter_lines():
|
|
386
|
+
_event = json.loads(event)
|
|
387
|
+
if _event["event"] == "log":
|
|
388
|
+
status.update(_event["message"])
|
|
389
|
+
if _event["event"] == "service":
|
|
390
|
+
service, result, operation = (
|
|
391
|
+
_event["service"],
|
|
392
|
+
_event["status"],
|
|
393
|
+
_event["operation"],
|
|
394
|
+
)
|
|
395
|
+
message = (
|
|
396
|
+
f"{service}: {operation} succeeded"
|
|
397
|
+
if result == "ok"
|
|
398
|
+
else f"{service}: {operation} failed"
|
|
399
|
+
)
|
|
400
|
+
status.update(message)
|
|
401
|
+
elif _event["event"] == "completion":
|
|
402
|
+
status.stop()
|
|
403
|
+
if _event["status"] == "error":
|
|
404
|
+
raise Exception(_event.get("message"))
|
|
405
|
+
if _event["operation"] == "save":
|
|
406
|
+
pod_info = PodInfo(**_event["info"])
|
|
407
|
+
return pod_info
|
|
408
|
+
status.stop()
|
|
409
|
+
|
|
410
|
+
def save(
|
|
411
|
+
self,
|
|
412
|
+
pod_name: str,
|
|
413
|
+
attributes: CloudPodRemoteAttributes | None = None,
|
|
414
|
+
remote: RemoteConfigParams | None = None,
|
|
415
|
+
local: bool = False,
|
|
416
|
+
version: int | None = None,
|
|
417
|
+
secret: str | None = None,
|
|
418
|
+
) -> PodInfo:
|
|
419
|
+
url = f"{get_runtime_pods_endpoint(secret)}/{pod_name}?"
|
|
420
|
+
if local:
|
|
421
|
+
url += "&local=true"
|
|
422
|
+
if version:
|
|
423
|
+
url += f"&version={version}"
|
|
424
|
+
payload: PodSaveRequest = _get_remote_params_payload(remote)
|
|
425
|
+
payload.update({"attributes": attributes})
|
|
426
|
+
headers = _get_headers()
|
|
427
|
+
if secret:
|
|
428
|
+
headers[HEADER_POD_SECRET] = secret
|
|
429
|
+
|
|
430
|
+
response = requests.post(url=url, json=payload, headers=headers, stream=self.interactive)
|
|
431
|
+
if not response.ok:
|
|
432
|
+
_raise_exception_with_formatted_message(f"Unable to save pod {pod_name}", response)
|
|
433
|
+
|
|
434
|
+
pod_info = {}
|
|
435
|
+
if self.interactive:
|
|
436
|
+
pod_info = self._process_response(response, message=f"Saving Cloud Pod {pod_name}")
|
|
437
|
+
else:
|
|
438
|
+
for line in response.iter_lines():
|
|
439
|
+
line = json.loads(line)
|
|
440
|
+
if line["event"] == "pod_info":
|
|
441
|
+
pod_info = PodInfo(**line["extra"])
|
|
442
|
+
elif line["event"] == "exception":
|
|
443
|
+
raise Exception(line["message"])
|
|
444
|
+
return pod_info
|
|
445
|
+
|
|
446
|
+
def delete(
|
|
447
|
+
self,
|
|
448
|
+
pod_name: str,
|
|
449
|
+
remote: RemoteConfigParams | None = None,
|
|
450
|
+
delete_from_remote: bool = True,
|
|
451
|
+
):
|
|
452
|
+
url = f"{get_runtime_pods_endpoint()}/{pod_name}"
|
|
453
|
+
if not delete_from_remote:
|
|
454
|
+
url += "?local=true"
|
|
455
|
+
payload = _get_remote_params_payload(remote)
|
|
456
|
+
response = requests.delete(url=url, json=payload, headers=_get_headers())
|
|
457
|
+
if not response.ok:
|
|
458
|
+
_raise_exception_with_formatted_message(
|
|
459
|
+
f"Unable to delete Cloud Pod '{pod_name}'", response
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def load(
|
|
463
|
+
self,
|
|
464
|
+
pod_name: str,
|
|
465
|
+
remote: RemoteConfigParams | None = None,
|
|
466
|
+
version: int | None = None,
|
|
467
|
+
merge_strategy: str | None = None,
|
|
468
|
+
ignore_version_mismatches: bool = False,
|
|
469
|
+
secret: str | None = None,
|
|
470
|
+
) -> None:
|
|
471
|
+
# If we are in CI, we do not ask for a confirmation
|
|
472
|
+
if in_ci():
|
|
473
|
+
ignore_version_mismatches = True
|
|
474
|
+
remote_config = _get_remote_configuration(remote, render_params=False) if remote else None
|
|
475
|
+
is_custom_remote = remote_config and remote_config.scheme != DEFAULT_REMOTE_SCHEME
|
|
476
|
+
|
|
477
|
+
# At the moment, we do not store the information about the originating LocalStack version for additional
|
|
478
|
+
# remotes. Therefore, we skip here the compatibility check.
|
|
479
|
+
if not is_custom_remote and not ignore_version_mismatches:
|
|
480
|
+
pod_info = self._get_cloud_pods_info(pod_name)
|
|
481
|
+
ls_pod_version: str = self._get_localstack_pod_version(
|
|
482
|
+
pod_name=pod_name, version=version, cloud_pods_dict=pod_info
|
|
483
|
+
)
|
|
484
|
+
ls_version: str = get_ls_version_from_health()
|
|
485
|
+
if not is_compatible_version(ls_pod_version, ls_version) and not click.confirm(
|
|
486
|
+
f"This Cloud Pod was created with LocalStack {ls_pod_version} but you are running "
|
|
487
|
+
f"LocalStack {ls_version}. Cloud Pods might be incompatible across different LocalStack versions.\n"
|
|
488
|
+
f"Loading a Cloud Pod with mismatching version might lead to a corrupted state of the emulator. "
|
|
489
|
+
f"Do you want to continue?"
|
|
490
|
+
):
|
|
491
|
+
raise click.Abort("LocalStack version mismatch")
|
|
492
|
+
|
|
493
|
+
url = f"{get_runtime_pods_endpoint()}/{pod_name}"
|
|
494
|
+
query_args = {}
|
|
495
|
+
if version:
|
|
496
|
+
query_args["version"] = version
|
|
497
|
+
if merge_strategy:
|
|
498
|
+
query_args["merge"] = merge_strategy
|
|
499
|
+
if query_args:
|
|
500
|
+
url += f"?{parse.urlencode(query_args)}"
|
|
501
|
+
|
|
502
|
+
payload = _get_remote_params_payload(remote)
|
|
503
|
+
headers = _get_headers()
|
|
504
|
+
if secret:
|
|
505
|
+
headers[HEADER_POD_SECRET] = secret
|
|
506
|
+
response = requests.put(url=url, json=payload, headers=headers, stream=self.interactive)
|
|
507
|
+
if not response.ok:
|
|
508
|
+
_raise_exception_with_formatted_message(f"Unable to load pod {pod_name}", response)
|
|
509
|
+
|
|
510
|
+
if self.interactive:
|
|
511
|
+
self._process_response(response, message=f"Loading Cloud Pod {pod_name}")
|
|
512
|
+
|
|
513
|
+
def list(self, remote: RemoteConfigParams | None = None, creator: str | None = None) -> list:
|
|
514
|
+
payload = _get_remote_params_payload(remote)
|
|
515
|
+
url = get_runtime_pods_endpoint()
|
|
516
|
+
if creator:
|
|
517
|
+
url += f"?creator={creator}"
|
|
518
|
+
response = requests.get(url, json=payload, headers=_get_headers())
|
|
519
|
+
if not response.ok:
|
|
520
|
+
_raise_exception_with_formatted_message("Unable to list Cloud Pods", response)
|
|
521
|
+
return json.loads(response.content).get("cloudpods", [])
|
|
522
|
+
|
|
523
|
+
def get_versions(
|
|
524
|
+
self, pod_name: str, remote: RemoteConfigParams | None = None
|
|
525
|
+
) -> builtins.list:
|
|
526
|
+
"""
|
|
527
|
+
Returns the list of versions for a cloud pod on a given remote
|
|
528
|
+
:param pod_name: the name of the Cloud Pod
|
|
529
|
+
:param remote: the name of the remote storage. None equals to our platform
|
|
530
|
+
"""
|
|
531
|
+
payload = _get_remote_params_payload(remote)
|
|
532
|
+
response = requests.get(
|
|
533
|
+
url=f"{get_runtime_pods_endpoint()}/{pod_name}/versions",
|
|
534
|
+
json=payload,
|
|
535
|
+
headers=_get_headers(),
|
|
536
|
+
)
|
|
537
|
+
if response.status_code == 404:
|
|
538
|
+
raise Exception(f"Cloud Pod {pod_name} not found")
|
|
539
|
+
if not response.ok:
|
|
540
|
+
_raise_exception_with_formatted_message(
|
|
541
|
+
f"Unable to get versions for pod {pod_name}", response
|
|
542
|
+
)
|
|
543
|
+
return json.loads(response.content).get("versions", [])
|
|
544
|
+
|
|
545
|
+
def diff(
|
|
546
|
+
self,
|
|
547
|
+
pod_name: str,
|
|
548
|
+
remote: RemoteConfigParams | None = None,
|
|
549
|
+
version: int | None = None,
|
|
550
|
+
) -> DiffResult:
|
|
551
|
+
url = f"{get_runtime_pods_endpoint()}/{pod_name}/diff"
|
|
552
|
+
query_args = {}
|
|
553
|
+
if version:
|
|
554
|
+
query_args["version"] = version
|
|
555
|
+
url += f"?{parse.urlencode(query_args)}"
|
|
556
|
+
payload = _get_remote_params_payload(remote)
|
|
557
|
+
response = requests.get(
|
|
558
|
+
url=url,
|
|
559
|
+
json=payload,
|
|
560
|
+
headers=_get_headers(),
|
|
561
|
+
)
|
|
562
|
+
if not response.ok:
|
|
563
|
+
_raise_exception_with_formatted_message(
|
|
564
|
+
f"Unable to get diff for pod {pod_name}", response
|
|
565
|
+
)
|
|
566
|
+
return json.loads(response.content)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _get_remote_configuration(
|
|
570
|
+
params: RemoteConfigParams, render_params: bool = True
|
|
571
|
+
) -> RemoteConfig:
|
|
572
|
+
"""
|
|
573
|
+
Get the remote configuration given a remote URL.
|
|
574
|
+
:param params: remote parameters
|
|
575
|
+
:param render_params: whether to render the remote parameters into the remote URL
|
|
576
|
+
"""
|
|
577
|
+
remotes_client = CloudPodsRemotesClient()
|
|
578
|
+
try:
|
|
579
|
+
remote = remotes_client.get_remote(name=params.remote_name)
|
|
580
|
+
except Exception as e:
|
|
581
|
+
raise ClickException(
|
|
582
|
+
f"Error getting configuration for the remote {params.remote_name}"
|
|
583
|
+
) from e
|
|
584
|
+
remote_url = remote["remote_url"]
|
|
585
|
+
if render_params:
|
|
586
|
+
remote_url = params.render_url(remote_url)
|
|
587
|
+
LOG.debug("Remote configuration: %s", remote)
|
|
588
|
+
return RemoteConfig(remote_url=remote_url)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ------------
|
|
592
|
+
# CLI HELPERS
|
|
593
|
+
# ------------
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def get_runtime_pods_endpoint(passphrase: str | None = None) -> str:
|
|
597
|
+
"""
|
|
598
|
+
Returns the runtime endpoint for the pods api.
|
|
599
|
+
If a passphrase is provided, we enforce HTTPS. Moreover, we resolve the hostname and warn the user if LocalStack
|
|
600
|
+
is not running locally.
|
|
601
|
+
"""
|
|
602
|
+
if not passphrase:
|
|
603
|
+
return f"{config.external_service_url()}{API_PATH_PODS}"
|
|
604
|
+
|
|
605
|
+
edge_url = config.external_service_url(protocol="https")
|
|
606
|
+
if LOCALHOST_HOSTNAME not in edge_url:
|
|
607
|
+
LOG.warning(
|
|
608
|
+
"LocalStack is not running locally and we are sending the encryption passphrase to a different host!"
|
|
609
|
+
)
|
|
610
|
+
return f"{edge_url}{API_PATH_PODS}"
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class StateService:
|
|
614
|
+
"""Service for the local operations on Cloud Pods, i.e., exporting and importing a Cloud Pod to/from host."""
|
|
615
|
+
|
|
616
|
+
def export_pod(self, target: str, services: Optional[list[str]] = None) -> PodInfo: # noqa
|
|
617
|
+
"""
|
|
618
|
+
Export a Cloud Pod to the local file system. It does not export the product space (i.e., objects and YAML files
|
|
619
|
+
with information about the versions). It will save a zip file named as the pod's name in the given directory
|
|
620
|
+
(if existing).
|
|
621
|
+
:param target: the path on the local disk where the Cloud Pod will be saved
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
# check the path
|
|
625
|
+
_path = urlparse(target)
|
|
626
|
+
path: str = os.path.abspath(os.path.join(_path.netloc, _path.path))
|
|
627
|
+
parent_path = Path(path).parent.absolute()
|
|
628
|
+
if not os.path.exists(parent_path):
|
|
629
|
+
raise Exception(f"{parent_path} is not a valid path")
|
|
630
|
+
|
|
631
|
+
with open(path, "wb") as outfile:
|
|
632
|
+
metadata = write_state_zip_to_file(outfile, services)
|
|
633
|
+
|
|
634
|
+
yaml_dict: dict = get_environment_metadata()
|
|
635
|
+
yaml_dict["name"] = os.path.basename(target)
|
|
636
|
+
yaml_dict.update(metadata)
|
|
637
|
+
|
|
638
|
+
# add YAML file with some metadata inside the zip
|
|
639
|
+
with zipfile.ZipFile(file=path, mode="a") as zip_file:
|
|
640
|
+
zip_file.writestr(CLOUDPODS_METADATA_FILE, yaml.dump(yaml_dict))
|
|
641
|
+
return metadata
|
|
642
|
+
|
|
643
|
+
def import_pod(self, source: str, show_progress: bool = True) -> None: # noqa
|
|
644
|
+
"""
|
|
645
|
+
Load a Cloud Pod from the local file system into the LocalStack running instance.
|
|
646
|
+
:param source: the path on the local disk where the Cloud Pod is saved
|
|
647
|
+
:param show_progress: flag to show visual progress in the CLI;
|
|
648
|
+
"""
|
|
649
|
+
url = urlparse(source)
|
|
650
|
+
path = os.path.abspath(os.path.join(url.netloc, url.path))
|
|
651
|
+
if not os.path.exists(path):
|
|
652
|
+
raise Exception(f"Path {path} does not exist")
|
|
653
|
+
if not os.path.isfile(path):
|
|
654
|
+
raise Exception(f"Path {path} is not a file")
|
|
655
|
+
with open(path, mode="rb") as infile:
|
|
656
|
+
with zipfile.ZipFile(infile, "r") as zip_file:
|
|
657
|
+
yaml_metadata = read_metadata_from_pod(zip_file) or {}
|
|
658
|
+
|
|
659
|
+
services = yaml_metadata.get("services", [])
|
|
660
|
+
is_licensed_user: bool = get_environment_metadata().get("pro")
|
|
661
|
+
# we check if the Cloud Pod has been generated by a pro user, and we are trying to load it as a community one
|
|
662
|
+
if yaml_metadata.get("pro", False) and not is_licensed_user:
|
|
663
|
+
console.print(
|
|
664
|
+
"Warning: You are trying to load a Cloud Pod generated with a Pro license."
|
|
665
|
+
"The loaded state might be incomplete."
|
|
666
|
+
)
|
|
667
|
+
infile.seek(0)
|
|
668
|
+
load_local_state_from_open_zipfile(
|
|
669
|
+
file=infile, number_services=len(services), show_progress=show_progress
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def list_public_pods() -> list[str]:
|
|
674
|
+
"""List all the public pods"""
|
|
675
|
+
url = create_platform_url("public")
|
|
676
|
+
auth_header = auth.get_platform_auth_headers()
|
|
677
|
+
response = safe_requests.get(url, headers=auth_header)
|
|
678
|
+
if not response.ok:
|
|
679
|
+
raise Exception(to_str(response.content))
|
|
680
|
+
content = json.loads(response.content)
|
|
681
|
+
return [pod["pod_name"] for pod in content]
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
#####################
|
|
685
|
+
# Utility functions #
|
|
686
|
+
#####################
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@singledispatch
|
|
690
|
+
def read_metadata_from_pod(zip_file: zipfile.ZipFile) -> dict | None:
|
|
691
|
+
try:
|
|
692
|
+
yaml_metadata = yaml.safe_load(zip_file.read(CLOUDPODS_METADATA_FILE))
|
|
693
|
+
return yaml_metadata
|
|
694
|
+
except KeyError:
|
|
695
|
+
LOG.debug("No %s file in the archive", CLOUDPODS_METADATA_FILE)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@read_metadata_from_pod.register(bytes)
|
|
699
|
+
def _(zip_file: bytes) -> dict | None:
|
|
700
|
+
zip_file = zipfile.ZipFile(io.BytesIO(zip_file), "r")
|
|
701
|
+
return read_metadata_from_pod(zip_file)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
@read_metadata_from_pod.register(str)
|
|
705
|
+
def _(zip_file: str) -> dict | None:
|
|
706
|
+
with zipfile.ZipFile(zip_file) as zp:
|
|
707
|
+
return read_metadata_from_pod(zp)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def call_post_load_endpoint(content: bytes | IO[bytes], stream: bool) -> requests.Response:
|
|
711
|
+
"""Calls the endpoint to load a state into LocalStack.
|
|
712
|
+
:param content: the content to load
|
|
713
|
+
:param stream: flag for a chunked response
|
|
714
|
+
:return the response from the endpoint, if the status is ok.
|
|
715
|
+
:raise an Exception if a non-ok status is returned
|
|
716
|
+
"""
|
|
717
|
+
url = get_runtime_pods_endpoint()
|
|
718
|
+
try:
|
|
719
|
+
response = requests.post(url, data=content, timeout=POD_LOAD_CLI_TIMEOUT, stream=stream)
|
|
720
|
+
if not stream:
|
|
721
|
+
LOG.debug("Loaded services from local state file: %s", response.content)
|
|
722
|
+
except requests.exceptions.Timeout as e:
|
|
723
|
+
raise Exception(
|
|
724
|
+
"Timeout exceed for the pod load operation. To avoid this issue, try to increase the"
|
|
725
|
+
"value of the POD_LOAD_CLI_TIMEOUT configuration variable."
|
|
726
|
+
) from e
|
|
727
|
+
if not response.ok:
|
|
728
|
+
raise Exception(f"Unable to load LocalStack state via {url}")
|
|
729
|
+
return response
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def load_state_with_progress_bar(content: bytes | IO[bytes], number_services: int = 0) -> None:
|
|
733
|
+
result = call_post_load_endpoint(content, stream=True)
|
|
734
|
+
i = 0
|
|
735
|
+
with Progress() as progress:
|
|
736
|
+
load_task = progress.add_task("Loading state", total=number_services)
|
|
737
|
+
for line in result.iter_lines():
|
|
738
|
+
content = json.loads(line)
|
|
739
|
+
LOG.debug("Loaded service: %s", content)
|
|
740
|
+
service, status = content["service"], "✅" if content["status"] else "❌"
|
|
741
|
+
progress.log(f"{service}: {status}")
|
|
742
|
+
i += 1
|
|
743
|
+
progress.update(load_task, completed=i)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def load_local_state(content: bytes, number_services: int = 0, show_progress: bool = True) -> None:
|
|
747
|
+
"""
|
|
748
|
+
Call to the _localstack/pods endpoint to inject a Cloud Pod into the system.
|
|
749
|
+
:param number_services: the number of services contained in the state to load. Used to display a progress bar.
|
|
750
|
+
:param content: the content of the Cloud Pod to be loaded into LocalStack.
|
|
751
|
+
:param show_progress: flag to show visual progress in the CLI.
|
|
752
|
+
:raises Exception: if the call to the endpoint does not return an ok status code (less than 400) or if a timeout
|
|
753
|
+
exceeds.
|
|
754
|
+
"""
|
|
755
|
+
if show_progress:
|
|
756
|
+
load_state_with_progress_bar(content=content, number_services=number_services)
|
|
757
|
+
else:
|
|
758
|
+
call_post_load_endpoint(content=content, stream=False)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def load_local_state_from_open_zipfile(
|
|
762
|
+
file: IO[bytes], number_services: int = 0, show_progress: bool = False
|
|
763
|
+
) -> None:
|
|
764
|
+
if show_progress:
|
|
765
|
+
load_state_with_progress_bar(content=file, number_services=number_services)
|
|
766
|
+
else:
|
|
767
|
+
call_post_load_endpoint(content=file, stream=False)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def get_environment_metadata() -> dict:
|
|
771
|
+
endpoint = get_runtime_pods_endpoint()
|
|
772
|
+
url = f"{endpoint}/environment"
|
|
773
|
+
result = requests.get(url)
|
|
774
|
+
if not result.ok:
|
|
775
|
+
raise Exception(f"Unable to retrieve environment metadata from {url}")
|
|
776
|
+
return json.loads(result.content)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
# ---------------
|
|
780
|
+
# UTIL FUNCTIONS
|
|
781
|
+
# ---------------
|
|
782
|
+
def reset_state(services: list = None) -> None:
|
|
783
|
+
"""
|
|
784
|
+
Call the reset endpoint in the persistence plugin to reset the LocalStack container.
|
|
785
|
+
If a list of services is provided, it calls POST _localstack/state/<service>/reset for each service.
|
|
786
|
+
It not, it calls POST _localstack/state/reset that reset the entire LocalStack instance.
|
|
787
|
+
|
|
788
|
+
:param services: the subset of services to reset (optional)
|
|
789
|
+
"""
|
|
790
|
+
|
|
791
|
+
def _call_reset(_url: str) -> None:
|
|
792
|
+
response = requests.post(_url)
|
|
793
|
+
if not response.ok:
|
|
794
|
+
LOG.debug("Reset call to %s failed: status code %s", _url, response.status_code)
|
|
795
|
+
raise Exception("Failed to reset LocalStack")
|
|
796
|
+
|
|
797
|
+
if not services:
|
|
798
|
+
url = f"{config.external_service_url()}/_localstack/state/reset"
|
|
799
|
+
_call_reset(url)
|
|
800
|
+
return
|
|
801
|
+
|
|
802
|
+
for service in services:
|
|
803
|
+
url = f"{config.external_service_url()}/_localstack/state/{service}/reset"
|
|
804
|
+
_call_reset(url)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def get_ls_version_from_health() -> str:
|
|
808
|
+
"""Returns the localstack ext version fetched from the health end point."""
|
|
809
|
+
try:
|
|
810
|
+
health_url = f"{config.external_service_url()}/_localstack/health"
|
|
811
|
+
health_response = requests.get(health_url).json()
|
|
812
|
+
return health_response["version"]
|
|
813
|
+
except Exception:
|
|
814
|
+
return ""
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def create_platform_url(path: str | None = None, api_endpoint: str | None = None) -> str:
|
|
818
|
+
api_endpoint = api_endpoint or constants.API_ENDPOINT
|
|
819
|
+
base_url = f"{api_endpoint}/cloudpods"
|
|
820
|
+
if not path:
|
|
821
|
+
return base_url
|
|
822
|
+
path = path if path.startswith("/") else f"/{path}"
|
|
823
|
+
return f"{base_url}{path}"
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def is_compatible_version(version_one: str | None, version_two: str | None) -> bool:
|
|
827
|
+
"""Check if two given version are equal up to the patch level."""
|
|
828
|
+
|
|
829
|
+
# if we cannot parse the version, we assume that it is not compatible
|
|
830
|
+
if not version_one or not version_two:
|
|
831
|
+
return False
|
|
832
|
+
v1 = version.parse(version_one)
|
|
833
|
+
v2 = version.parse(version_two)
|
|
834
|
+
return v1.base_version == v2.base_version
|