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,1323 @@
|
|
|
1
|
+
"""CLI/Commands - Push packages."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-lines
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from ...core import utils as core_utils
|
|
14
|
+
from ...core.api.exceptions import ApiException
|
|
15
|
+
from ...core.api.files import (
|
|
16
|
+
CHUNK_SIZE,
|
|
17
|
+
multi_part_upload_file,
|
|
18
|
+
request_file_upload,
|
|
19
|
+
upload_file as api_upload_file,
|
|
20
|
+
validate_request_file_upload,
|
|
21
|
+
)
|
|
22
|
+
from ...core.api.metadata import (
|
|
23
|
+
create_metadata as api_create_metadata,
|
|
24
|
+
validate_metadata as api_validate_metadata,
|
|
25
|
+
)
|
|
26
|
+
from ...core.api.packages import (
|
|
27
|
+
create_package as api_create_package,
|
|
28
|
+
get_package_formats,
|
|
29
|
+
get_package_status,
|
|
30
|
+
validate_create_package as api_validate_create_package,
|
|
31
|
+
)
|
|
32
|
+
from .. import command, decorators, utils, validators
|
|
33
|
+
from ..exceptions import handle_api_exceptions
|
|
34
|
+
from ..metadata_common import (
|
|
35
|
+
MetadataContentError,
|
|
36
|
+
ResolvedMetadata,
|
|
37
|
+
attach_metadata_options,
|
|
38
|
+
default_metadata_source_identity,
|
|
39
|
+
require_metadata_content_type,
|
|
40
|
+
resolve_metadata_content,
|
|
41
|
+
source_label_for,
|
|
42
|
+
)
|
|
43
|
+
from ..types import ExpandPath
|
|
44
|
+
from ..utils import maybe_spinner
|
|
45
|
+
from .main import main
|
|
46
|
+
|
|
47
|
+
#: Env var that lets CI/CD wrappers (e.g. GHA) opt out of hard-failing the
|
|
48
|
+
#: push when push-time metadata attachment fails. Defaults to ``error`` so an
|
|
49
|
+
#: invalid metadata content aborts the upload (design requirement: metadata
|
|
50
|
+
#: pushes must surface failures by default). Set to ``0`` or ``warn`` to
|
|
51
|
+
#: downgrade failures to a warning and let the package upload regardless.
|
|
52
|
+
#: Equivalent settings exist as a CLI flag (``--on-metadata-failure``) and a
|
|
53
|
+
#: ``metadata_failure_mode`` key in ``config.ini``; precedence at resolution
|
|
54
|
+
#: time is flag > env > config > default.
|
|
55
|
+
METADATA_FAILURE_MODE_ENV = "CLOUDSMITH_METADATA_FAILURE_MODE"
|
|
56
|
+
METADATA_FAILURE_MODE_WARN = {"0", "warn"}
|
|
57
|
+
#: Click option dest names for the push-time metadata flags. Used by the
|
|
58
|
+
#: push handler to split metadata flags off from the package-create payload
|
|
59
|
+
#: kwargs (the API client would otherwise reject the unknown keys).
|
|
60
|
+
METADATA_KWARG_NAMES = (
|
|
61
|
+
"metadata_content_file",
|
|
62
|
+
"metadata_content",
|
|
63
|
+
"metadata_content_type",
|
|
64
|
+
"metadata_source_identity",
|
|
65
|
+
)
|
|
66
|
+
#: Click dest name for ``--on-metadata-failure``. Popped off the push kwargs
|
|
67
|
+
#: separately from the metadata payload kwargs so it does not leak into the
|
|
68
|
+
#: package-create API call.
|
|
69
|
+
METADATA_FAILURE_MODE_KWARG = "cli_metadata_failure_mode"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _metadata_failure_is_warn(opts=None):
|
|
73
|
+
"""Return True iff metadata failures should be downgraded to a warning.
|
|
74
|
+
|
|
75
|
+
Single source of truth for the failure-mode lookup so the validation and
|
|
76
|
+
attach paths cannot drift. Resolves in precedence order:
|
|
77
|
+
|
|
78
|
+
1. ``--on-metadata-failure`` flag (``opts.cli_metadata_failure_mode``)
|
|
79
|
+
2. ``$CLOUDSMITH_METADATA_FAILURE_MODE`` env var
|
|
80
|
+
3. ``metadata_failure_mode`` config key (``opts.metadata_failure_mode``)
|
|
81
|
+
4. Default — ``error``
|
|
82
|
+
|
|
83
|
+
``opts`` is optional so direct callers without a CLI context (legacy
|
|
84
|
+
tests) keep working on the env-var path.
|
|
85
|
+
"""
|
|
86
|
+
candidates = (
|
|
87
|
+
getattr(opts, "cli_metadata_failure_mode", None) if opts is not None else None,
|
|
88
|
+
os.environ.get(METADATA_FAILURE_MODE_ENV),
|
|
89
|
+
getattr(opts, "metadata_failure_mode", None) if opts is not None else None,
|
|
90
|
+
)
|
|
91
|
+
for value in candidates:
|
|
92
|
+
if value is None:
|
|
93
|
+
continue
|
|
94
|
+
return str(value).strip().lower() in METADATA_FAILURE_MODE_WARN
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _metadata_content_failure_info(exc):
|
|
99
|
+
info = {
|
|
100
|
+
"status": "content_invalid",
|
|
101
|
+
"error": str(exc),
|
|
102
|
+
}
|
|
103
|
+
if getattr(exc, "source_label", None):
|
|
104
|
+
info["source"] = exc.source_label
|
|
105
|
+
return info
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _warn_metadata_failure(failure_info):
|
|
109
|
+
click.secho(
|
|
110
|
+
"Metadata content is invalid: %(error)s" % failure_info,
|
|
111
|
+
fg="yellow",
|
|
112
|
+
err=True,
|
|
113
|
+
)
|
|
114
|
+
click.secho(
|
|
115
|
+
"Package upload will continue without metadata. Pass "
|
|
116
|
+
"``--on-metadata-failure error`` (or set the "
|
|
117
|
+
f"``metadata_failure_mode`` config key / ``${METADATA_FAILURE_MODE_ENV}`` "
|
|
118
|
+
"env var to ``error``) to fail the push instead.",
|
|
119
|
+
fg="yellow",
|
|
120
|
+
err=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_push_metadata_options(
|
|
125
|
+
*,
|
|
126
|
+
metadata_content_file=None,
|
|
127
|
+
metadata_content=None,
|
|
128
|
+
metadata_content_type=None,
|
|
129
|
+
metadata_source_identity=None,
|
|
130
|
+
opts=None,
|
|
131
|
+
):
|
|
132
|
+
"""Resolve push-time metadata flags once before package upload loops."""
|
|
133
|
+
if metadata_content_file is not None and metadata_content is not None:
|
|
134
|
+
raise click.UsageError(
|
|
135
|
+
"--metadata-content-file and --metadata-content are mutually exclusive."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
metadata_provided = (
|
|
139
|
+
metadata_content_file is not None or metadata_content is not None
|
|
140
|
+
)
|
|
141
|
+
if not metadata_provided:
|
|
142
|
+
if metadata_content_type or metadata_source_identity:
|
|
143
|
+
raise click.UsageError(
|
|
144
|
+
"Add --metadata-content-file or --metadata-content when using "
|
|
145
|
+
"--metadata-content-type or --metadata-source-identity."
|
|
146
|
+
)
|
|
147
|
+
return ResolvedMetadata(provided=False, content=None), None
|
|
148
|
+
|
|
149
|
+
require_metadata_content_type(
|
|
150
|
+
content_type=metadata_content_type,
|
|
151
|
+
content_provided=True,
|
|
152
|
+
option_name="--metadata-content-type",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
metadata = resolve_metadata_content(
|
|
157
|
+
content_file=metadata_content_file,
|
|
158
|
+
inline_content=metadata_content,
|
|
159
|
+
required=True,
|
|
160
|
+
file_option_name="--metadata-content-file",
|
|
161
|
+
content_option_name="--metadata-content",
|
|
162
|
+
)
|
|
163
|
+
except MetadataContentError as exc:
|
|
164
|
+
if not _metadata_failure_is_warn(opts):
|
|
165
|
+
raise
|
|
166
|
+
|
|
167
|
+
source_label = exc.source_label or source_label_for(metadata_content_file)
|
|
168
|
+
metadata = ResolvedMetadata(
|
|
169
|
+
provided=True,
|
|
170
|
+
content=None,
|
|
171
|
+
content_type=metadata_content_type,
|
|
172
|
+
source_identity=(
|
|
173
|
+
metadata_source_identity or default_metadata_source_identity()
|
|
174
|
+
),
|
|
175
|
+
content_file=metadata_content_file,
|
|
176
|
+
source_label=source_label,
|
|
177
|
+
)
|
|
178
|
+
return metadata, _metadata_content_failure_info(exc)
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
attach_metadata_options(
|
|
182
|
+
metadata,
|
|
183
|
+
content_type=metadata_content_type,
|
|
184
|
+
source_identity=metadata_source_identity,
|
|
185
|
+
),
|
|
186
|
+
None,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _handle_metadata_api_exception(ctx, opts, exc, context_msg, skip_errors=False):
|
|
191
|
+
"""Route metadata API failures through the standard API exception handler."""
|
|
192
|
+
with handle_api_exceptions(
|
|
193
|
+
ctx,
|
|
194
|
+
opts=opts,
|
|
195
|
+
context_msg=context_msg,
|
|
196
|
+
reraise_on_error=skip_errors,
|
|
197
|
+
):
|
|
198
|
+
raise exc
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _print_metadata_retry_hint(
|
|
202
|
+
opts,
|
|
203
|
+
owner,
|
|
204
|
+
repo,
|
|
205
|
+
slug,
|
|
206
|
+
metadata_content_file,
|
|
207
|
+
cli_content_type,
|
|
208
|
+
cli_source_identity,
|
|
209
|
+
reason="attach_failed",
|
|
210
|
+
):
|
|
211
|
+
"""Print a copy-paste ``cloudsmith metadata add`` line for failed attaches.
|
|
212
|
+
|
|
213
|
+
Skipped in JSON output mode — the envelope already carries slugs and
|
|
214
|
+
failure context, so CI can reconstruct the command without text parsing.
|
|
215
|
+
Skipped for inline ``--metadata-content`` payloads, since they are not
|
|
216
|
+
safely reproducible as a single shell line (multi-line / quoting / size).
|
|
217
|
+
|
|
218
|
+
``reason`` distinguishes a transient/policy attach failure (``"attach_failed"``,
|
|
219
|
+
where retrying the same payload may succeed) from a pre-validation
|
|
220
|
+
failure (``"validation_failed"``, where the payload itself is broken and
|
|
221
|
+
must be fixed first). Wording changes accordingly.
|
|
222
|
+
"""
|
|
223
|
+
if utils.should_use_stderr(opts):
|
|
224
|
+
return
|
|
225
|
+
# Skip when no file path (inline ``--metadata-content``) or stdin ("-"),
|
|
226
|
+
# since neither is reproducible as a single shell line.
|
|
227
|
+
if not metadata_content_file or metadata_content_file == "-":
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
parts = [
|
|
231
|
+
f"cloudsmith metadata add {shlex.quote(f'{owner}/{repo}/{slug}')}",
|
|
232
|
+
f" --file {shlex.quote(metadata_content_file)}",
|
|
233
|
+
]
|
|
234
|
+
if cli_source_identity:
|
|
235
|
+
parts.append(f" --source-identity {shlex.quote(cli_source_identity)}")
|
|
236
|
+
if cli_content_type:
|
|
237
|
+
parts.append(f" --content-type {shlex.quote(cli_content_type)}")
|
|
238
|
+
|
|
239
|
+
if reason == "validation_failed":
|
|
240
|
+
heading = "Fix the metadata content, then run:"
|
|
241
|
+
else:
|
|
242
|
+
heading = "Run this command to attach metadata:"
|
|
243
|
+
|
|
244
|
+
click.echo(err=True)
|
|
245
|
+
click.secho(heading, fg="yellow", err=True)
|
|
246
|
+
click.secho(" \\\n".join(parts), fg="yellow", err=True)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def validate_metadata_payload(
|
|
250
|
+
ctx,
|
|
251
|
+
opts,
|
|
252
|
+
content,
|
|
253
|
+
content_type,
|
|
254
|
+
source=None,
|
|
255
|
+
skip_errors=False,
|
|
256
|
+
):
|
|
257
|
+
"""Validate metadata against ``POST /v2/metadata/validate/`` pre-upload.
|
|
258
|
+
|
|
259
|
+
Runs before any file upload so a malformed payload does not produce an
|
|
260
|
+
orphan package. Returns ``None`` on success. Routes validation failure
|
|
261
|
+
through ``handle_api_exceptions`` by default so the push aborts before
|
|
262
|
+
any S3 traffic.
|
|
263
|
+
When ``$CLOUDSMITH_METADATA_FAILURE_MODE`` is ``warn``/``0`` it returns a
|
|
264
|
+
metadata-info dict instead so the caller can skip attachment but continue
|
|
265
|
+
the push.
|
|
266
|
+
|
|
267
|
+
``source`` is a human-readable label for the payload origin (file
|
|
268
|
+
basename, ``"stdin"``, ``"inline"``) — surfaced in the progress line so
|
|
269
|
+
users know which source is being validated.
|
|
270
|
+
"""
|
|
271
|
+
# pylint: disable=too-many-arguments
|
|
272
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
273
|
+
|
|
274
|
+
if source:
|
|
275
|
+
message = "Validating metadata content from {source} ... ".format(
|
|
276
|
+
source=click.style(source, bold=True),
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
message = "Validating metadata content ... "
|
|
280
|
+
|
|
281
|
+
click.echo(
|
|
282
|
+
message,
|
|
283
|
+
nl=False,
|
|
284
|
+
err=use_stderr,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
with maybe_spinner(opts):
|
|
289
|
+
api_validate_metadata(content=content, content_type=content_type)
|
|
290
|
+
except ApiException as exc:
|
|
291
|
+
http_status = getattr(exc, "status", None)
|
|
292
|
+
detail = (
|
|
293
|
+
getattr(exc, "detail", None)
|
|
294
|
+
or getattr(exc, "status_description", None)
|
|
295
|
+
or str(exc)
|
|
296
|
+
or "unknown error"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
click.secho("FAILED", fg="red", err=use_stderr)
|
|
300
|
+
|
|
301
|
+
if http_status is not None:
|
|
302
|
+
message = (
|
|
303
|
+
f"Metadata content failed validation (HTTP {http_status}): {detail}"
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
message = f"Metadata content failed validation: {detail}"
|
|
307
|
+
failure_info = {
|
|
308
|
+
"status": "validation_failed",
|
|
309
|
+
"http_status": http_status,
|
|
310
|
+
"error": detail,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if not _metadata_failure_is_warn(opts):
|
|
314
|
+
opts.push_metadata_info = failure_info
|
|
315
|
+
_handle_metadata_api_exception(
|
|
316
|
+
ctx,
|
|
317
|
+
opts,
|
|
318
|
+
exc,
|
|
319
|
+
context_msg=message,
|
|
320
|
+
skip_errors=skip_errors,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
click.secho(message, fg="yellow", err=True)
|
|
324
|
+
click.secho(
|
|
325
|
+
"Package upload will continue without metadata. Pass "
|
|
326
|
+
"``--on-metadata-failure error`` (or set the "
|
|
327
|
+
f"``metadata_failure_mode`` config key / ``${METADATA_FAILURE_MODE_ENV}`` "
|
|
328
|
+
"env var to ``error``) to fail the push instead.",
|
|
329
|
+
fg="yellow",
|
|
330
|
+
err=True,
|
|
331
|
+
)
|
|
332
|
+
return failure_info
|
|
333
|
+
|
|
334
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def attach_metadata_to_package(
|
|
339
|
+
ctx,
|
|
340
|
+
opts,
|
|
341
|
+
owner,
|
|
342
|
+
repo,
|
|
343
|
+
slug,
|
|
344
|
+
slug_perm,
|
|
345
|
+
content,
|
|
346
|
+
content_type,
|
|
347
|
+
source_identity,
|
|
348
|
+
skip_errors=False,
|
|
349
|
+
metadata_content_file=None,
|
|
350
|
+
cli_content_type=None,
|
|
351
|
+
cli_source_identity=None,
|
|
352
|
+
):
|
|
353
|
+
"""Attach a metadata entry to a freshly-created package.
|
|
354
|
+
|
|
355
|
+
Failure is fatal by default: the API error is reported and the push
|
|
356
|
+
exits non-zero so CI/CD pipelines surface broken SBOM/BuildInfo uploads
|
|
357
|
+
instead of silently shipping a package without metadata. Wrappers that
|
|
358
|
+
explicitly want the legacy non-fatal behaviour can set
|
|
359
|
+
``$CLOUDSMITH_METADATA_FAILURE_MODE`` to ``warn`` (or ``0``).
|
|
360
|
+
"""
|
|
361
|
+
# pylint: disable=too-many-arguments
|
|
362
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
363
|
+
|
|
364
|
+
click.echo(
|
|
365
|
+
"Attaching metadata to package %(slug)s ... "
|
|
366
|
+
% {"slug": click.style(slug_perm, bold=True)},
|
|
367
|
+
nl=False,
|
|
368
|
+
err=use_stderr,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
with maybe_spinner(opts):
|
|
373
|
+
entry = api_create_metadata(
|
|
374
|
+
slug_perm,
|
|
375
|
+
content=content,
|
|
376
|
+
content_type=content_type,
|
|
377
|
+
source_identity=source_identity,
|
|
378
|
+
)
|
|
379
|
+
except ApiException as exc:
|
|
380
|
+
click.secho("FAILED", fg="red", err=use_stderr)
|
|
381
|
+
|
|
382
|
+
http_status = getattr(exc, "status", None)
|
|
383
|
+
detail = (
|
|
384
|
+
getattr(exc, "detail", None)
|
|
385
|
+
or getattr(exc, "status_description", None)
|
|
386
|
+
or str(exc)
|
|
387
|
+
or "unknown error"
|
|
388
|
+
)
|
|
389
|
+
if http_status is not None:
|
|
390
|
+
message = (
|
|
391
|
+
f"Could not attach metadata to package {slug_perm} "
|
|
392
|
+
f"(HTTP {http_status}): {detail}"
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
message = f"Could not attach metadata to package {slug_perm}: {detail}"
|
|
396
|
+
failure_info = {
|
|
397
|
+
"status": "attach_failed",
|
|
398
|
+
"http_status": http_status,
|
|
399
|
+
"error": detail,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
hint_kwargs = {
|
|
403
|
+
"opts": opts,
|
|
404
|
+
"owner": owner,
|
|
405
|
+
"repo": repo,
|
|
406
|
+
"slug": slug,
|
|
407
|
+
"metadata_content_file": metadata_content_file,
|
|
408
|
+
"cli_content_type": cli_content_type,
|
|
409
|
+
"cli_source_identity": cli_source_identity,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if not _metadata_failure_is_warn(opts):
|
|
413
|
+
opts.push_metadata_info = failure_info
|
|
414
|
+
_print_metadata_retry_hint(**hint_kwargs)
|
|
415
|
+
_handle_metadata_api_exception(
|
|
416
|
+
ctx,
|
|
417
|
+
opts,
|
|
418
|
+
exc,
|
|
419
|
+
context_msg=message,
|
|
420
|
+
skip_errors=skip_errors,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
click.secho(message, fg="yellow", err=True)
|
|
424
|
+
click.secho(
|
|
425
|
+
"Package upload completed without metadata. Pass "
|
|
426
|
+
"``--on-metadata-failure error`` (or set the "
|
|
427
|
+
f"``metadata_failure_mode`` config key / ``${METADATA_FAILURE_MODE_ENV}`` "
|
|
428
|
+
"env var to ``error``) to fail the push instead. "
|
|
429
|
+
"Accepted warn-mode values are ``warn`` and ``0``.",
|
|
430
|
+
fg="yellow",
|
|
431
|
+
err=True,
|
|
432
|
+
)
|
|
433
|
+
_print_metadata_retry_hint(**hint_kwargs)
|
|
434
|
+
return failure_info
|
|
435
|
+
|
|
436
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
437
|
+
|
|
438
|
+
metadata_slug_perm = (entry or {}).get("slug_perm") or "?"
|
|
439
|
+
package_path = "{owner}/{repo}/{slug}".format(
|
|
440
|
+
owner=click.style(owner, fg="magenta"),
|
|
441
|
+
repo=click.style(repo, fg="magenta"),
|
|
442
|
+
slug=click.style(slug, fg="green"),
|
|
443
|
+
)
|
|
444
|
+
click.echo(
|
|
445
|
+
"Metadata attached: %(path)s/%(metadata)s"
|
|
446
|
+
% {
|
|
447
|
+
"path": package_path,
|
|
448
|
+
"metadata": click.style(metadata_slug_perm, bold=True),
|
|
449
|
+
},
|
|
450
|
+
err=use_stderr,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
"status": "attached",
|
|
455
|
+
"slug_perm": (entry or {}).get("slug_perm"),
|
|
456
|
+
"entry": entry or None,
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def validate_upload_file(ctx, opts, owner, repo, filepath, skip_errors):
|
|
461
|
+
"""Validate parameters for requesting a file upload."""
|
|
462
|
+
filename = click.format_filename(filepath)
|
|
463
|
+
basename = os.path.basename(filename)
|
|
464
|
+
|
|
465
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
466
|
+
|
|
467
|
+
click.echo(
|
|
468
|
+
"Checking %(filename)s file upload parameters ... "
|
|
469
|
+
% {"filename": click.style(basename, bold=True)},
|
|
470
|
+
nl=False,
|
|
471
|
+
err=use_stderr,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
context_msg = "Failed to validate upload parameters!"
|
|
475
|
+
with handle_api_exceptions(
|
|
476
|
+
ctx, opts=opts, context_msg=context_msg, reraise_on_error=skip_errors
|
|
477
|
+
):
|
|
478
|
+
with maybe_spinner(opts):
|
|
479
|
+
md5_checksum = validate_request_file_upload(
|
|
480
|
+
owner=owner, repo=repo, filepath=filename
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
484
|
+
|
|
485
|
+
return md5_checksum
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def upload_file(ctx, opts, owner, repo, filepath, skip_errors, md5_checksum):
|
|
489
|
+
"""Upload a package file via the API."""
|
|
490
|
+
filename = click.format_filename(filepath)
|
|
491
|
+
basename = os.path.basename(filename)
|
|
492
|
+
|
|
493
|
+
filesize = core_utils.get_file_size(filepath=filename)
|
|
494
|
+
projected_chunks = math.floor(filesize / CHUNK_SIZE) + 1
|
|
495
|
+
is_multi_part_upload = projected_chunks > 1
|
|
496
|
+
|
|
497
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
498
|
+
|
|
499
|
+
click.echo(
|
|
500
|
+
"Requesting file upload for %(filename)s ... "
|
|
501
|
+
% {"filename": click.style(basename, bold=True)},
|
|
502
|
+
nl=False,
|
|
503
|
+
err=use_stderr,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
context_msg = "Failed to request file upload!"
|
|
507
|
+
with handle_api_exceptions(
|
|
508
|
+
ctx, opts=opts, context_msg=context_msg, reraise_on_error=skip_errors
|
|
509
|
+
):
|
|
510
|
+
with maybe_spinner(opts):
|
|
511
|
+
identifier, upload_url, upload_fields = request_file_upload(
|
|
512
|
+
owner=owner,
|
|
513
|
+
repo=repo,
|
|
514
|
+
filepath=filename,
|
|
515
|
+
md5_checksum=md5_checksum,
|
|
516
|
+
is_multi_part_upload=is_multi_part_upload,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
520
|
+
|
|
521
|
+
context_msg = "Failed to upload file!"
|
|
522
|
+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
|
|
523
|
+
label = f"Uploading {click.style(basename, bold=True)}:"
|
|
524
|
+
|
|
525
|
+
if not is_multi_part_upload:
|
|
526
|
+
if use_stderr:
|
|
527
|
+
api_upload_file(
|
|
528
|
+
upload_url=upload_url,
|
|
529
|
+
upload_fields=upload_fields,
|
|
530
|
+
filepath=filename,
|
|
531
|
+
)
|
|
532
|
+
else:
|
|
533
|
+
# We can upload the whole file in one go.
|
|
534
|
+
with click.progressbar(
|
|
535
|
+
length=filesize,
|
|
536
|
+
label=label,
|
|
537
|
+
fill_char=click.style("#", fg="green"),
|
|
538
|
+
empty_char=click.style("-", fg="red"),
|
|
539
|
+
) as pb:
|
|
540
|
+
|
|
541
|
+
def progress_callback(monitor):
|
|
542
|
+
pb.update(monitor.bytes_read)
|
|
543
|
+
|
|
544
|
+
api_upload_file(
|
|
545
|
+
upload_url=upload_url,
|
|
546
|
+
upload_fields=upload_fields,
|
|
547
|
+
filepath=filename,
|
|
548
|
+
callback=progress_callback,
|
|
549
|
+
)
|
|
550
|
+
else:
|
|
551
|
+
if use_stderr:
|
|
552
|
+
multi_part_upload_file(
|
|
553
|
+
opts=opts,
|
|
554
|
+
upload_url=upload_url,
|
|
555
|
+
owner=owner,
|
|
556
|
+
repo=repo,
|
|
557
|
+
filepath=filename,
|
|
558
|
+
upload_id=identifier,
|
|
559
|
+
callback=lambda: None,
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
# The file is sufficiently large that we need to upload in chunks.
|
|
563
|
+
with click.progressbar(
|
|
564
|
+
length=projected_chunks,
|
|
565
|
+
label=label,
|
|
566
|
+
fill_char=click.style("#", fg="green"),
|
|
567
|
+
empty_char=click.style("-", fg="red"),
|
|
568
|
+
) as pb:
|
|
569
|
+
|
|
570
|
+
def progress_callback():
|
|
571
|
+
pb.update(1)
|
|
572
|
+
|
|
573
|
+
multi_part_upload_file(
|
|
574
|
+
opts=opts,
|
|
575
|
+
upload_url=upload_url,
|
|
576
|
+
owner=owner,
|
|
577
|
+
repo=repo,
|
|
578
|
+
filepath=filename,
|
|
579
|
+
callback=progress_callback,
|
|
580
|
+
upload_id=identifier,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
return identifier
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def validate_create_package(
|
|
587
|
+
ctx, opts, owner, repo, package_type, skip_errors, **kwargs
|
|
588
|
+
):
|
|
589
|
+
"""Check new package parameters via the API."""
|
|
590
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
591
|
+
|
|
592
|
+
click.echo(
|
|
593
|
+
"Checking %(package_type)s package upload parameters ... "
|
|
594
|
+
% {"package_type": click.style(package_type, bold=True)},
|
|
595
|
+
nl=False,
|
|
596
|
+
err=use_stderr,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
context_msg = "Failed to validate upload parameters!"
|
|
600
|
+
with handle_api_exceptions(
|
|
601
|
+
ctx, opts=opts, context_msg=context_msg, reraise_on_error=skip_errors
|
|
602
|
+
):
|
|
603
|
+
with maybe_spinner(opts):
|
|
604
|
+
api_validate_create_package(
|
|
605
|
+
package_format=package_type, owner=owner, repo=repo, **kwargs
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
609
|
+
return True
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def create_package(ctx, opts, owner, repo, package_type, skip_errors, **kwargs):
|
|
613
|
+
"""Create a new package via the API."""
|
|
614
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
615
|
+
|
|
616
|
+
click.echo(
|
|
617
|
+
"Creating a new %(package_type)s package ... "
|
|
618
|
+
% {"package_type": click.style(package_type, bold=True)},
|
|
619
|
+
nl=False,
|
|
620
|
+
err=use_stderr,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
context_msg = "Failed to create package!"
|
|
624
|
+
with handle_api_exceptions(
|
|
625
|
+
ctx, opts=opts, context_msg=context_msg, reraise_on_error=skip_errors
|
|
626
|
+
):
|
|
627
|
+
with maybe_spinner(opts):
|
|
628
|
+
slug_perm, slug = api_create_package(
|
|
629
|
+
package_format=package_type, owner=owner, repo=repo, **kwargs
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
633
|
+
|
|
634
|
+
click.echo(
|
|
635
|
+
"Created: %(owner)s/%(repo)s/%(slug)s (%(slug_perm)s)"
|
|
636
|
+
% {
|
|
637
|
+
"owner": click.style(owner, fg="magenta"),
|
|
638
|
+
"repo": click.style(repo, fg="magenta"),
|
|
639
|
+
"slug": click.style(slug, fg="green"),
|
|
640
|
+
"slug_perm": click.style(slug_perm, bold=True),
|
|
641
|
+
},
|
|
642
|
+
err=use_stderr,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
return slug_perm, slug
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def wait_for_package_sync(
|
|
649
|
+
ctx, opts, owner, repo, slug, wait_interval, skip_errors, attempts=3
|
|
650
|
+
):
|
|
651
|
+
"""Wait for a package to synchronise (or fail)."""
|
|
652
|
+
# pylint: disable=too-many-locals
|
|
653
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
654
|
+
|
|
655
|
+
attempts -= 1
|
|
656
|
+
click.echo(err=use_stderr)
|
|
657
|
+
label = f"Synchronising {click.style(slug, fg='green')}:"
|
|
658
|
+
|
|
659
|
+
status_str = "Waiting"
|
|
660
|
+
stage_str = None
|
|
661
|
+
|
|
662
|
+
def display_status(current):
|
|
663
|
+
"""Display current sync status."""
|
|
664
|
+
# pylint: disable=unused-argument
|
|
665
|
+
if not stage_str or "Unknown" in stage_str:
|
|
666
|
+
return status_str
|
|
667
|
+
return click.style(
|
|
668
|
+
f"{status_str} / {stage_str}",
|
|
669
|
+
fg="cyan",
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
start = datetime.now()
|
|
673
|
+
context_msg = "Failed to synchronise file!"
|
|
674
|
+
with handle_api_exceptions(
|
|
675
|
+
ctx, opts=opts, context_msg=context_msg, reraise_on_error=skip_errors
|
|
676
|
+
):
|
|
677
|
+
left = 100
|
|
678
|
+
last_progress = 0
|
|
679
|
+
total_wait_interval = max(1.0, wait_interval)
|
|
680
|
+
first = True
|
|
681
|
+
|
|
682
|
+
if use_stderr:
|
|
683
|
+
# When using stderr for logs, avoid an interactive progress bar and just poll for status.
|
|
684
|
+
while True:
|
|
685
|
+
res = get_package_status(owner, repo, slug)
|
|
686
|
+
ok, failed, _, _, _, _ = res
|
|
687
|
+
if ok or failed:
|
|
688
|
+
break
|
|
689
|
+
|
|
690
|
+
# Sleep if we are going to loop again
|
|
691
|
+
if not first:
|
|
692
|
+
time.sleep(total_wait_interval)
|
|
693
|
+
total_wait_interval = min(
|
|
694
|
+
300.0, total_wait_interval + wait_interval
|
|
695
|
+
)
|
|
696
|
+
first = False
|
|
697
|
+
|
|
698
|
+
else:
|
|
699
|
+
with click.progressbar(
|
|
700
|
+
length=left,
|
|
701
|
+
label=label,
|
|
702
|
+
fill_char=click.style("#", fg="green"),
|
|
703
|
+
empty_char=click.style("-", fg="red"),
|
|
704
|
+
item_show_func=display_status,
|
|
705
|
+
) as pb:
|
|
706
|
+
while True:
|
|
707
|
+
res = get_package_status(owner, repo, slug)
|
|
708
|
+
ok, failed, progress, status_str, stage_str, reason = res
|
|
709
|
+
progress = max(1, progress)
|
|
710
|
+
delta = progress - last_progress
|
|
711
|
+
pb.update(delta)
|
|
712
|
+
if delta > 0:
|
|
713
|
+
last_progress = progress
|
|
714
|
+
left -= delta
|
|
715
|
+
if ok or failed:
|
|
716
|
+
break
|
|
717
|
+
if first:
|
|
718
|
+
first = False
|
|
719
|
+
else:
|
|
720
|
+
# Sleep, but only after the first status call
|
|
721
|
+
time.sleep(total_wait_interval)
|
|
722
|
+
total_wait_interval = min(
|
|
723
|
+
300.0, total_wait_interval + wait_interval
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
if left > 0:
|
|
727
|
+
pb.update(left)
|
|
728
|
+
|
|
729
|
+
end = datetime.now()
|
|
730
|
+
seconds = (end - start).total_seconds()
|
|
731
|
+
|
|
732
|
+
click.echo(err=use_stderr)
|
|
733
|
+
|
|
734
|
+
if ok:
|
|
735
|
+
click.secho(
|
|
736
|
+
"Package synchronised successfully in %(seconds)s second(s)!"
|
|
737
|
+
% {"seconds": click.style(str(seconds), bold=True)},
|
|
738
|
+
fg="green",
|
|
739
|
+
err=use_stderr,
|
|
740
|
+
)
|
|
741
|
+
return
|
|
742
|
+
|
|
743
|
+
click.secho(
|
|
744
|
+
"Package failed to synchronise in %(seconds)s during stage: %(stage)s"
|
|
745
|
+
% {
|
|
746
|
+
"seconds": click.style(str(seconds), bold=True),
|
|
747
|
+
"stage": click.style(stage_str or "Unknown", fg="yellow"),
|
|
748
|
+
},
|
|
749
|
+
fg="red",
|
|
750
|
+
err=use_stderr,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
if reason:
|
|
754
|
+
click.secho(
|
|
755
|
+
f"Reason given: {click.style(reason, fg='yellow')}",
|
|
756
|
+
fg="red",
|
|
757
|
+
err=use_stderr,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# pylint: disable=fixme
|
|
761
|
+
# FIXME: The API should communicate "no retry" fails
|
|
762
|
+
if "package should be deleted" in reason and attempts > 1:
|
|
763
|
+
click.secho(
|
|
764
|
+
"This is not recoverable, so stopping further attempts!",
|
|
765
|
+
fg="red",
|
|
766
|
+
err=use_stderr,
|
|
767
|
+
)
|
|
768
|
+
click.echo(err=use_stderr)
|
|
769
|
+
attempts = 0
|
|
770
|
+
|
|
771
|
+
if attempts + 1 > 0:
|
|
772
|
+
# Show attempts upto and including zero attempts left
|
|
773
|
+
click.secho(
|
|
774
|
+
"Attempts left: %(left)s (%(action)s)"
|
|
775
|
+
% {
|
|
776
|
+
"left": click.style(str(attempts), bold=True),
|
|
777
|
+
"action": "trying again" if attempts > 0 else "giving up",
|
|
778
|
+
},
|
|
779
|
+
err=use_stderr,
|
|
780
|
+
)
|
|
781
|
+
click.echo(err=use_stderr)
|
|
782
|
+
|
|
783
|
+
if attempts > 0:
|
|
784
|
+
from .resync import resync_package
|
|
785
|
+
|
|
786
|
+
resync_package(
|
|
787
|
+
ctx=ctx,
|
|
788
|
+
opts=opts,
|
|
789
|
+
owner=owner,
|
|
790
|
+
repo=repo,
|
|
791
|
+
slug=slug,
|
|
792
|
+
skip_errors=skip_errors,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
wait_for_package_sync(
|
|
796
|
+
ctx=ctx,
|
|
797
|
+
opts=opts,
|
|
798
|
+
owner=owner,
|
|
799
|
+
repo=repo,
|
|
800
|
+
slug=slug,
|
|
801
|
+
wait_interval=wait_interval,
|
|
802
|
+
skip_errors=skip_errors,
|
|
803
|
+
attempts=attempts,
|
|
804
|
+
)
|
|
805
|
+
else:
|
|
806
|
+
ctx.exit(1)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def upload_files_and_create_package(
|
|
810
|
+
ctx,
|
|
811
|
+
opts,
|
|
812
|
+
package_type,
|
|
813
|
+
owner_repo,
|
|
814
|
+
dry_run,
|
|
815
|
+
no_wait_for_sync,
|
|
816
|
+
wait_interval,
|
|
817
|
+
skip_errors,
|
|
818
|
+
sync_attempts,
|
|
819
|
+
metadata_content_file=None,
|
|
820
|
+
metadata_content=None,
|
|
821
|
+
metadata_content_type=None,
|
|
822
|
+
metadata_source_identity=None,
|
|
823
|
+
metadata=None,
|
|
824
|
+
metadata_failure_info=None,
|
|
825
|
+
**kwargs,
|
|
826
|
+
):
|
|
827
|
+
"""Upload package files and create a new package."""
|
|
828
|
+
# pylint: disable=unused-argument,too-many-arguments,too-many-locals
|
|
829
|
+
owner, repo = owner_repo
|
|
830
|
+
|
|
831
|
+
# Reset push-time metadata state for this call. ``handle_api_exceptions``
|
|
832
|
+
# consults this attribute to surface validation/attach context in the
|
|
833
|
+
# JSON error envelope; an unset value would leak prior state on retries.
|
|
834
|
+
opts.push_metadata_info = None
|
|
835
|
+
|
|
836
|
+
# 0. Resolve push-time metadata before package work. The dynamic command
|
|
837
|
+
# handler resolves once for multi-file pushes so stdin is consumed once;
|
|
838
|
+
# direct callers can still pass the metadata flags for test coverage.
|
|
839
|
+
if metadata is None:
|
|
840
|
+
metadata, metadata_failure_info = resolve_push_metadata_options(
|
|
841
|
+
metadata_content_file=metadata_content_file,
|
|
842
|
+
metadata_content=metadata_content,
|
|
843
|
+
metadata_content_type=metadata_content_type,
|
|
844
|
+
metadata_source_identity=metadata_source_identity,
|
|
845
|
+
opts=opts,
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
should_attach_metadata = metadata.provided and metadata_failure_info is None
|
|
849
|
+
|
|
850
|
+
# Publish a warn-mode resolve failure on ``opts`` BEFORE the package
|
|
851
|
+
# validation call so the JSON error envelope still carries the metadata
|
|
852
|
+
# context if ``validate_create_package`` aborts the push (e.g. typo in
|
|
853
|
+
# --name/--version, bad repo, auth failure).
|
|
854
|
+
if metadata_failure_info is not None:
|
|
855
|
+
opts.push_metadata_info = metadata_failure_info
|
|
856
|
+
_warn_metadata_failure(metadata_failure_info)
|
|
857
|
+
|
|
858
|
+
# 1. Validate package create parameters. This runs before the metadata
|
|
859
|
+
# pre-validation so a typo in --name/--version fails fast without
|
|
860
|
+
# burning a /v2/metadata/validate/ round-trip first.
|
|
861
|
+
validate_create_package(
|
|
862
|
+
ctx=ctx,
|
|
863
|
+
opts=opts,
|
|
864
|
+
owner=owner,
|
|
865
|
+
repo=repo,
|
|
866
|
+
package_type=package_type,
|
|
867
|
+
skip_errors=skip_errors,
|
|
868
|
+
**kwargs,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
# 1b. Pre-validate metadata against the server-side schema endpoint so a
|
|
872
|
+
# malformed payload cannot produce an orphan package (the upload would
|
|
873
|
+
# succeed and only the attach would fail).
|
|
874
|
+
if metadata_failure_info is None and should_attach_metadata:
|
|
875
|
+
validation_failure = validate_metadata_payload(
|
|
876
|
+
ctx=ctx,
|
|
877
|
+
opts=opts,
|
|
878
|
+
content=metadata.content,
|
|
879
|
+
content_type=metadata.content_type,
|
|
880
|
+
source=metadata.source_label,
|
|
881
|
+
skip_errors=skip_errors,
|
|
882
|
+
)
|
|
883
|
+
if validation_failure is not None:
|
|
884
|
+
# Warn-mode validation failure: keep the push, drop the attach.
|
|
885
|
+
should_attach_metadata = False
|
|
886
|
+
opts.push_metadata_info = validation_failure
|
|
887
|
+
|
|
888
|
+
# 2. Validate file upload parameters
|
|
889
|
+
md5_checksums = {}
|
|
890
|
+
for k, v in kwargs.items():
|
|
891
|
+
if not v:
|
|
892
|
+
continue
|
|
893
|
+
|
|
894
|
+
# Handle a single file
|
|
895
|
+
if k.endswith("_file"):
|
|
896
|
+
md5_checksums[k] = validate_upload_file(
|
|
897
|
+
ctx=ctx,
|
|
898
|
+
opts=opts,
|
|
899
|
+
owner=owner,
|
|
900
|
+
repo=repo,
|
|
901
|
+
filepath=v,
|
|
902
|
+
skip_errors=skip_errors,
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
# Check if the key is "extra_files" (to handle multiple files)
|
|
906
|
+
if k == "extra_files" and isinstance(v, list):
|
|
907
|
+
md5_checksums[k] = [
|
|
908
|
+
validate_upload_file(
|
|
909
|
+
ctx=ctx,
|
|
910
|
+
opts=opts,
|
|
911
|
+
owner=owner,
|
|
912
|
+
repo=repo,
|
|
913
|
+
filepath=file,
|
|
914
|
+
skip_errors=skip_errors,
|
|
915
|
+
)
|
|
916
|
+
for file in v
|
|
917
|
+
]
|
|
918
|
+
|
|
919
|
+
if dry_run:
|
|
920
|
+
click.echo()
|
|
921
|
+
click.secho("You requested a dry run so skipping upload.", fg="yellow")
|
|
922
|
+
return
|
|
923
|
+
|
|
924
|
+
# 3. Upload any arguments that look like files
|
|
925
|
+
for k, v in kwargs.items():
|
|
926
|
+
if not v:
|
|
927
|
+
continue
|
|
928
|
+
|
|
929
|
+
# Handle a single file
|
|
930
|
+
if k.endswith("_file"):
|
|
931
|
+
kwargs[k] = upload_file(
|
|
932
|
+
ctx=ctx,
|
|
933
|
+
opts=opts,
|
|
934
|
+
owner=owner,
|
|
935
|
+
repo=repo,
|
|
936
|
+
filepath=v,
|
|
937
|
+
skip_errors=skip_errors,
|
|
938
|
+
md5_checksum=md5_checksums[k],
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
# Check if the key is "extra_files" (to handle multiple files)
|
|
942
|
+
if k == "extra_files" and isinstance(v, list):
|
|
943
|
+
kwargs[k] = [
|
|
944
|
+
upload_file(
|
|
945
|
+
ctx=ctx,
|
|
946
|
+
opts=opts,
|
|
947
|
+
owner=owner,
|
|
948
|
+
repo=repo,
|
|
949
|
+
filepath=file,
|
|
950
|
+
skip_errors=skip_errors,
|
|
951
|
+
md5_checksum=md5_checksums[k][idx],
|
|
952
|
+
)
|
|
953
|
+
for idx, file in enumerate(v)
|
|
954
|
+
]
|
|
955
|
+
|
|
956
|
+
# 4. Create the package with package files and additional arguments
|
|
957
|
+
slug_perm, slug = create_package(
|
|
958
|
+
ctx=ctx,
|
|
959
|
+
opts=opts,
|
|
960
|
+
owner=owner,
|
|
961
|
+
repo=repo,
|
|
962
|
+
package_type=package_type,
|
|
963
|
+
skip_errors=skip_errors,
|
|
964
|
+
**kwargs,
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
# 5. Attach push-time metadata, if provided AND it passed validation.
|
|
968
|
+
# Warn-mode metadata failures leave opts.push_metadata_info populated
|
|
969
|
+
# and should_attach_metadata=False; surface a retry hint now that we
|
|
970
|
+
# have the package slug. Skipped in JSON mode and for inline payloads.
|
|
971
|
+
if should_attach_metadata:
|
|
972
|
+
opts.push_metadata_info = attach_metadata_to_package(
|
|
973
|
+
ctx=ctx,
|
|
974
|
+
opts=opts,
|
|
975
|
+
owner=owner,
|
|
976
|
+
repo=repo,
|
|
977
|
+
slug=slug,
|
|
978
|
+
slug_perm=slug_perm,
|
|
979
|
+
content=metadata.content,
|
|
980
|
+
content_type=metadata.content_type,
|
|
981
|
+
source_identity=metadata.source_identity,
|
|
982
|
+
skip_errors=skip_errors,
|
|
983
|
+
metadata_content_file=metadata.content_file,
|
|
984
|
+
cli_content_type=metadata.content_type,
|
|
985
|
+
cli_source_identity=metadata_source_identity,
|
|
986
|
+
)
|
|
987
|
+
elif metadata.provided:
|
|
988
|
+
# Metadata resolution/validation already warned the user; the payload
|
|
989
|
+
# is broken so a straight retry would fail. Use the "fix first" hint.
|
|
990
|
+
_print_metadata_retry_hint(
|
|
991
|
+
opts=opts,
|
|
992
|
+
owner=owner,
|
|
993
|
+
repo=repo,
|
|
994
|
+
slug=slug,
|
|
995
|
+
metadata_content_file=metadata.content_file,
|
|
996
|
+
cli_content_type=metadata.content_type,
|
|
997
|
+
cli_source_identity=metadata_source_identity,
|
|
998
|
+
reason="validation_failed",
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
if no_wait_for_sync:
|
|
1002
|
+
return slug_perm, slug
|
|
1003
|
+
|
|
1004
|
+
# 6. (optionally) Wait for the package to synchronise
|
|
1005
|
+
wait_for_package_sync(
|
|
1006
|
+
ctx=ctx,
|
|
1007
|
+
opts=opts,
|
|
1008
|
+
owner=owner,
|
|
1009
|
+
repo=repo,
|
|
1010
|
+
slug=slug,
|
|
1011
|
+
wait_interval=wait_interval,
|
|
1012
|
+
skip_errors=skip_errors,
|
|
1013
|
+
attempts=sync_attempts,
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
return slug_perm, slug
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def create_push_handlers(): # noqa: C901
|
|
1020
|
+
"""Create a handler for upload per package format."""
|
|
1021
|
+
# pylint: disable=fixme
|
|
1022
|
+
# HACK: hacky territory - Dynamically generate a handler for each of the
|
|
1023
|
+
# package formats, until we have slightly more clever 'guess type'
|
|
1024
|
+
# handling. :-)
|
|
1025
|
+
handlers = create_push_handlers.handlers = {}
|
|
1026
|
+
context = create_push_handlers.context = get_package_formats()
|
|
1027
|
+
|
|
1028
|
+
for key, parameters in context.items():
|
|
1029
|
+
kwargs = parameters.copy()
|
|
1030
|
+
|
|
1031
|
+
# Remove standard arguments
|
|
1032
|
+
kwargs.pop("package_file")
|
|
1033
|
+
if "distribution" in parameters:
|
|
1034
|
+
has_distribution_param = True
|
|
1035
|
+
kwargs.pop("distribution")
|
|
1036
|
+
else:
|
|
1037
|
+
has_distribution_param = False
|
|
1038
|
+
|
|
1039
|
+
has_additional_params = len(kwargs) > 0
|
|
1040
|
+
|
|
1041
|
+
help_text = f"""
|
|
1042
|
+
Push/upload a new {key.capitalize()} package upstream.
|
|
1043
|
+
"""
|
|
1044
|
+
|
|
1045
|
+
if has_additional_params:
|
|
1046
|
+
help_text += """
|
|
1047
|
+
|
|
1048
|
+
PACKAGE_FILE: The main file to create the package from.
|
|
1049
|
+
"""
|
|
1050
|
+
else:
|
|
1051
|
+
help_text += """
|
|
1052
|
+
|
|
1053
|
+
PACKAGE_FILE: Any number of files to create packages from. Each
|
|
1054
|
+
file will result in a separate package.
|
|
1055
|
+
"""
|
|
1056
|
+
|
|
1057
|
+
if has_distribution_param:
|
|
1058
|
+
target_metavar = "OWNER/REPO/DISTRO/RELEASE"
|
|
1059
|
+
target_callback = validators.validate_owner_repo_distro
|
|
1060
|
+
help_text += """
|
|
1061
|
+
|
|
1062
|
+
OWNER/REPO/DISTRO/RELEASE: Specify the OWNER namespace (i.e.
|
|
1063
|
+
user or org), the REPO name where the package file will be uploaded
|
|
1064
|
+
to, and the DISTRO and RELEASE the package is for. All separated by
|
|
1065
|
+
a slash.
|
|
1066
|
+
|
|
1067
|
+
Example: 'your-org/awesome-repo/ubuntu/xenial'.
|
|
1068
|
+
"""
|
|
1069
|
+
else:
|
|
1070
|
+
target_metavar = "OWNER/REPO"
|
|
1071
|
+
target_callback = validators.validate_owner_repo
|
|
1072
|
+
help_text += """
|
|
1073
|
+
|
|
1074
|
+
OWNER/REPO: Specify the OWNER namespace (i.e. user or org), and the
|
|
1075
|
+
REPO name where the package file will be uploaded to. All separated
|
|
1076
|
+
by a slash.
|
|
1077
|
+
|
|
1078
|
+
Example: 'your-org/awesome-repo'.
|
|
1079
|
+
"""
|
|
1080
|
+
|
|
1081
|
+
@push.command(name=key, help=help_text)
|
|
1082
|
+
@decorators.common_cli_config_options
|
|
1083
|
+
@decorators.common_cli_output_options
|
|
1084
|
+
@decorators.common_package_action_options
|
|
1085
|
+
@decorators.common_api_auth_options
|
|
1086
|
+
@decorators.initialise_api
|
|
1087
|
+
@click.argument("owner_repo", metavar=target_metavar, callback=target_callback)
|
|
1088
|
+
@click.argument(
|
|
1089
|
+
"package_file",
|
|
1090
|
+
nargs=1 if has_additional_params else -1,
|
|
1091
|
+
type=ExpandPath(
|
|
1092
|
+
dir_okay=False, exists=True, writable=False, resolve_path=True
|
|
1093
|
+
),
|
|
1094
|
+
)
|
|
1095
|
+
@click.option(
|
|
1096
|
+
"-n",
|
|
1097
|
+
"--dry-run",
|
|
1098
|
+
default=False,
|
|
1099
|
+
is_flag=True,
|
|
1100
|
+
help="Execute in dry run mode (don't upload anything.)",
|
|
1101
|
+
)
|
|
1102
|
+
@click.option(
|
|
1103
|
+
"--metadata-content-file",
|
|
1104
|
+
"metadata_content_file",
|
|
1105
|
+
type=click.Path(
|
|
1106
|
+
exists=True,
|
|
1107
|
+
dir_okay=False,
|
|
1108
|
+
readable=True,
|
|
1109
|
+
resolve_path=True,
|
|
1110
|
+
allow_dash=True,
|
|
1111
|
+
),
|
|
1112
|
+
default=None,
|
|
1113
|
+
help=(
|
|
1114
|
+
"Read metadata content from a JSON file "
|
|
1115
|
+
"(for example, SBOM or BuildInfo). Use '-' for stdin. "
|
|
1116
|
+
"Content must be a JSON object. "
|
|
1117
|
+
"Mutually exclusive with --metadata-content. "
|
|
1118
|
+
"Metadata failures abort the push by default; pass "
|
|
1119
|
+
"--on-metadata-failure warn (or set the "
|
|
1120
|
+
"metadata_failure_mode config key / "
|
|
1121
|
+
"$CLOUDSMITH_METADATA_FAILURE_MODE env var to warn) to "
|
|
1122
|
+
"downgrade to a warning and keep the package upload."
|
|
1123
|
+
),
|
|
1124
|
+
)
|
|
1125
|
+
@click.option(
|
|
1126
|
+
"--metadata-content",
|
|
1127
|
+
"metadata_content",
|
|
1128
|
+
default=None,
|
|
1129
|
+
help=(
|
|
1130
|
+
"Set metadata content from inline JSON. Content must be a "
|
|
1131
|
+
"JSON object. "
|
|
1132
|
+
"Mutually exclusive with --metadata-content-file. "
|
|
1133
|
+
"Metadata failures abort the push by default; pass "
|
|
1134
|
+
"--on-metadata-failure warn (or set the "
|
|
1135
|
+
"metadata_failure_mode config key / "
|
|
1136
|
+
"$CLOUDSMITH_METADATA_FAILURE_MODE env var to warn) to "
|
|
1137
|
+
"downgrade to a warning and keep the package upload."
|
|
1138
|
+
),
|
|
1139
|
+
)
|
|
1140
|
+
@click.option(
|
|
1141
|
+
"--metadata-content-type",
|
|
1142
|
+
"metadata_content_type",
|
|
1143
|
+
default=None,
|
|
1144
|
+
help=(
|
|
1145
|
+
"Content type for metadata content "
|
|
1146
|
+
"(for example, 'application/vnd.jfrog.buildinfo+json'). "
|
|
1147
|
+
"Required when metadata content is supplied and determines "
|
|
1148
|
+
"the schema used for validation."
|
|
1149
|
+
),
|
|
1150
|
+
)
|
|
1151
|
+
@click.option(
|
|
1152
|
+
"--metadata-source-identity",
|
|
1153
|
+
"metadata_source_identity",
|
|
1154
|
+
default=None,
|
|
1155
|
+
help=(
|
|
1156
|
+
"Identifier for the metadata source. "
|
|
1157
|
+
"Defaults to 'cloudsmith-cli@<version>'."
|
|
1158
|
+
),
|
|
1159
|
+
)
|
|
1160
|
+
@click.option(
|
|
1161
|
+
"--on-metadata-failure",
|
|
1162
|
+
METADATA_FAILURE_MODE_KWARG,
|
|
1163
|
+
type=click.Choice(["error", "warn"]),
|
|
1164
|
+
default=None,
|
|
1165
|
+
help=(
|
|
1166
|
+
"How to handle push-time metadata failures. 'error' "
|
|
1167
|
+
"(default) aborts the push so CI/CD surfaces broken "
|
|
1168
|
+
"SBOM/BuildInfo uploads; 'warn' downgrades to a warning "
|
|
1169
|
+
"and lets the package upload regardless. Overrides the "
|
|
1170
|
+
"$CLOUDSMITH_METADATA_FAILURE_MODE env var and the "
|
|
1171
|
+
"'metadata_failure_mode' config key for this push."
|
|
1172
|
+
),
|
|
1173
|
+
)
|
|
1174
|
+
@click.pass_context
|
|
1175
|
+
def push_handler(ctx, *args, **kwargs):
|
|
1176
|
+
"""Handle upload for a specific package format."""
|
|
1177
|
+
opts = kwargs.get("opts")
|
|
1178
|
+
parameters = context.get(ctx.info_name)
|
|
1179
|
+
kwargs["package_type"] = ctx.info_name
|
|
1180
|
+
|
|
1181
|
+
owner_repo = kwargs.pop("owner_repo")
|
|
1182
|
+
if "distribution" in parameters:
|
|
1183
|
+
kwargs["distribution"] = "/".join(owner_repo[2:])
|
|
1184
|
+
owner_repo = owner_repo[0:2]
|
|
1185
|
+
kwargs["owner_repo"] = owner_repo
|
|
1186
|
+
|
|
1187
|
+
# Metadata flags are not part of the package-create payload, so
|
|
1188
|
+
# pop them and forward them as explicit kwargs so they don't leak
|
|
1189
|
+
# into validate_create_package() / create_package().
|
|
1190
|
+
metadata_kwargs = {
|
|
1191
|
+
key: kwargs.pop(key, None) for key in METADATA_KWARG_NAMES
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
# ``--on-metadata-failure`` is also not a package-create kwarg;
|
|
1195
|
+
# publish it onto opts so the failure-mode helper can prefer it
|
|
1196
|
+
# over env/config without an explicit thread through every call.
|
|
1197
|
+
cli_failure_mode = kwargs.pop(METADATA_FAILURE_MODE_KWARG, None)
|
|
1198
|
+
if cli_failure_mode is not None:
|
|
1199
|
+
opts.cli_metadata_failure_mode = cli_failure_mode
|
|
1200
|
+
|
|
1201
|
+
package_files = kwargs.pop("package_file")
|
|
1202
|
+
if not isinstance(package_files, tuple):
|
|
1203
|
+
package_files = (package_files,)
|
|
1204
|
+
|
|
1205
|
+
# Reject multi-file push combined with metadata flags. A single
|
|
1206
|
+
# metadata payload semantically belongs to one package; silently
|
|
1207
|
+
# fanning it out across N packages (and validating + attaching it
|
|
1208
|
+
# N times) is almost never what the user wants. Force them to
|
|
1209
|
+
# push files individually with metadata, or drop the flags.
|
|
1210
|
+
metadata_flags_set = any(
|
|
1211
|
+
metadata_kwargs.get(k) for k in METADATA_KWARG_NAMES
|
|
1212
|
+
)
|
|
1213
|
+
if len(package_files) > 1 and metadata_flags_set:
|
|
1214
|
+
raise click.UsageError(
|
|
1215
|
+
"Metadata flags (--metadata-content-file, --metadata-content, "
|
|
1216
|
+
"--metadata-content-type, --metadata-source-identity) cannot "
|
|
1217
|
+
"be combined with multiple package files. Push files "
|
|
1218
|
+
"individually when attaching metadata."
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
metadata, metadata_failure_info = resolve_push_metadata_options(
|
|
1222
|
+
**metadata_kwargs, opts=opts
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
results = []
|
|
1226
|
+
for package_file in package_files:
|
|
1227
|
+
kwargs["package_file"] = package_file
|
|
1228
|
+
|
|
1229
|
+
try:
|
|
1230
|
+
click.echo(err=utils.should_use_stderr(opts))
|
|
1231
|
+
res = upload_files_and_create_package(
|
|
1232
|
+
ctx,
|
|
1233
|
+
*args,
|
|
1234
|
+
**kwargs,
|
|
1235
|
+
**metadata_kwargs,
|
|
1236
|
+
metadata=metadata,
|
|
1237
|
+
metadata_failure_info=metadata_failure_info,
|
|
1238
|
+
)
|
|
1239
|
+
if res:
|
|
1240
|
+
# ``upload_files_and_create_package`` resets and then
|
|
1241
|
+
# populates ``opts.push_metadata_info`` on every call,
|
|
1242
|
+
# so reading it here always reflects this iteration.
|
|
1243
|
+
results.append((res, opts.push_metadata_info))
|
|
1244
|
+
except ApiException:
|
|
1245
|
+
click.secho(
|
|
1246
|
+
"Skipping error and moving on.",
|
|
1247
|
+
fg="yellow",
|
|
1248
|
+
err=utils.should_use_stderr(opts),
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
click.echo(err=utils.should_use_stderr(opts))
|
|
1252
|
+
|
|
1253
|
+
if utils.should_use_stderr(opts):
|
|
1254
|
+
data = []
|
|
1255
|
+
for (slug_perm, slug), metadata_info in results:
|
|
1256
|
+
entry = {
|
|
1257
|
+
"slug_perm": slug_perm,
|
|
1258
|
+
"slug": slug,
|
|
1259
|
+
"status": "OK", # Assuming success if we got here
|
|
1260
|
+
}
|
|
1261
|
+
if metadata_info is not None:
|
|
1262
|
+
entry["metadata_attachment"] = metadata_info
|
|
1263
|
+
data.append(entry)
|
|
1264
|
+
|
|
1265
|
+
if len(data) == 1:
|
|
1266
|
+
utils.maybe_print_as_json(opts, data[0])
|
|
1267
|
+
else:
|
|
1268
|
+
utils.maybe_print_as_json(opts, data)
|
|
1269
|
+
|
|
1270
|
+
# Add any additional arguments
|
|
1271
|
+
for k, info in kwargs.items():
|
|
1272
|
+
option_kwargs = {}
|
|
1273
|
+
option_name_fmt = "--%(key)s"
|
|
1274
|
+
|
|
1275
|
+
if k.endswith("_file"):
|
|
1276
|
+
# Treat parameters that end with _file as uploadable filepaths.
|
|
1277
|
+
option_kwargs["type"] = ExpandPath(
|
|
1278
|
+
dir_okay=False, exists=True, writable=False, resolve_path=True
|
|
1279
|
+
)
|
|
1280
|
+
elif k == "extra_files":
|
|
1281
|
+
# Handle multiple files for extra_files parameter.
|
|
1282
|
+
option_kwargs["type"] = str
|
|
1283
|
+
option_kwargs["multiple"] = True
|
|
1284
|
+
option_kwargs["callback"] = validators.validate_extra_files_parameter
|
|
1285
|
+
info["help"] = (
|
|
1286
|
+
info["help"] + " Accepts a comma-separated list of values."
|
|
1287
|
+
)
|
|
1288
|
+
elif info["type"] == "bool":
|
|
1289
|
+
option_name_fmt = "--%(key)s/--no-%(key)s"
|
|
1290
|
+
option_kwargs["is_flag"] = True
|
|
1291
|
+
else:
|
|
1292
|
+
option_kwargs["type"] = str
|
|
1293
|
+
|
|
1294
|
+
if k == "republish":
|
|
1295
|
+
# None is required to default upload republish settings to the repo republish settings
|
|
1296
|
+
option_kwargs["default"] = None
|
|
1297
|
+
|
|
1298
|
+
option_name = option_name_fmt % {"key": k.replace("_", "-")}
|
|
1299
|
+
decorator = click.option(
|
|
1300
|
+
option_name,
|
|
1301
|
+
required=info["required"],
|
|
1302
|
+
help=info["help"],
|
|
1303
|
+
**option_kwargs,
|
|
1304
|
+
)
|
|
1305
|
+
push_handler = decorator(push_handler)
|
|
1306
|
+
|
|
1307
|
+
handlers[key] = push_handler
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
@main.group(cls=command.AliasGroup, aliases=["upload", "deploy"])
|
|
1311
|
+
@click.pass_context
|
|
1312
|
+
def push(ctx): # pylint: disable=unused-argument
|
|
1313
|
+
"""
|
|
1314
|
+
Push (upload) a new package to a repository.
|
|
1315
|
+
|
|
1316
|
+
At the moment you need to specify the package format (see below) of
|
|
1317
|
+
the package you're uploading. Each package format may have additional
|
|
1318
|
+
options/parameters that are specific to that package format (e.g. the
|
|
1319
|
+
Maven backend has the concepts of artifact and group IDs).
|
|
1320
|
+
"""
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
create_push_handlers()
|