cloudsmith-cli 1.20.0__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.
- cloudsmith_cli/__init__.py +10 -0
- cloudsmith_cli/__main__.py +8 -0
- cloudsmith_cli/cli/__init__.py +1 -0
- cloudsmith_cli/cli/command.py +160 -0
- cloudsmith_cli/cli/commands/__init__.py +33 -0
- cloudsmith_cli/cli/commands/auth.py +173 -0
- cloudsmith_cli/cli/commands/check.py +129 -0
- cloudsmith_cli/cli/commands/copy.py +98 -0
- cloudsmith_cli/cli/commands/credential_helper/__init__.py +39 -0
- cloudsmith_cli/cli/commands/credential_helper/docker.py +66 -0
- cloudsmith_cli/cli/commands/credential_helper/manage.py +299 -0
- cloudsmith_cli/cli/commands/delete.py +67 -0
- cloudsmith_cli/cli/commands/dependencies.py +108 -0
- cloudsmith_cli/cli/commands/docs.py +16 -0
- cloudsmith_cli/cli/commands/download.py +620 -0
- cloudsmith_cli/cli/commands/entitlements.py +819 -0
- cloudsmith_cli/cli/commands/help_.py +12 -0
- cloudsmith_cli/cli/commands/list_.py +317 -0
- cloudsmith_cli/cli/commands/login.py +99 -0
- cloudsmith_cli/cli/commands/logout.py +151 -0
- cloudsmith_cli/cli/commands/main.py +73 -0
- cloudsmith_cli/cli/commands/mcp.py +523 -0
- cloudsmith_cli/cli/commands/metadata.py +503 -0
- cloudsmith_cli/cli/commands/metrics/__init__.py +2 -0
- cloudsmith_cli/cli/commands/metrics/command.py +20 -0
- cloudsmith_cli/cli/commands/metrics/entitlements.py +148 -0
- cloudsmith_cli/cli/commands/metrics/packages.py +134 -0
- cloudsmith_cli/cli/commands/move.py +111 -0
- cloudsmith_cli/cli/commands/policy/__init__.py +3 -0
- cloudsmith_cli/cli/commands/policy/command.py +20 -0
- cloudsmith_cli/cli/commands/policy/deny.py +248 -0
- cloudsmith_cli/cli/commands/policy/license.py +335 -0
- cloudsmith_cli/cli/commands/policy/vulnerability.py +322 -0
- cloudsmith_cli/cli/commands/push.py +1323 -0
- cloudsmith_cli/cli/commands/quarantine.py +148 -0
- cloudsmith_cli/cli/commands/quota/__init__.py +2 -0
- cloudsmith_cli/cli/commands/quota/command.py +20 -0
- cloudsmith_cli/cli/commands/quota/history.py +122 -0
- cloudsmith_cli/cli/commands/quota/quota.py +107 -0
- cloudsmith_cli/cli/commands/repos.py +330 -0
- cloudsmith_cli/cli/commands/resync.py +90 -0
- cloudsmith_cli/cli/commands/status.py +92 -0
- cloudsmith_cli/cli/commands/tags.py +375 -0
- cloudsmith_cli/cli/commands/tokens.py +318 -0
- cloudsmith_cli/cli/commands/upstream.py +479 -0
- cloudsmith_cli/cli/commands/vulnerabilities.py +141 -0
- cloudsmith_cli/cli/commands/whoami.py +188 -0
- cloudsmith_cli/cli/config.py +635 -0
- cloudsmith_cli/cli/decorators.py +624 -0
- cloudsmith_cli/cli/exceptions.py +215 -0
- cloudsmith_cli/cli/metadata_common.py +146 -0
- cloudsmith_cli/cli/saml.py +109 -0
- cloudsmith_cli/cli/table.py +59 -0
- cloudsmith_cli/cli/types.py +14 -0
- cloudsmith_cli/cli/utils.py +267 -0
- cloudsmith_cli/cli/validators.py +378 -0
- cloudsmith_cli/cli/webserver.py +263 -0
- cloudsmith_cli/core/__init__.py +1 -0
- cloudsmith_cli/core/api/__init__.py +1 -0
- cloudsmith_cli/core/api/distros.py +31 -0
- cloudsmith_cli/core/api/entitlements.py +130 -0
- cloudsmith_cli/core/api/exceptions.py +57 -0
- cloudsmith_cli/core/api/files.py +131 -0
- cloudsmith_cli/core/api/init.py +109 -0
- cloudsmith_cli/core/api/metadata.py +217 -0
- cloudsmith_cli/core/api/metrics.py +78 -0
- cloudsmith_cli/core/api/orgs.py +201 -0
- cloudsmith_cli/core/api/packages.py +309 -0
- cloudsmith_cli/core/api/quota.py +64 -0
- cloudsmith_cli/core/api/rates.py +28 -0
- cloudsmith_cli/core/api/repos.py +81 -0
- cloudsmith_cli/core/api/status.py +27 -0
- cloudsmith_cli/core/api/upstreams.py +72 -0
- cloudsmith_cli/core/api/user.py +109 -0
- cloudsmith_cli/core/api/version.py +15 -0
- cloudsmith_cli/core/api/vulnerabilities.py +230 -0
- cloudsmith_cli/core/cache_utils.py +160 -0
- cloudsmith_cli/core/config.py +140 -0
- cloudsmith_cli/core/credentials/__init__.py +0 -0
- cloudsmith_cli/core/credentials/chain.py +69 -0
- cloudsmith_cli/core/credentials/models.py +44 -0
- cloudsmith_cli/core/credentials/oidc/__init__.py +6 -0
- cloudsmith_cli/core/credentials/oidc/cache.py +220 -0
- cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +122 -0
- cloudsmith_cli/core/credentials/oidc/detectors/aws.py +85 -0
- cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +70 -0
- cloudsmith_cli/core/credentials/oidc/detectors/base.py +26 -0
- cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +35 -0
- cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +40 -0
- cloudsmith_cli/core/credentials/oidc/detectors/generic.py +42 -0
- cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +64 -0
- cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +49 -0
- cloudsmith_cli/core/credentials/oidc/exchange.py +87 -0
- cloudsmith_cli/core/credentials/provider.py +17 -0
- cloudsmith_cli/core/credentials/providers/__init__.py +15 -0
- cloudsmith_cli/core/credentials/providers/cli_flag.py +24 -0
- cloudsmith_cli/core/credentials/providers/credentials_file.py +24 -0
- cloudsmith_cli/core/credentials/providers/env_var.py +24 -0
- cloudsmith_cli/core/credentials/providers/keyring_provider.py +59 -0
- cloudsmith_cli/core/credentials/providers/oidc_provider.py +115 -0
- cloudsmith_cli/core/download.py +594 -0
- cloudsmith_cli/core/keyring.py +171 -0
- cloudsmith_cli/core/mcp/__init__.py +0 -0
- cloudsmith_cli/core/mcp/data.py +17 -0
- cloudsmith_cli/core/mcp/server.py +786 -0
- cloudsmith_cli/core/pagination.py +131 -0
- cloudsmith_cli/core/ratelimits.py +88 -0
- cloudsmith_cli/core/rest.py +255 -0
- cloudsmith_cli/core/utils.py +95 -0
- cloudsmith_cli/core/version.py +20 -0
- cloudsmith_cli/credential_helpers/__init__.py +7 -0
- cloudsmith_cli/credential_helpers/backends.py +41 -0
- cloudsmith_cli/credential_helpers/common.py +111 -0
- cloudsmith_cli/credential_helpers/custom_domains.py +281 -0
- cloudsmith_cli/credential_helpers/docker/__init__.py +4 -0
- cloudsmith_cli/credential_helpers/docker/installer.py +349 -0
- cloudsmith_cli/credential_helpers/docker/runtime.py +117 -0
- cloudsmith_cli/credential_helpers/launchers.py +175 -0
- cloudsmith_cli/data/VERSION +1 -0
- cloudsmith_cli/data/config.ini +23 -0
- cloudsmith_cli/data/credentials.ini +14 -0
- cloudsmith_cli/templates/__init__.py +3 -0
- cloudsmith_cli/templates/auth_error.html +45 -0
- cloudsmith_cli/templates/auth_success.html +37 -0
- cloudsmith_cli-1.20.0.dist-info/METADATA +610 -0
- cloudsmith_cli-1.20.0.dist-info/RECORD +130 -0
- cloudsmith_cli-1.20.0.dist-info/WHEEL +5 -0
- cloudsmith_cli-1.20.0.dist-info/entry_points.txt +2 -0
- cloudsmith_cli-1.20.0.dist-info/licenses/LICENSE +201 -0
- cloudsmith_cli-1.20.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""CLI - Decorators."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from click.core import ParameterSource
|
|
8
|
+
|
|
9
|
+
from cloudsmith_cli.cli import validators
|
|
10
|
+
|
|
11
|
+
from ..core.api.init import initialise_api as _initialise_api
|
|
12
|
+
from ..core.credentials.chain import CredentialProviderChain
|
|
13
|
+
from ..core.credentials.models import CredentialContext
|
|
14
|
+
from ..core.credentials.oidc.detectors import (
|
|
15
|
+
disabled_detectors_from_env,
|
|
16
|
+
registered_detectors,
|
|
17
|
+
)
|
|
18
|
+
from ..core.mcp import server
|
|
19
|
+
from ..core.rest import create_requests_session as _create_session
|
|
20
|
+
from . import config, utils
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def report_retry(seconds, context=None):
|
|
24
|
+
if context == "retry-after":
|
|
25
|
+
click.echo()
|
|
26
|
+
click.echo(
|
|
27
|
+
"Request was throttled (429): Retrying after %(seconds)s second(s) ... "
|
|
28
|
+
% {"seconds": click.style(str(seconds), bold=True)}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _pop_boolean_flag(kwargs, name, invert=False):
|
|
33
|
+
"""Pop a boolean flag from kwargs, optionally inverting it."""
|
|
34
|
+
value = kwargs.pop(name)
|
|
35
|
+
if value is not None and invert:
|
|
36
|
+
value = not value
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def common_package_action_options(f):
|
|
41
|
+
"""Add common options for package actions."""
|
|
42
|
+
|
|
43
|
+
@click.option(
|
|
44
|
+
"-s",
|
|
45
|
+
"--skip-errors",
|
|
46
|
+
default=False,
|
|
47
|
+
is_flag=True,
|
|
48
|
+
help="Skip/ignore errors when copying packages.",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"-W",
|
|
52
|
+
"--no-wait-for-sync",
|
|
53
|
+
default=False,
|
|
54
|
+
is_flag=True,
|
|
55
|
+
help="Don't wait for package synchronisation to complete before exiting.",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"-I",
|
|
59
|
+
"--wait-interval",
|
|
60
|
+
default=5.0,
|
|
61
|
+
type=float,
|
|
62
|
+
show_default=True,
|
|
63
|
+
help=(
|
|
64
|
+
"The minimum time in seconds to wait between checking sync status after "
|
|
65
|
+
"uploading. This is cumulative, so that status checks happen with less "
|
|
66
|
+
"frequency over time, upto a maximum of 5 minutes of waiting."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
@click.option(
|
|
70
|
+
"--sync-attempts",
|
|
71
|
+
default=3,
|
|
72
|
+
type=int,
|
|
73
|
+
help="Number of times to attempt package synchronisation. If the "
|
|
74
|
+
"package fails the first time, the client will attempt to "
|
|
75
|
+
"automatically resynchronise it.",
|
|
76
|
+
)
|
|
77
|
+
@click.pass_context
|
|
78
|
+
@functools.wraps(f)
|
|
79
|
+
def wrapper(ctx, *args, **kwargs):
|
|
80
|
+
# pylint: disable=missing-docstring
|
|
81
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
82
|
+
|
|
83
|
+
return wrapper
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def common_cli_config_options(f):
|
|
87
|
+
"""Add common CLI config options to commands."""
|
|
88
|
+
|
|
89
|
+
@click.option(
|
|
90
|
+
"-C",
|
|
91
|
+
"--config-file",
|
|
92
|
+
envvar="CLOUDSMITH_CONFIG_FILE",
|
|
93
|
+
type=click.Path(dir_okay=True, exists=True, writable=False, resolve_path=True),
|
|
94
|
+
help="The path to your config.ini file.",
|
|
95
|
+
)
|
|
96
|
+
@click.option(
|
|
97
|
+
"--credentials-file",
|
|
98
|
+
envvar="CLOUDSMITH_CREDENTIALS_FILE",
|
|
99
|
+
type=click.Path(dir_okay=True, exists=True, writable=False, resolve_path=True),
|
|
100
|
+
help="The path to your credentials.ini file.",
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"-P",
|
|
104
|
+
"--profile",
|
|
105
|
+
default=None,
|
|
106
|
+
envvar="CLOUDSMITH_PROFILE",
|
|
107
|
+
help="The name of the profile to use for configuration.",
|
|
108
|
+
)
|
|
109
|
+
@click.pass_context
|
|
110
|
+
@functools.wraps(f)
|
|
111
|
+
def wrapper(ctx, *args, **kwargs):
|
|
112
|
+
# pylint: disable=missing-docstring
|
|
113
|
+
opts = config.get_or_create_options(ctx)
|
|
114
|
+
profile = kwargs.pop("profile") or ctx.meta.get("profile")
|
|
115
|
+
config_file = kwargs.pop("config_file") or ctx.meta.get("config_file")
|
|
116
|
+
creds_file = kwargs.pop("credentials_file") or ctx.meta.get("creds_file")
|
|
117
|
+
|
|
118
|
+
# Store in context for subcommands to inherit
|
|
119
|
+
ctx.meta["profile"] = profile
|
|
120
|
+
ctx.meta["config_file"] = config_file
|
|
121
|
+
ctx.meta["creds_file"] = creds_file
|
|
122
|
+
|
|
123
|
+
opts.load_config_file(path=config_file, profile=profile)
|
|
124
|
+
opts.load_creds_file(path=creds_file, profile=profile)
|
|
125
|
+
opts.api_key_from_file = opts.api_key
|
|
126
|
+
kwargs["opts"] = opts
|
|
127
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
128
|
+
|
|
129
|
+
return wrapper
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def common_cli_output_options(f):
|
|
133
|
+
"""Add common CLI output options to commands."""
|
|
134
|
+
|
|
135
|
+
@click.option(
|
|
136
|
+
"-d",
|
|
137
|
+
"--debug",
|
|
138
|
+
default=False,
|
|
139
|
+
is_flag=True,
|
|
140
|
+
help="Produce debug output during processing.",
|
|
141
|
+
)
|
|
142
|
+
@click.option(
|
|
143
|
+
"-F",
|
|
144
|
+
"--output-format",
|
|
145
|
+
default="pretty",
|
|
146
|
+
type=click.Choice(["pretty", "json", "pretty_json"]),
|
|
147
|
+
help="Determines how output is formatted. This is only supported by a "
|
|
148
|
+
"subset of the commands at the moment (e.g. list).",
|
|
149
|
+
)
|
|
150
|
+
@click.option(
|
|
151
|
+
"-v",
|
|
152
|
+
"--verbose",
|
|
153
|
+
is_flag=True,
|
|
154
|
+
default=False,
|
|
155
|
+
help="Produce more output during processing.",
|
|
156
|
+
)
|
|
157
|
+
@click.pass_context
|
|
158
|
+
@functools.wraps(f)
|
|
159
|
+
def wrapper(ctx, *args, **kwargs):
|
|
160
|
+
# pylint: disable=missing-docstring
|
|
161
|
+
opts = config.get_or_create_options(ctx)
|
|
162
|
+
opts.debug = kwargs.pop("debug")
|
|
163
|
+
opts.output = kwargs.pop("output_format")
|
|
164
|
+
opts.verbose = kwargs.pop("verbose")
|
|
165
|
+
kwargs["opts"] = opts
|
|
166
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
167
|
+
|
|
168
|
+
return wrapper
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def common_cli_list_options(f):
|
|
172
|
+
"""Add common list options to commands."""
|
|
173
|
+
|
|
174
|
+
@click.option(
|
|
175
|
+
"-l",
|
|
176
|
+
"--page-size",
|
|
177
|
+
default=30,
|
|
178
|
+
type=validators.IntOrWildcard(),
|
|
179
|
+
help="The amount of items to view per page for lists. Use '*' to show all results.",
|
|
180
|
+
callback=validators.validate_page_size,
|
|
181
|
+
)
|
|
182
|
+
@click.option(
|
|
183
|
+
"-p",
|
|
184
|
+
"--page",
|
|
185
|
+
default=1,
|
|
186
|
+
type=int,
|
|
187
|
+
help="The page to view for lists, where 1 is the first page",
|
|
188
|
+
callback=validators.validate_page,
|
|
189
|
+
)
|
|
190
|
+
@click.option(
|
|
191
|
+
"--page-all",
|
|
192
|
+
"--show-all",
|
|
193
|
+
default=False,
|
|
194
|
+
is_flag=True,
|
|
195
|
+
help="Show all results. Cannot be used with --page (-p) or --page-size (-l).",
|
|
196
|
+
) # Validation performed post-parse for order-independence.
|
|
197
|
+
@click.pass_context
|
|
198
|
+
@functools.wraps(f)
|
|
199
|
+
def wrapper(ctx, *args, **kwargs):
|
|
200
|
+
# pylint: disable=missing-docstring
|
|
201
|
+
opts = config.get_or_create_options(ctx)
|
|
202
|
+
|
|
203
|
+
# Handle wildcard page_size (converts to page_all behavior)
|
|
204
|
+
page_size = kwargs.get("page_size")
|
|
205
|
+
if page_size == -1:
|
|
206
|
+
kwargs["page_all"] = True
|
|
207
|
+
# Validate that wildcard isn't used with explicit --page
|
|
208
|
+
validators.enforce_page_all_exclusive(ctx, wildcard_used=True)
|
|
209
|
+
|
|
210
|
+
# Order-independent validation: perform after all options parsed.
|
|
211
|
+
page_all = kwargs.get("page_all")
|
|
212
|
+
if page_all:
|
|
213
|
+
validators.enforce_page_all_exclusive(ctx)
|
|
214
|
+
kwargs["opts"] = opts
|
|
215
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
216
|
+
|
|
217
|
+
return wrapper
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def common_api_auth_options(f):
|
|
221
|
+
"""Add common API authentication options to commands."""
|
|
222
|
+
|
|
223
|
+
@click.option(
|
|
224
|
+
"-k",
|
|
225
|
+
"--api-key",
|
|
226
|
+
hide_input=True,
|
|
227
|
+
envvar="CLOUDSMITH_API_KEY",
|
|
228
|
+
help="The API key for authenticating with the API.",
|
|
229
|
+
)
|
|
230
|
+
@click.pass_context
|
|
231
|
+
@functools.wraps(f)
|
|
232
|
+
def wrapper(ctx, *args, **kwargs):
|
|
233
|
+
# pylint: disable=missing-docstring
|
|
234
|
+
opts = config.get_or_create_options(ctx)
|
|
235
|
+
api_key = kwargs.pop("api_key")
|
|
236
|
+
|
|
237
|
+
source = ctx.get_parameter_source("api_key")
|
|
238
|
+
api_key_nonempty = api_key and api_key.strip()
|
|
239
|
+
if source == ParameterSource.COMMANDLINE and api_key_nonempty:
|
|
240
|
+
opts.api_key_from_flag = api_key
|
|
241
|
+
opts.api_key_from_env = None
|
|
242
|
+
elif source == ParameterSource.ENVIRONMENT and api_key_nonempty:
|
|
243
|
+
opts.api_key_from_flag = None
|
|
244
|
+
opts.api_key_from_env = api_key
|
|
245
|
+
else:
|
|
246
|
+
opts.api_key_from_flag = None
|
|
247
|
+
opts.api_key_from_env = None
|
|
248
|
+
|
|
249
|
+
# Keep opts.api_key populated for any code that still reads it directly.
|
|
250
|
+
if api_key:
|
|
251
|
+
opts.api_key = api_key
|
|
252
|
+
kwargs["opts"] = opts
|
|
253
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
254
|
+
|
|
255
|
+
return wrapper
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _parse_suffixes(value):
|
|
259
|
+
"""Split a CSV of trusted host suffixes into a normalised tuple."""
|
|
260
|
+
return tuple(
|
|
261
|
+
token.strip().lstrip(".") for token in (value or "").split(",") if token.strip()
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _guard_untrusted_endpoints(ctx, opts, host_suffixes, proxy_suffixes):
|
|
266
|
+
"""Reject api_host/api_proxy redirections from directory-relative config.
|
|
267
|
+
|
|
268
|
+
Enforcement applies only when the effective value originated from an
|
|
269
|
+
untrusted (relative) config file and was not supplied via a CLI flag or
|
|
270
|
+
environment variable. Both must match their allow-listed host suffixes;
|
|
271
|
+
api_proxy has no public default suffixes, so it is rejected unless one is
|
|
272
|
+
supplied via the environment.
|
|
273
|
+
"""
|
|
274
|
+
trusted_sources = (ParameterSource.COMMANDLINE, ParameterSource.ENVIRONMENT)
|
|
275
|
+
profile = ctx.meta.get("profile")
|
|
276
|
+
|
|
277
|
+
if ctx.get_parameter_source("api_host") not in trusted_sources:
|
|
278
|
+
relative_host = config.ConfigReader.read_relative_config_value(
|
|
279
|
+
"api_host", profile=profile
|
|
280
|
+
)
|
|
281
|
+
if relative_host and opts.api_host == relative_host:
|
|
282
|
+
validators.validate_untrusted_api_host(opts.api_host, host_suffixes)
|
|
283
|
+
|
|
284
|
+
if ctx.get_parameter_source("api_proxy") not in trusted_sources:
|
|
285
|
+
relative_proxy = config.ConfigReader.read_relative_config_value(
|
|
286
|
+
"api_proxy", profile=profile
|
|
287
|
+
)
|
|
288
|
+
if relative_proxy and opts.api_proxy == relative_proxy:
|
|
289
|
+
validators.validate_untrusted_api_proxy(opts.api_proxy, proxy_suffixes)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def initialise_session(f):
|
|
293
|
+
"""Create a shared HTTP session with proxy/SSL/user-agent settings."""
|
|
294
|
+
|
|
295
|
+
@click.option(
|
|
296
|
+
"--api-host", envvar="CLOUDSMITH_API_HOST", help="The API host to connect to."
|
|
297
|
+
)
|
|
298
|
+
@click.option(
|
|
299
|
+
"--api-proxy",
|
|
300
|
+
envvar="CLOUDSMITH_API_PROXY",
|
|
301
|
+
help="The API proxy to connect through.",
|
|
302
|
+
)
|
|
303
|
+
@click.option(
|
|
304
|
+
"-S",
|
|
305
|
+
"--without-api-ssl-verify",
|
|
306
|
+
default=None,
|
|
307
|
+
is_flag=True,
|
|
308
|
+
envvar="CLOUDSMITH_WITHOUT_API_SSL_VERIFY",
|
|
309
|
+
help="Don't verify the SSL connection for the API. This is dangerous and "
|
|
310
|
+
"should only be used in an old/broken environment that doesn't support the "
|
|
311
|
+
"same secure ciphers as Cloudsmith.",
|
|
312
|
+
)
|
|
313
|
+
@click.option(
|
|
314
|
+
"--api-user-agent",
|
|
315
|
+
envvar="CLOUDSMITH_API_USER_AGENT",
|
|
316
|
+
help="The user agent to use for requests.",
|
|
317
|
+
)
|
|
318
|
+
@click.option(
|
|
319
|
+
"--api-headers",
|
|
320
|
+
envvar="CLOUDSMITH_API_HEADERS",
|
|
321
|
+
help="A CSV list of extra headers (key=value) to send to the API.",
|
|
322
|
+
)
|
|
323
|
+
@click.option(
|
|
324
|
+
"--allowed-api-host-suffixes",
|
|
325
|
+
envvar="CLOUDSMITH_ALLOWED_API_HOST_SUFFIXES",
|
|
326
|
+
hidden=True,
|
|
327
|
+
help="Comma-separated extra trusted api_host suffixes (internal use).",
|
|
328
|
+
)
|
|
329
|
+
@click.option(
|
|
330
|
+
"--allowed-api-proxy-suffixes",
|
|
331
|
+
envvar="CLOUDSMITH_ALLOWED_API_PROXY_SUFFIXES",
|
|
332
|
+
hidden=True,
|
|
333
|
+
help="Comma-separated trusted api_proxy host suffixes (internal use).",
|
|
334
|
+
)
|
|
335
|
+
@click.pass_context
|
|
336
|
+
@functools.wraps(f)
|
|
337
|
+
def wrapper(ctx, *args, **kwargs):
|
|
338
|
+
# pylint: disable=missing-docstring
|
|
339
|
+
opts = config.get_or_create_options(ctx)
|
|
340
|
+
host_suffixes = _parse_suffixes(kwargs.pop("allowed_api_host_suffixes"))
|
|
341
|
+
proxy_suffixes = _parse_suffixes(kwargs.pop("allowed_api_proxy_suffixes"))
|
|
342
|
+
opts.api_host = kwargs.pop("api_host")
|
|
343
|
+
opts.api_proxy = kwargs.pop("api_proxy")
|
|
344
|
+
opts.api_ssl_verify = _pop_boolean_flag(
|
|
345
|
+
kwargs, "without_api_ssl_verify", invert=True
|
|
346
|
+
)
|
|
347
|
+
opts.api_user_agent = kwargs.pop("api_user_agent")
|
|
348
|
+
opts.api_headers = kwargs.pop("api_headers")
|
|
349
|
+
|
|
350
|
+
_guard_untrusted_endpoints(ctx, opts, host_suffixes, proxy_suffixes)
|
|
351
|
+
|
|
352
|
+
opts.session = _create_session(
|
|
353
|
+
proxy=opts.api_proxy,
|
|
354
|
+
ssl_verify=opts.api_ssl_verify,
|
|
355
|
+
user_agent=opts.api_user_agent,
|
|
356
|
+
headers=opts.api_headers,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
kwargs["opts"] = opts
|
|
360
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
361
|
+
|
|
362
|
+
return wrapper
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _parse_detector_ids(value):
|
|
366
|
+
"""Split a comma-separated detector id string into stripped, lowercased ids."""
|
|
367
|
+
return [
|
|
368
|
+
token.strip().lower() for token in (value or "").split(",") if token.strip()
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _config_disabled_detectors(value):
|
|
373
|
+
"""Detector ids disabled via the config-file ``oidc_disabled_detectors`` key."""
|
|
374
|
+
return frozenset(_parse_detector_ids(value))
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _warn_on_oidc_detector_controls(order, disabled):
|
|
378
|
+
"""Warn when the OIDC detector order/disable controls look misconfigured.
|
|
379
|
+
|
|
380
|
+
Advisory only — detection itself is resolved in the core detectors module;
|
|
381
|
+
these warnings surface user-facing mistakes (a typo'd id, or controls that
|
|
382
|
+
leave nothing enabled) without aborting the credential fallback chain.
|
|
383
|
+
"""
|
|
384
|
+
valid_ids = [detector_cls.id for detector_cls in registered_detectors()]
|
|
385
|
+
order_ids = _parse_detector_ids(order)
|
|
386
|
+
|
|
387
|
+
unknown_ids = [
|
|
388
|
+
identifier for identifier in order_ids if identifier not in valid_ids
|
|
389
|
+
]
|
|
390
|
+
if unknown_ids:
|
|
391
|
+
click.secho(
|
|
392
|
+
"OIDC: ignoring unknown detector id(s) in --oidc-detector-order: "
|
|
393
|
+
f"{', '.join(unknown_ids)}. Valid ids: {', '.join(valid_ids)}.",
|
|
394
|
+
fg="yellow",
|
|
395
|
+
err=True,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
candidate_ids = [i for i in order_ids if i in valid_ids] if order_ids else valid_ids
|
|
399
|
+
enabled_ids = [i for i in candidate_ids if i not in disabled]
|
|
400
|
+
if (order_ids or disabled) and not enabled_ids:
|
|
401
|
+
click.secho(
|
|
402
|
+
"OIDC: no detectors are enabled after applying the detector "
|
|
403
|
+
"order/disable controls; OIDC auto-discovery will be skipped.",
|
|
404
|
+
fg="yellow",
|
|
405
|
+
err=True,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def resolve_credentials(f):
|
|
410
|
+
"""Resolve credentials via the provider chain. Depends on initialise_session."""
|
|
411
|
+
|
|
412
|
+
@click.option(
|
|
413
|
+
"--oidc-audience",
|
|
414
|
+
envvar="CLOUDSMITH_OIDC_AUDIENCE",
|
|
415
|
+
help="The OIDC audience for token requests.",
|
|
416
|
+
)
|
|
417
|
+
@click.option(
|
|
418
|
+
"--oidc-org",
|
|
419
|
+
envvar="CLOUDSMITH_ORG",
|
|
420
|
+
help="The Cloudsmith organisation slug for OIDC token exchange.",
|
|
421
|
+
)
|
|
422
|
+
@click.option(
|
|
423
|
+
"--oidc-service-slug",
|
|
424
|
+
envvar="CLOUDSMITH_SERVICE_SLUG",
|
|
425
|
+
help="The Cloudsmith service slug for OIDC token exchange.",
|
|
426
|
+
)
|
|
427
|
+
@click.option(
|
|
428
|
+
"--oidc-discovery-disabled",
|
|
429
|
+
default=None,
|
|
430
|
+
is_flag=True,
|
|
431
|
+
envvar="CLOUDSMITH_OIDC_DISCOVERY_DISABLED",
|
|
432
|
+
help="Disable OIDC auto-discovery.",
|
|
433
|
+
)
|
|
434
|
+
@click.option(
|
|
435
|
+
"--oidc-detector-order",
|
|
436
|
+
envvar="CLOUDSMITH_OIDC_DETECTOR_ORDER",
|
|
437
|
+
help="Comma-separated OIDC detector ids to control which detectors "
|
|
438
|
+
"are considered and the order they are tried in.",
|
|
439
|
+
)
|
|
440
|
+
@click.pass_context
|
|
441
|
+
@functools.wraps(f)
|
|
442
|
+
def wrapper(ctx, *args, **kwargs):
|
|
443
|
+
# pylint: disable=missing-docstring
|
|
444
|
+
opts = config.get_or_create_options(ctx)
|
|
445
|
+
|
|
446
|
+
oidc_audience = kwargs.pop("oidc_audience")
|
|
447
|
+
oidc_org = kwargs.pop("oidc_org")
|
|
448
|
+
oidc_service_slug = kwargs.pop("oidc_service_slug")
|
|
449
|
+
oidc_discovery_disabled = _pop_boolean_flag(kwargs, "oidc_discovery_disabled")
|
|
450
|
+
oidc_detector_order = kwargs.pop("oidc_detector_order")
|
|
451
|
+
|
|
452
|
+
if oidc_audience:
|
|
453
|
+
opts.oidc_audience = oidc_audience
|
|
454
|
+
if oidc_org:
|
|
455
|
+
opts.oidc_org = oidc_org
|
|
456
|
+
if oidc_service_slug:
|
|
457
|
+
opts.oidc_service_slug = oidc_service_slug
|
|
458
|
+
if oidc_discovery_disabled:
|
|
459
|
+
opts.oidc_discovery_disabled = oidc_discovery_disabled
|
|
460
|
+
if oidc_detector_order:
|
|
461
|
+
opts.oidc_detector_order = oidc_detector_order
|
|
462
|
+
|
|
463
|
+
oidc_disabled_detectors = disabled_detectors_from_env(
|
|
464
|
+
os.environ
|
|
465
|
+
) | _config_disabled_detectors(opts.oidc_disabled_detectors)
|
|
466
|
+
_warn_on_oidc_detector_controls(
|
|
467
|
+
opts.oidc_detector_order, oidc_disabled_detectors
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
context = CredentialContext(
|
|
471
|
+
session=opts.session,
|
|
472
|
+
api_key_from_flag=opts.api_key_from_flag,
|
|
473
|
+
api_key_from_env=opts.api_key_from_env,
|
|
474
|
+
api_key_from_file=opts.api_key_from_file,
|
|
475
|
+
api_host=opts.api_host or "https://api.cloudsmith.io",
|
|
476
|
+
creds_file_path=ctx.meta.get("creds_file"),
|
|
477
|
+
profile=ctx.meta.get("profile"),
|
|
478
|
+
debug=opts.debug,
|
|
479
|
+
oidc_audience=opts.oidc_audience,
|
|
480
|
+
oidc_org=opts.oidc_org,
|
|
481
|
+
oidc_service_slug=opts.oidc_service_slug,
|
|
482
|
+
oidc_discovery_disabled=opts.oidc_discovery_disabled,
|
|
483
|
+
oidc_detector_order=opts.oidc_detector_order,
|
|
484
|
+
oidc_disabled_detectors=oidc_disabled_detectors,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
chain = CredentialProviderChain()
|
|
488
|
+
credential = chain.resolve(context)
|
|
489
|
+
|
|
490
|
+
if context.keyring_refresh_failed:
|
|
491
|
+
click.secho(
|
|
492
|
+
"An error occurred when attempting to refresh your SSO access token. "
|
|
493
|
+
"To refresh this session, run 'cloudsmith auth'",
|
|
494
|
+
fg="yellow",
|
|
495
|
+
err=True,
|
|
496
|
+
)
|
|
497
|
+
if credential:
|
|
498
|
+
click.secho(
|
|
499
|
+
"Falling back to API key authentication.",
|
|
500
|
+
fg="yellow",
|
|
501
|
+
err=True,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
opts.credential = credential
|
|
505
|
+
|
|
506
|
+
kwargs["opts"] = opts
|
|
507
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
508
|
+
|
|
509
|
+
return initialise_session(wrapper)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def initialise_api(f):
|
|
513
|
+
"""Initialise the Cloudsmith API for use. Depends on resolve_credentials."""
|
|
514
|
+
|
|
515
|
+
@click.option(
|
|
516
|
+
"-R",
|
|
517
|
+
"--without-rate-limit",
|
|
518
|
+
default=None,
|
|
519
|
+
is_flag=True,
|
|
520
|
+
help="Don't obey the suggested rate limit interval. The CLI will "
|
|
521
|
+
"otherwise automatically sleep between commands to ensure that you do "
|
|
522
|
+
"not hit the server-side rate limit.",
|
|
523
|
+
)
|
|
524
|
+
@click.option(
|
|
525
|
+
"--rate-limit-warning",
|
|
526
|
+
default=30,
|
|
527
|
+
help="When rate limiting, display information that it is happening "
|
|
528
|
+
"if wait interval is higher than this setting. By default no "
|
|
529
|
+
"information will be printed. Set to zero to always see it.",
|
|
530
|
+
)
|
|
531
|
+
@click.option(
|
|
532
|
+
"--error-retry-max",
|
|
533
|
+
default=6,
|
|
534
|
+
help="The maximum amount of times to retry on errors received from "
|
|
535
|
+
"the API, as determined by the --error-retry-codes parameter.",
|
|
536
|
+
)
|
|
537
|
+
@click.option(
|
|
538
|
+
"--error-retry-backoff",
|
|
539
|
+
default=1.0,
|
|
540
|
+
type=float,
|
|
541
|
+
help="The backoff factor determines how long to wait in seconds "
|
|
542
|
+
"between error retries. The backoff factor is multiplied by the "
|
|
543
|
+
"amount of retries so far. So if 1.0, then the wait is 1.0s then "
|
|
544
|
+
"2.0s, then 4.0s, and so forth.",
|
|
545
|
+
)
|
|
546
|
+
@click.option(
|
|
547
|
+
"--error-retry-codes",
|
|
548
|
+
default="429,500,502,503,504",
|
|
549
|
+
help="The status codes that when received from the API will cause "
|
|
550
|
+
"a retry (if --error-retry-max is > 0). By default this will be for "
|
|
551
|
+
"429, 500, 502, 503 and 504 error codes.",
|
|
552
|
+
)
|
|
553
|
+
@click.pass_context
|
|
554
|
+
@functools.wraps(f)
|
|
555
|
+
def wrapper(ctx, *args, **kwargs):
|
|
556
|
+
# pylint: disable=missing-docstring
|
|
557
|
+
opts = config.get_or_create_options(ctx)
|
|
558
|
+
opts.rate_limit = _pop_boolean_flag(kwargs, "without_rate_limit", invert=True)
|
|
559
|
+
opts.rate_limit_warning = kwargs.pop("rate_limit_warning")
|
|
560
|
+
opts.error_retry_max = kwargs.pop("error_retry_max")
|
|
561
|
+
opts.error_retry_backoff = kwargs.pop("error_retry_backoff")
|
|
562
|
+
opts.error_retry_codes = kwargs.pop("error_retry_codes")
|
|
563
|
+
opts.error_retry_cb = report_retry
|
|
564
|
+
|
|
565
|
+
def call_print_rate_limit_info_with_opts(rate_info):
|
|
566
|
+
utils.print_rate_limit_info(opts, rate_info)
|
|
567
|
+
|
|
568
|
+
opts.api_config = _initialise_api(
|
|
569
|
+
debug=opts.debug,
|
|
570
|
+
host=opts.api_host,
|
|
571
|
+
credential=opts.credential,
|
|
572
|
+
proxy=opts.api_proxy,
|
|
573
|
+
ssl_verify=opts.api_ssl_verify,
|
|
574
|
+
user_agent=opts.api_user_agent,
|
|
575
|
+
headers=opts.api_headers,
|
|
576
|
+
rate_limit=opts.rate_limit,
|
|
577
|
+
rate_limit_callback=call_print_rate_limit_info_with_opts,
|
|
578
|
+
error_retry_max=opts.error_retry_max,
|
|
579
|
+
error_retry_backoff=opts.error_retry_backoff,
|
|
580
|
+
error_retry_codes=opts.error_retry_codes,
|
|
581
|
+
error_retry_cb=opts.error_retry_cb,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
kwargs["opts"] = opts
|
|
585
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
586
|
+
|
|
587
|
+
return resolve_credentials(wrapper)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def initialise_mcp(f):
|
|
591
|
+
@click.option(
|
|
592
|
+
"-a",
|
|
593
|
+
"--all-tools",
|
|
594
|
+
default=False,
|
|
595
|
+
is_flag=True,
|
|
596
|
+
help="Show all tools",
|
|
597
|
+
)
|
|
598
|
+
@click.option(
|
|
599
|
+
"-D",
|
|
600
|
+
"--allow-destructive-tools",
|
|
601
|
+
default=False,
|
|
602
|
+
is_flag=True,
|
|
603
|
+
help="Allow destructive tools to be used",
|
|
604
|
+
)
|
|
605
|
+
@click.pass_context
|
|
606
|
+
@functools.wraps(f)
|
|
607
|
+
def wrapper(ctx, *args, **kwargs):
|
|
608
|
+
opts = kwargs.get("opts")
|
|
609
|
+
|
|
610
|
+
all_tools = kwargs.pop("all_tools")
|
|
611
|
+
allow_destructive_tools = kwargs.pop("allow_destructive_tools")
|
|
612
|
+
|
|
613
|
+
mcp_server = server.DynamicMCPServer(
|
|
614
|
+
api_config=opts.api_config,
|
|
615
|
+
debug_mode=opts.debug,
|
|
616
|
+
allow_destructive_tools=allow_destructive_tools,
|
|
617
|
+
allowed_tool_groups=opts.mcp_allowed_tool_groups,
|
|
618
|
+
allowed_tools=opts.mcp_allowed_tools,
|
|
619
|
+
force_all_tools=all_tools,
|
|
620
|
+
)
|
|
621
|
+
kwargs["mcp_server"] = mcp_server
|
|
622
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
623
|
+
|
|
624
|
+
return wrapper
|