1use clap::{Parser, ValueEnum};
2
3use crate::{
4 cmds::cicd::{
5 mermaid::ChartType, JobListCliArgs, LintFilePathArgs, RunnerListCliArgs,
6 RunnerMetadataGetCliArgs, RunnerPostDataCliArgs, RunnerStatus, RunnerType,
7 },
8 remote::ListRemoteCliArgs,
9};
10
11use super::common::{GetArgs, ListArgs};
12
13#[derive(Parser)]
14pub struct PipelineCommand {
15 #[clap(subcommand)]
16 subcommand: PipelineSubcommand,
17}
18
19#[derive(Parser)]
20enum PipelineSubcommand {
21 #[clap(about = "Lint ci yml files. Default is .gitlab-ci.yml")]
22 Lint(FilePathArgs),
23 #[clap(
24 about = "Get merged .gitlab-ci.yml. Total .gitlab-ci.yml result of merging included yaml pipeline files in the repository"
25 )]
26 MergedCi,
27 #[clap(about = "Create a Mermaid diagram of the .gitlab-ci.yml pipeline")]
28 Chart(ChartArgs),
29 #[clap(about = "List pipelines")]
30 List(ListArgs),
31 #[clap(subcommand, name = "jb", about = "Job operations")]
32 Jobs(JobsSubCommand),
33 #[clap(subcommand, name = "rn", about = "Runner operations")]
34 Runners(RunnerSubCommand),
35}
36
37#[derive(Parser)]
38enum JobsSubCommand {
39 #[clap(about = "List jobs")]
40 List(ListJob),
41}
42
43#[derive(Parser)]
44struct ListJob {
45 #[command(flatten)]
46 list_args: ListArgs,
47}
48
49#[derive(Parser)]
50struct FilePathArgs {
51 #[clap(default_value = ".gitlab-ci.yml")]
53 path: String,
54}
55
56#[derive(Parser)]
57struct ChartArgs {
58 #[clap(long, default_value = "stageswithjobs")]
60 chart_type: ChartTypeCli,
61}
62
63#[derive(ValueEnum, Clone, PartialEq, Debug)]
64enum ChartTypeCli {
65 #[clap(name = "stageswithjobs")]
66 StagesWithJobs,
67 Jobs,
68 Stages,
69}
70
71#[derive(Parser)]
72enum RunnerSubCommand {
73 #[clap(about = "List runners")]
74 List(ListRunner),
75 #[clap(about = "Get runner metadata")]
76 Get(RunnerMetadata),
77 #[clap(about = "Create a new runner")]
78 Create(RunnerPostData),
79}
80
81#[derive(ValueEnum, Clone, PartialEq, Debug)]
82enum RunnerStatusCli {
83 Online,
84 Offline,
85 Stale,
86 NeverContacted,
87 All,
88}
89
90#[derive(Parser)]
91struct ListRunner {
92 #[clap()]
94 status: RunnerStatusCli,
95 #[clap(long, value_delimiter = ',', help_heading = "Runner options")]
97 tags: Option<Vec<String>>,
98 #[clap(long, help_heading = "Runner options")]
100 all: bool,
101 #[command(flatten)]
102 list_args: ListArgs,
103}
104
105#[derive(Parser)]
106struct RunnerMetadata {
107 #[clap()]
109 id: i64,
110 #[clap(flatten)]
111 get_args: GetArgs,
112}
113
114#[derive(Parser, Default)]
115struct RunnerPostData {
116 #[clap(long)]
118 description: Option<String>,
119 #[clap(long, value_delimiter = ',')]
121 tags: Option<Vec<String>>,
122 #[clap(long)]
124 kind: RunnerTypeCli,
125 #[clap(long)]
126 run_untagged: bool,
128 #[clap(long, group = "runner_target_id")]
130 project_id: Option<i64>,
131 #[clap(long, group = "runner_target_id")]
133 group_id: Option<i64>,
134}
135
136impl RunnerPostData {
137 fn validate_runner_type_id(&self) -> Result<(), String> {
138 if self.kind == RunnerTypeCli::Project && self.project_id.is_none() {
139 return Err("error: project id is required for project runner".to_string());
140 }
141 if self.kind == RunnerTypeCli::Group && self.group_id.is_none() {
142 return Err("error: group id is required for group runner".to_string());
143 }
144 if self.kind == RunnerTypeCli::Instance
145 && (self.project_id.is_some() || self.group_id.is_some())
146 {
147 return Err(
148 "error: project id and group id are not required for instance runner".to_string(),
149 );
150 }
151 Ok(())
152 }
153}
154
155#[derive(ValueEnum, Clone, PartialEq, Debug, Default)]
156enum RunnerTypeCli {
157 #[default]
158 Instance,
159 Group,
160 Project,
161}
162
163impl From<ChartTypeCli> for ChartType {
164 fn from(chart_type: ChartTypeCli) -> Self {
165 match chart_type {
166 ChartTypeCli::StagesWithJobs => ChartType::StagesWithJobs,
167 ChartTypeCli::Jobs => ChartType::Jobs,
168 ChartTypeCli::Stages => ChartType::Stages,
169 }
170 }
171}
172
173impl From<ChartArgs> for ChartType {
174 fn from(args: ChartArgs) -> Self {
175 args.chart_type.into()
176 }
177}
178
179impl From<ChartArgs> for PipelineOptions {
180 fn from(options: ChartArgs) -> Self {
181 PipelineOptions::Chart(options.into())
182 }
183}
184
185impl From<PipelineCommand> for PipelineOptions {
186 fn from(options: PipelineCommand) -> Self {
187 match options.subcommand {
188 PipelineSubcommand::Lint(options) => options.into(),
189 PipelineSubcommand::MergedCi => PipelineOptions::MergedCi,
190 PipelineSubcommand::Chart(options) => PipelineOptions::Chart(options.into()),
191 PipelineSubcommand::List(options) => options.into(),
192 PipelineSubcommand::Runners(options) => options.into(),
193 PipelineSubcommand::Jobs(options) => options.into(),
194 }
195 }
196}
197
198impl From<FilePathArgs> for PipelineOptions {
199 fn from(options: FilePathArgs) -> Self {
200 PipelineOptions::Lint(options.into())
201 }
202}
203
204impl From<FilePathArgs> for LintFilePathArgs {
205 fn from(options: FilePathArgs) -> Self {
206 LintFilePathArgs::builder()
207 .path(options.path)
208 .build()
209 .unwrap()
210 }
211}
212
213impl From<ListArgs> for PipelineOptions {
214 fn from(options: ListArgs) -> Self {
215 PipelineOptions::List(options.into())
216 }
217}
218
219impl From<RunnerSubCommand> for PipelineOptions {
220 fn from(options: RunnerSubCommand) -> Self {
221 match options {
222 RunnerSubCommand::List(options) => PipelineOptions::Runners(options.into()),
223 RunnerSubCommand::Get(options) => PipelineOptions::Runners(options.into()),
224 RunnerSubCommand::Create(options) => PipelineOptions::Runners(options.into()),
225 }
226 }
227}
228
229impl From<RunnerStatusCli> for RunnerStatus {
230 fn from(status: RunnerStatusCli) -> Self {
231 match status {
232 RunnerStatusCli::Online => RunnerStatus::Online,
233 RunnerStatusCli::Offline => RunnerStatus::Offline,
234 RunnerStatusCli::Stale => RunnerStatus::Stale,
235 RunnerStatusCli::NeverContacted => RunnerStatus::NeverContacted,
236 RunnerStatusCli::All => RunnerStatus::All,
237 }
238 }
239}
240
241impl From<ListRunner> for RunnerOptions {
242 fn from(options: ListRunner) -> Self {
243 RunnerOptions::List(
244 RunnerListCliArgs::builder()
245 .status(options.status.into())
246 .tags(options.tags.map(|tags| tags.join(",").to_string()))
247 .all(options.all)
248 .list_args(options.list_args.into())
249 .build()
250 .unwrap(),
251 )
252 }
253}
254
255impl From<RunnerMetadata> for RunnerOptions {
256 fn from(options: RunnerMetadata) -> Self {
257 RunnerOptions::Get(
258 RunnerMetadataGetCliArgs::builder()
259 .id(options.id)
260 .get_args(options.get_args.into())
261 .build()
262 .unwrap(),
263 )
264 }
265}
266
267impl From<RunnerPostData> for RunnerOptions {
268 fn from(options: RunnerPostData) -> Self {
269 if let Err(e) = options.validate_runner_type_id() {
270 eprintln!("{e}");
271 std::process::exit(2);
272 };
273 RunnerOptions::Create(
274 RunnerPostDataCliArgs::builder()
275 .description(options.description)
276 .tags(options.tags.map(|tags| tags.join(",").to_string()))
277 .kind(options.kind.into())
278 .run_untagged(options.run_untagged)
279 .project_id(options.project_id)
280 .group_id(options.group_id)
281 .build()
282 .unwrap(),
283 )
284 }
285}
286
287impl From<RunnerTypeCli> for RunnerType {
288 fn from(kind: RunnerTypeCli) -> Self {
289 match kind {
290 RunnerTypeCli::Instance => RunnerType::Instance,
291 RunnerTypeCli::Group => RunnerType::Group,
292 RunnerTypeCli::Project => RunnerType::Project,
293 }
294 }
295}
296
297impl From<ListJob> for JobOptions {
298 fn from(options: ListJob) -> Self {
299 JobOptions::List(
300 JobListCliArgs::builder()
301 .list_args(options.list_args.into())
302 .build()
303 .unwrap(),
304 )
305 }
306}
307
308impl From<JobsSubCommand> for PipelineOptions {
309 fn from(options: JobsSubCommand) -> Self {
310 match options {
311 JobsSubCommand::List(options) => PipelineOptions::Jobs(options.into()),
312 }
313 }
314}
315
316pub enum PipelineOptions {
317 Lint(LintFilePathArgs),
318 List(ListRemoteCliArgs),
319 Runners(RunnerOptions),
320 MergedCi,
321 Chart(ChartType),
322 Jobs(JobOptions),
323}
324
325pub enum JobOptions {
326 List(JobListCliArgs),
327}
328
329pub enum RunnerOptions {
330 List(RunnerListCliArgs),
331 Get(RunnerMetadataGetCliArgs),
332 Create(RunnerPostDataCliArgs),
333}
334
335#[cfg(test)]
336mod test {
337 use super::*;
338 use crate::cli::{Args, Command};
339
340 #[test]
341 fn test_pipeline_cli_list() {
342 let args = Args::parse_from(vec![
343 "gr",
344 "pp",
345 "list",
346 "--from-page",
347 "1",
348 "--to-page",
349 "2",
350 ]);
351 let list_args = match args.command {
352 Command::Pipeline(PipelineCommand {
353 subcommand: PipelineSubcommand::List(options),
354 }) => {
355 assert_eq!(options.from_page, Some(1));
356 assert_eq!(options.to_page, Some(2));
357 options
358 }
359 _ => panic!("Expected PipelineCommand"),
360 };
361 let options: PipelineOptions = list_args.into();
362 match options {
363 PipelineOptions::List(args) => {
364 assert_eq!(args.from_page, Some(1));
365 assert_eq!(args.to_page, Some(2));
366 }
367 _ => panic!("Expected PipelineOptions::List"),
368 }
369 }
370
371 #[test]
372 fn test_pipeline_cli_runners_list() {
373 let args = Args::parse_from(vec![
374 "gr",
375 "pp",
376 "rn",
377 "list",
378 "online",
379 "--tags",
380 "tag1,tag2",
381 "--all",
382 "--from-page",
383 "1",
384 "--to-page",
385 "2",
386 ]);
387 let list_args = match args.command {
388 Command::Pipeline(PipelineCommand {
389 subcommand: PipelineSubcommand::Runners(RunnerSubCommand::List(options)),
390 }) => {
391 assert_eq!(options.status, RunnerStatusCli::Online);
392 assert_eq!(
393 options.tags,
394 Some(vec!["tag1".to_string(), "tag2".to_string()])
395 );
396 assert!(options.all);
397 assert_eq!(options.list_args.from_page, Some(1));
398 assert_eq!(options.list_args.to_page, Some(2));
399 options
400 }
401 _ => panic!("Expected PipelineCommand"),
402 };
403 let options: RunnerOptions = list_args.into();
404 match options {
405 RunnerOptions::List(args) => {
406 assert_eq!(args.status, RunnerStatus::Online);
407 assert_eq!(args.tags, Some("tag1,tag2".to_string()));
408 assert!(args.all);
409 assert_eq!(args.list_args.from_page, Some(1));
410 assert_eq!(args.list_args.to_page, Some(2));
411 }
412 _ => panic!("Expected RunnerOptions::List"),
413 }
414 }
415
416 #[test]
417 fn test_get_gitlab_runner_metadata() {
418 let args = Args::parse_from(vec!["gr", "pp", "rn", "get", "123"]);
419 let list_args = match args.command {
420 Command::Pipeline(PipelineCommand {
421 subcommand: PipelineSubcommand::Runners(RunnerSubCommand::Get(options)),
422 }) => {
423 assert_eq!(options.id, 123);
424 options
425 }
426 _ => panic!("Expected PipelineCommand"),
427 };
428 let options: RunnerOptions = list_args.into();
429 match options {
430 RunnerOptions::Get(args) => {
431 assert_eq!(args.id, 123);
432 }
433 _ => panic!("Expected RunnerOptions::Get"),
434 }
435 }
436
437 #[test]
438 fn test_pipeline_create_runner() {
439 let args = Args::parse_from(vec![
440 "gr",
441 "pp",
442 "rn",
443 "create",
444 "--description",
445 "test-runner",
446 "--tags",
447 "tag1,tag2",
448 "--kind",
449 "instance",
450 ]);
451 let args = match args.command {
452 Command::Pipeline(PipelineCommand {
453 subcommand: PipelineSubcommand::Runners(RunnerSubCommand::Create(options)),
454 }) => {
455 assert_eq!(options.description, Some("test-runner".to_string()));
456 assert_eq!(
457 options.tags,
458 Some(vec!["tag1".to_string(), "tag2".to_string()])
459 );
460 assert_eq!(options.kind, RunnerTypeCli::Instance);
461 options
462 }
463 _ => panic!("Expected PipelineCommand"),
464 };
465 let options: RunnerOptions = args.into();
466 match options {
467 RunnerOptions::Create(args) => {
468 assert_eq!(args.description, Some("test-runner".to_string()));
469 assert_eq!(args.tags, Some("tag1,tag2".to_string()));
470 assert_eq!(args.kind, RunnerType::Instance);
471 }
472 _ => panic!("Expected RunnerOptions::Create"),
473 }
474 }
475
476 #[test]
477 fn test_lint_ci_file_args() {
478 let args = Args::parse_from(vec!["gr", "pp", "lint"]);
479 let options = match args.command {
480 Command::Pipeline(PipelineCommand {
481 subcommand: PipelineSubcommand::Lint(options),
482 }) => {
483 assert_eq!(options.path, ".gitlab-ci.yml");
484 options
485 }
486 _ => panic!("Expected PipelineCommand"),
487 };
488 let options: PipelineOptions = options.into();
489 match options {
490 PipelineOptions::Lint(args) => {
491 assert_eq!(args.path, ".gitlab-ci.yml");
492 }
493 _ => panic!("Expected PipelineOptions::Lint"),
494 }
495 }
496
497 #[test]
498 fn test_lint_ci_file_args_with_path() {
499 let args = Args::parse_from(vec!["gr", "pp", "lint", "path/to/ci.yml"]);
500 let options = match args.command {
501 Command::Pipeline(PipelineCommand {
502 subcommand: PipelineSubcommand::Lint(options),
503 }) => {
504 assert_eq!(options.path, "path/to/ci.yml");
505 options
506 }
507 _ => panic!("Expected PipelineCommand"),
508 };
509 let options: PipelineOptions = options.into();
510 match options {
511 PipelineOptions::Lint(args) => {
512 assert_eq!(args.path, "path/to/ci.yml");
513 }
514 _ => panic!("Expected PipelineOptions::Lint"),
515 }
516 }
517
518 #[test]
519 fn test_merged_ci_file_args() {
520 let args = Args::parse_from(vec!["gr", "pp", "merged-ci"]);
521 let options = match args.command {
522 Command::Pipeline(PipelineCommand {
523 subcommand: PipelineSubcommand::MergedCi,
524 }) => PipelineOptions::MergedCi,
525 _ => panic!("Expected PipelineCommand"),
526 };
527 match options {
528 PipelineOptions::MergedCi => {}
529 _ => panic!("Expected PipelineOptions::MergedCi"),
530 }
531 }
532
533 #[test]
534 fn test_chart_cli_args() {
535 let args = Args::parse_from(vec!["gr", "pp", "chart"]);
536 let options = match args.command {
537 Command::Pipeline(PipelineCommand {
538 subcommand: PipelineSubcommand::Chart(options),
539 }) => {
540 assert_eq!(options.chart_type, ChartTypeCli::StagesWithJobs);
541 options
542 }
543 _ => panic!("Expected PipelineCommand"),
544 };
545 let options: PipelineOptions = options.into();
546 match options {
547 PipelineOptions::Chart(args) => {
548 assert_eq!(args, ChartType::StagesWithJobs);
549 }
550 _ => panic!("Expected PipelineOptions::Chart"),
551 }
552 }
553
554 #[test]
555 fn test_pipeline_cli_jobs_list() {
556 let args = Args::parse_from(vec![
557 "gr",
558 "pp",
559 "jb",
560 "list",
561 "--from-page",
562 "1",
563 "--to-page",
564 "2",
565 ]);
566
567 let list_args = match args.command {
568 Command::Pipeline(PipelineCommand {
569 subcommand: PipelineSubcommand::Jobs(JobsSubCommand::List(options)),
570 }) => {
571 assert_eq!(options.list_args.from_page, Some(1));
572 assert_eq!(options.list_args.to_page, Some(2));
573 options
574 }
575 _ => panic!("Expected PipelineCommand"),
576 };
577 let options: JobOptions = list_args.into();
578 match options {
579 JobOptions::List(args) => {
580 assert_eq!(args.list_args.from_page, Some(1));
581 assert_eq!(args.list_args.to_page, Some(2));
582 }
583 }
584 }
585
586 #[test]
587 fn test_project_runner_with_project_id() {
588 let data = RunnerPostData {
589 kind: RunnerTypeCli::Project,
590 project_id: Some(123),
591 group_id: None,
592 ..Default::default()
593 };
594 assert!(data.validate_runner_type_id().is_ok());
595 }
596
597 #[test]
598 fn test_project_runner_without_project_id() {
599 let data = RunnerPostData {
600 kind: RunnerTypeCli::Project,
601 project_id: None,
602 group_id: None,
603 ..Default::default()
604 };
605 assert_eq!(
606 data.validate_runner_type_id(),
607 Err("error: project id is required for project runner".to_string())
608 );
609 }
610
611 #[test]
612 fn test_group_runner_with_group_id() {
613 let data = RunnerPostData {
614 kind: RunnerTypeCli::Group,
615 project_id: None,
616 group_id: Some(456),
617 ..Default::default()
618 };
619 assert!(data.validate_runner_type_id().is_ok());
620 }
621
622 #[test]
623 fn test_group_runner_without_group_id() {
624 let data = RunnerPostData {
625 kind: RunnerTypeCli::Group,
626 project_id: None,
627 group_id: None,
628 ..Default::default()
629 };
630 assert_eq!(
631 data.validate_runner_type_id(),
632 Err("error: group id is required for group runner".to_string())
633 );
634 }
635
636 #[test]
637 fn test_instance_runner_without_ids() {
638 let data = RunnerPostData {
639 kind: RunnerTypeCli::Instance,
640 project_id: None,
641 group_id: None,
642 ..Default::default()
643 };
644 assert!(data.validate_runner_type_id().is_ok());
645 }
646
647 #[test]
648 fn test_instance_runner_with_project_id() {
649 let data = RunnerPostData {
650 kind: RunnerTypeCli::Instance,
651 project_id: Some(123),
652 group_id: None,
653 ..Default::default()
654 };
655 assert_eq!(
656 data.validate_runner_type_id(),
657 Err("error: project id and group id are not required for instance runner".to_string())
658 );
659 }
660
661 #[test]
662 fn test_instance_runner_with_group_id() {
663 let data = RunnerPostData {
664 kind: RunnerTypeCli::Instance,
665 project_id: None,
666 group_id: Some(456),
667 ..Default::default()
668 };
669 assert_eq!(
670 data.validate_runner_type_id(),
671 Err("error: project id and group id are not required for instance runner".to_string())
672 );
673 }
674
675 #[test]
676 fn test_instance_runner_with_both_ids() {
677 let data = RunnerPostData {
678 kind: RunnerTypeCli::Instance,
679 project_id: Some(123),
680 group_id: Some(456),
681 ..Default::default()
682 };
683 assert_eq!(
684 data.validate_runner_type_id(),
685 Err("error: project id and group id are not required for instance runner".to_string())
686 );
687 }
688}