python-jcli 0.1.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.
@@ -0,0 +1,210 @@
1
+ """Jenkins Credential management commands."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import click
8
+
9
+ from jcli.cli_helpers import get_client, get_formatter
10
+ from jcli.sdk.credential import (
11
+ create_credential,
12
+ delete_credential,
13
+ get_credential,
14
+ list_credentials,
15
+ update_credential,
16
+ )
17
+
18
+
19
+ def _display_result(ctx: click.Context, result: Any) -> None:
20
+ """Display result using the configured formatter.
21
+
22
+ If result is a dict with a ``credentials`` key, render as a table.
23
+ If result is a dict, pretty-print.
24
+ Otherwise, print as JSON.
25
+ """
26
+ formatter = get_formatter(ctx)
27
+
28
+ if isinstance(result, dict) and "credentials" in result:
29
+ creds = result["credentials"]
30
+ if not creds:
31
+ formatter.print_info("No credentials found.")
32
+ return
33
+ rows = [
34
+ [c.get("id", ""), c.get("typeName", ""), c.get("description", "")]
35
+ for c in creds
36
+ ]
37
+ formatter.print_table(
38
+ headers=["ID", "Type", "Description"],
39
+ rows=rows,
40
+ title="Credentials",
41
+ )
42
+ else:
43
+ formatter.print_json(result)
44
+
45
+
46
+ @click.group("credential", help="Manage Jenkins credentials.")
47
+ def credential_group():
48
+ """Credential management commands."""
49
+
50
+
51
+ @credential_group.command("list")
52
+ @click.option(
53
+ "--store", "-s", default="system", help="Credential store ID (default: system)."
54
+ )
55
+ @click.option(
56
+ "--domain", "-d", default="_", help="Credential domain ID (default: _)."
57
+ )
58
+ @click.pass_context
59
+ def cmd_list(ctx: click.Context, store: str, domain: str) -> None:
60
+ """List Jenkins credentials."""
61
+ client = get_client(ctx)
62
+ try:
63
+ result = list_credentials(client, store=store, domain=domain, depth=1)
64
+ _display_result(ctx, result)
65
+ except Exception as exc:
66
+ get_formatter(ctx).print_error(str(exc))
67
+ sys.exit(1)
68
+
69
+
70
+ @credential_group.command("get")
71
+ @click.argument("cred_id")
72
+ @click.option(
73
+ "--store", "-s", default="system", help="Credential store ID (default: system)."
74
+ )
75
+ @click.pass_context
76
+ def cmd_get(ctx: click.Context, cred_id: str, store: str) -> None:
77
+ """Get credential details."""
78
+ client = get_client(ctx)
79
+ try:
80
+ result = get_credential(client, cred_id=cred_id, store=store)
81
+ get_formatter(ctx).print_json(result)
82
+ except Exception as exc:
83
+ get_formatter(ctx).print_error(str(exc))
84
+ sys.exit(1)
85
+
86
+
87
+ @credential_group.command("create")
88
+ @click.argument("cred_id")
89
+ @click.argument(
90
+ "config_xml",
91
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
92
+ )
93
+ @click.option(
94
+ "--store", "-s", default="system", help="Credential store ID (default: system)."
95
+ )
96
+ @click.option(
97
+ "--domain", "-d", default="_", help="Credential domain ID (default: _)."
98
+ )
99
+ @click.pass_context
100
+ def cmd_create(
101
+ ctx: click.Context,
102
+ cred_id: str,
103
+ config_xml: Path,
104
+ store: str,
105
+ domain: str,
106
+ ) -> None:
107
+ """Create a credential from an XML config file.
108
+
109
+ CRED_ID is the credential ID (ignored by Jenkins API, used for reference).
110
+ CONFIG_XML is a path to the credential XML file.
111
+
112
+ The XML must follow Jenkins' XStream format for the target credential type.
113
+ Example for UsernamePasswordCredentialsImpl:
114
+
115
+ \\b
116
+ <com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
117
+ <scope>GLOBAL</scope>
118
+ <id>my-cred-id</id>
119
+ <description>My credential</description>
120
+ <username>admin</username>
121
+ <password>secret</password>
122
+ </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
123
+ """
124
+ client = get_client(ctx)
125
+ formatter = get_formatter(ctx)
126
+ try:
127
+ xml_data = config_xml.read_text(encoding="utf-8")
128
+ create_credential(client, xml_data=xml_data, store=store, domain=domain)
129
+ formatter.print_success(f"Credential created in store '{store}' domain '{domain}'.")
130
+ except Exception as exc:
131
+ formatter.print_error(str(exc))
132
+ sys.exit(1)
133
+
134
+
135
+ @credential_group.command("update")
136
+ @click.argument("cred_id")
137
+ @click.argument(
138
+ "config_file",
139
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
140
+ )
141
+ @click.option(
142
+ "--store", "-s", default="system", help="Credential store ID (default: system)."
143
+ )
144
+ @click.pass_context
145
+ def cmd_update(
146
+ ctx: click.Context,
147
+ cred_id: str,
148
+ config_file: Path,
149
+ store: str,
150
+ ) -> None:
151
+ """Update a credential from an XML config file.
152
+
153
+ CRED_ID is the credential ID to update.
154
+ CONFIG_FILE is a path to the credential XML file.
155
+
156
+ The XML must follow Jenkins' XStream format for the target credential type.
157
+ Example for UsernamePasswordCredentialsImpl:
158
+
159
+ \\b
160
+ <com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
161
+ <scope>GLOBAL</scope>
162
+ <id>my-cred-id</id>
163
+ <description>My credential</description>
164
+ <username>admin</username>
165
+ <password>new-secret</password>
166
+ </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
167
+ """
168
+ client = get_client(ctx)
169
+ formatter = get_formatter(ctx)
170
+ try:
171
+ xml_data = config_file.read_text(encoding="utf-8")
172
+ update_credential(client, cred_id=cred_id, xml_data=xml_data, store=store)
173
+ formatter.print_success(f"Credential '{cred_id}' updated in store '{store}'.")
174
+ except Exception as exc:
175
+ formatter.print_error(str(exc))
176
+ sys.exit(1)
177
+
178
+
179
+ @credential_group.command("delete")
180
+ @click.argument("cred_id")
181
+ @click.option(
182
+ "--store", "-s", default="system", help="Credential store ID (default: system)."
183
+ )
184
+ @click.option(
185
+ "--force", "-f", is_flag=True, default=False, help="Skip confirmation prompt."
186
+ )
187
+ @click.pass_context
188
+ def cmd_delete(
189
+ ctx: click.Context, cred_id: str, store: str, force: bool
190
+ ) -> None:
191
+ """Delete a credential."""
192
+ if not force:
193
+ click.confirm(
194
+ f"Delete credential '{cred_id}' from store '{store}'?",
195
+ abort=True,
196
+ )
197
+
198
+ client = get_client(ctx)
199
+ formatter = get_formatter(ctx)
200
+ try:
201
+ delete_credential(client, cred_id=cred_id, store=store)
202
+ formatter.print_success(f"Credential '{cred_id}' deleted.")
203
+ except Exception as exc:
204
+ formatter.print_error(str(exc))
205
+ sys.exit(1)
206
+
207
+
208
+ def register(parent_group: click.Group) -> None:
209
+ """Register the credential subgroup under the parent Click group."""
210
+ parent_group.add_command(credential_group)
jcli/plugins/job.py ADDED
@@ -0,0 +1,425 @@
1
+ """Jenkins Job management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from jcli.cli_helpers import get_client, get_formatter
8
+ from jcli.sdk.exceptions import JenkinsError, JenkinsNotFoundError
9
+ from jcli.sdk.job import (
10
+ copy_job,
11
+ create_folder,
12
+ create_job,
13
+ delete_folder,
14
+ delete_job,
15
+ disable_job,
16
+ enable_job,
17
+ get_job,
18
+ get_job_config,
19
+ list_jobs,
20
+ rename_job,
21
+ update_job_config,
22
+ )
23
+
24
+
25
+ # ------------------------------------------------------------------
26
+ # Click group
27
+ # ------------------------------------------------------------------
28
+
29
+
30
+ @click.group("job", help="Manage Jenkins jobs.")
31
+ def job_group() -> None:
32
+ """Job management commands."""
33
+
34
+
35
+ # ------------------------------------------------------------------
36
+ # list
37
+ # ------------------------------------------------------------------
38
+
39
+
40
+ @job_group.command("list")
41
+ @click.pass_context
42
+ def list_cmd(ctx: click.Context) -> None:
43
+ """List all Jenkins jobs."""
44
+ client = get_client(ctx)
45
+ fmt = get_formatter(ctx)
46
+
47
+ try:
48
+ jobs = list_jobs(client)
49
+ except JenkinsError as exc:
50
+ fmt.print_error(str(exc))
51
+ raise SystemExit(1) from exc
52
+
53
+ if not jobs:
54
+ fmt.print_info("No jobs found.")
55
+ return
56
+
57
+ headers = ["Name", "URL", "Color"]
58
+ rows = [[j.get("name", ""), j.get("url", ""), j.get("color", "")] for j in jobs]
59
+ fmt.print_table(headers, rows, title="Jenkins Jobs")
60
+
61
+
62
+ # ------------------------------------------------------------------
63
+ # get
64
+ # ------------------------------------------------------------------
65
+
66
+
67
+ @job_group.command("get")
68
+ @click.argument("name")
69
+ @click.pass_context
70
+ def get_cmd(ctx: click.Context, name: str) -> None:
71
+ """Show details for a specific job."""
72
+ client = get_client(ctx)
73
+ fmt = get_formatter(ctx)
74
+
75
+ try:
76
+ job = get_job(client, name)
77
+ except JenkinsNotFoundError:
78
+ fmt.print_error(f"Job '{name}' not found.")
79
+ raise SystemExit(1)
80
+ except JenkinsError as exc:
81
+ fmt.print_error(str(exc))
82
+ raise SystemExit(1) from exc
83
+
84
+ fmt.print_json(job)
85
+
86
+
87
+ # ------------------------------------------------------------------
88
+ # create
89
+ # ------------------------------------------------------------------
90
+
91
+
92
+ @job_group.command("create")
93
+ @click.argument("name")
94
+ @click.argument("config_file", type=click.Path(exists=True, dir_okay=False, readable=True), required=False)
95
+ @click.option("--type", "job_type", type=click.Choice(["freestyle", "pipeline"]), help="Job type (skip config_file).")
96
+ @click.option("--git-url", help="Git repository URL.")
97
+ @click.option("--git-branch", default="main", help="Git branch (default: main).")
98
+ @click.option("--description", default="", help="Job description.")
99
+ @click.option("--shell", help="Shell script to run (freestyle only).")
100
+ @click.option("--cron", help="Cron schedule for periodic build (freestyle only).")
101
+ @click.option("--jenkinsfile", default="Jenkinsfile", help="Jenkinsfile path (pipeline only).")
102
+ @click.option("--script", help="Inline Pipeline script (pipeline only).")
103
+ @click.pass_context
104
+ def create_cmd(
105
+ ctx: click.Context,
106
+ name: str,
107
+ config_file: str | None,
108
+ job_type: str | None,
109
+ git_url: str | None,
110
+ git_branch: str,
111
+ description: str,
112
+ shell: str | None,
113
+ cron: str | None,
114
+ jenkinsfile: str,
115
+ script: str | None,
116
+ ) -> None:
117
+ """Create a new job from an XML configuration file or parameters.
118
+
119
+ NAME is the name for the new job.
120
+ CONFIG_FILE is the path to the XML file (optional if --type is specified).
121
+
122
+ Examples:
123
+ jcli job create myjob config.xml
124
+ jcli job create myjob --type freestyle --git-url https://... --shell "make build"
125
+ jcli job create mypipe --type pipeline --git-url https://... --jenkinsfile Jenkinsfile
126
+ jcli job create mypipe --type pipeline --script "pipeline { agent any; stages { stage('build') { steps { echo 'hi' } } } }"
127
+ """
128
+ client = get_client(ctx)
129
+ fmt = get_formatter(ctx)
130
+
131
+ try:
132
+ if config_file:
133
+ # XML file mode (backward compatible)
134
+ try:
135
+ with open(config_file, "r", encoding="utf-8") as f:
136
+ config_xml = f.read()
137
+ except OSError as exc:
138
+ fmt.print_error(f"Cannot read config file '{config_file}': {exc}")
139
+ raise SystemExit(1) from exc
140
+ elif job_type:
141
+ # Parameter mode
142
+ from jcli.sdk.job_templates import generate_freestyle_xml, generate_pipeline_xml
143
+
144
+ if job_type == "freestyle":
145
+ config_xml = generate_freestyle_xml(
146
+ git_url=git_url or "",
147
+ git_branch=git_branch,
148
+ shell_script=shell or "",
149
+ description=description,
150
+ cron_schedule=cron or "",
151
+ )
152
+ else:
153
+ config_xml = generate_pipeline_xml(
154
+ git_url=git_url or "",
155
+ git_branch=git_branch,
156
+ jenkinsfile_path=jenkinsfile,
157
+ description=description,
158
+ script=script or "",
159
+ )
160
+ else:
161
+ raise click.UsageError("Must specify either CONFIG_FILE or --type")
162
+
163
+ create_job(client, name, config_xml)
164
+ except JenkinsError as exc:
165
+ fmt.print_error(str(exc))
166
+ raise SystemExit(1) from exc
167
+
168
+ fmt.print_success(f"Job '{name}' created.")
169
+
170
+
171
+ # ------------------------------------------------------------------
172
+ # update
173
+ # ------------------------------------------------------------------
174
+
175
+
176
+ @job_group.command("update")
177
+ @click.argument("job_name")
178
+ @click.argument("config_file", type=click.Path(exists=True, dir_okay=False))
179
+ @click.pass_context
180
+ def update_cmd(ctx: click.Context, job_name: str, config_file: str) -> None:
181
+ """Update a job's configuration from an XML file.
182
+
183
+ JOB_NAME is the name of the job to update.
184
+ CONFIG_FILE is the path to the new XML configuration file.
185
+ """
186
+ client = get_client(ctx)
187
+ fmt = get_formatter(ctx)
188
+
189
+ try:
190
+ with open(config_file, "r", encoding="utf-8") as f:
191
+ config_xml = f.read()
192
+ except OSError as exc:
193
+ fmt.print_error(f"Cannot read config file '{config_file}': {exc}")
194
+ raise SystemExit(1) from exc
195
+
196
+ try:
197
+ update_job_config(client, job_name, config_xml)
198
+ except JenkinsError as exc:
199
+ fmt.print_error(str(exc))
200
+ raise SystemExit(1) from exc
201
+
202
+ fmt.print_success(f"Job '{job_name}' updated.")
203
+
204
+
205
+ # ------------------------------------------------------------------
206
+ # delete
207
+ # ------------------------------------------------------------------
208
+
209
+
210
+ @job_group.command("delete")
211
+ @click.argument("name")
212
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
213
+ @click.pass_context
214
+ def delete_cmd(ctx: click.Context, name: str, yes: bool) -> None:
215
+ """Delete a job.
216
+
217
+ Prompts for confirmation unless --yes/-y is given.
218
+ """
219
+ client = get_client(ctx)
220
+ fmt = get_formatter(ctx)
221
+
222
+ if not yes:
223
+ confirm = input(f"Are you sure you want to delete job '{name}'? [y/N]: ")
224
+ if confirm.lower() not in ("y", "yes"):
225
+ fmt.print_info("Delete cancelled.")
226
+ return
227
+
228
+ try:
229
+ delete_job(client, name)
230
+ except JenkinsNotFoundError:
231
+ fmt.print_error(f"Job '{name}' not found.")
232
+ raise SystemExit(1)
233
+ except JenkinsError as exc:
234
+ fmt.print_error(str(exc))
235
+ raise SystemExit(1) from exc
236
+
237
+ fmt.print_success(f"Job '{name}' deleted.")
238
+
239
+
240
+ # ------------------------------------------------------------------
241
+ # copy
242
+ # ------------------------------------------------------------------
243
+
244
+
245
+ @job_group.command("copy")
246
+ @click.argument("from_name")
247
+ @click.argument("new_name")
248
+ @click.pass_context
249
+ def copy_cmd(ctx: click.Context, from_name: str, new_name: str) -> None:
250
+ """Copy a job to a new name.
251
+
252
+ FROM_NAME is the source job.
253
+ NEW_NAME is the name for the copied job.
254
+ """
255
+ client = get_client(ctx)
256
+ fmt = get_formatter(ctx)
257
+
258
+ try:
259
+ copy_job(client, from_name, new_name)
260
+ except JenkinsNotFoundError:
261
+ fmt.print_error(f"Source job '{from_name}' not found.")
262
+ raise SystemExit(1)
263
+ except JenkinsError as exc:
264
+ fmt.print_error(str(exc))
265
+ raise SystemExit(1) from exc
266
+
267
+ fmt.print_success(f"Job '{from_name}' copied to '{new_name}'.")
268
+
269
+
270
+ # ------------------------------------------------------------------
271
+ # rename
272
+ # ------------------------------------------------------------------
273
+
274
+
275
+ @job_group.command("rename")
276
+ @click.argument("old_name")
277
+ @click.argument("new_name")
278
+ @click.pass_context
279
+ def rename_job_cmd(ctx: click.Context, old_name: str, new_name: str) -> None:
280
+ """Rename a job.
281
+
282
+ OLD_NAME is the current name of the job.
283
+ NEW_NAME is the new name to assign to the job.
284
+ """
285
+ client = get_client(ctx)
286
+ fmt = get_formatter(ctx)
287
+
288
+ try:
289
+ rename_job(client, old_name, new_name)
290
+ except JenkinsNotFoundError:
291
+ fmt.print_error(f"Job '{old_name}' not found.")
292
+ raise SystemExit(1)
293
+ except JenkinsError as exc:
294
+ fmt.print_error(str(exc))
295
+ raise SystemExit(1) from exc
296
+
297
+ fmt.print_success(f"Job renamed from '{old_name}' to '{new_name}'.")
298
+
299
+
300
+ # ------------------------------------------------------------------
301
+ # enable / disable
302
+ # ------------------------------------------------------------------
303
+
304
+
305
+ @job_group.command("enable")
306
+ @click.argument("name")
307
+ @click.pass_context
308
+ def enable_cmd(ctx: click.Context, name: str) -> None:
309
+ """Enable a disabled job."""
310
+ client = get_client(ctx)
311
+ fmt = get_formatter(ctx)
312
+
313
+ try:
314
+ enable_job(client, name)
315
+ except JenkinsNotFoundError:
316
+ fmt.print_error(f"Job '{name}' not found.")
317
+ raise SystemExit(1)
318
+ except JenkinsError as exc:
319
+ fmt.print_error(str(exc))
320
+ raise SystemExit(1) from exc
321
+
322
+ fmt.print_success(f"Job '{name}' enabled.")
323
+
324
+
325
+ @job_group.command("disable")
326
+ @click.argument("name")
327
+ @click.pass_context
328
+ def disable_cmd(ctx: click.Context, name: str) -> None:
329
+ """Disable a job."""
330
+ client = get_client(ctx)
331
+ fmt = get_formatter(ctx)
332
+
333
+ try:
334
+ disable_job(client, name)
335
+ except JenkinsNotFoundError:
336
+ fmt.print_error(f"Job '{name}' not found.")
337
+ raise SystemExit(1)
338
+ except JenkinsError as exc:
339
+ fmt.print_error(str(exc))
340
+ raise SystemExit(1) from exc
341
+
342
+ fmt.print_success(f"Job '{name}' disabled.")
343
+
344
+
345
+ # ------------------------------------------------------------------
346
+ # config
347
+ # ------------------------------------------------------------------
348
+
349
+
350
+ @job_group.command("config")
351
+ @click.argument("name")
352
+ @click.pass_context
353
+ def config_cmd(ctx: click.Context, name: str) -> None:
354
+ """Print the XML configuration of a job."""
355
+ client = get_client(ctx)
356
+ fmt = get_formatter(ctx)
357
+
358
+ try:
359
+ xml_text = get_job_config(client, name)
360
+ except JenkinsNotFoundError:
361
+ fmt.print_error(f"Job '{name}' not found.")
362
+ raise SystemExit(1)
363
+ except JenkinsError as exc:
364
+ fmt.print_error(str(exc))
365
+ raise SystemExit(1) from exc
366
+
367
+ fmt.console.print(xml_text)
368
+
369
+
370
+ # ------------------------------------------------------------------
371
+ # create-folder
372
+ # ------------------------------------------------------------------
373
+
374
+
375
+ @job_group.command("create-folder")
376
+ @click.argument("name")
377
+ @click.pass_context
378
+ def create_folder_cmd(ctx: click.Context, name: str) -> None:
379
+ """Create a new folder."""
380
+ client = get_client(ctx)
381
+ fmt = get_formatter(ctx)
382
+
383
+ try:
384
+ create_folder(client, name)
385
+ except JenkinsError as exc:
386
+ fmt.print_error(str(exc))
387
+ raise SystemExit(1) from exc
388
+
389
+ fmt.print_success(f"Folder '{name}' created.")
390
+
391
+
392
+ # ------------------------------------------------------------------
393
+ # delete-folder
394
+ # ------------------------------------------------------------------
395
+
396
+
397
+ @job_group.command("delete-folder")
398
+ @click.argument("name")
399
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
400
+ @click.pass_context
401
+ def delete_folder_cmd(ctx: click.Context, name: str, yes: bool) -> None:
402
+ """Delete a folder and all its contents."""
403
+ if not yes:
404
+ click.confirm(f"Delete folder '{name}' and ALL its contents?", abort=True)
405
+
406
+ client = get_client(ctx)
407
+ fmt = get_formatter(ctx)
408
+
409
+ try:
410
+ delete_folder(client, name)
411
+ except JenkinsError as exc:
412
+ fmt.print_error(str(exc))
413
+ raise SystemExit(1) from exc
414
+
415
+ fmt.print_success(f"Folder '{name}' deleted.")
416
+
417
+
418
+ # ------------------------------------------------------------------
419
+ # Registration
420
+ # ------------------------------------------------------------------
421
+
422
+
423
+ def register(parent_group: click.Group) -> None:
424
+ """Register the job subgroup under the parent Click group."""
425
+ parent_group.add_command(job_group)