1use mermaid::{generate_mermaid_stages_diagram, YamlParser};
2use yaml::load_yaml;
3
4use crate::api_traits::{Cicd, CicdJob, CicdRunner, Timestamp};
5use crate::cli::cicd::{JobOptions, PipelineOptions, RunnerOptions};
6use crate::config::ConfigProperties;
7use crate::display::{Column, DisplayBody};
8use crate::remote::{CacheType, GetRemoteCliArgs, ListBodyArgs, ListRemoteCliArgs};
9use crate::{display, error, remote, Result};
10use std::fmt::Display;
11use std::io::{Read, Write};
12use std::sync::Arc;
13
14pub mod mermaid;
15pub mod yaml;
16
17use super::common::{
18 self, num_cicd_pages, num_cicd_resources, num_job_pages, num_job_resources, num_runner_pages,
19 num_runner_resources,
20};
21
22#[derive(Builder, Clone, Debug)]
23pub struct Pipeline {
24 id: i64,
25 pub status: String,
26 web_url: String,
27 branch: String,
28 sha: String,
29 created_at: String,
30 updated_at: String,
31 duration: u64,
32}
33
34impl Pipeline {
35 pub fn builder() -> PipelineBuilder {
36 PipelineBuilder::default()
37 }
38}
39
40impl Timestamp for Pipeline {
41 fn created_at(&self) -> String {
42 self.created_at.clone()
43 }
44}
45
46impl From<Pipeline> for DisplayBody {
47 fn from(p: Pipeline) -> DisplayBody {
48 DisplayBody {
49 columns: vec![
50 Column::new("ID", p.id.to_string()),
51 Column::new("URL", p.web_url),
52 Column::new("Branch", p.branch),
53 Column::new("SHA", p.sha),
54 Column::new("Created at", p.created_at),
55 Column::new("Updated at", p.updated_at),
56 Column::new("Duration", p.duration.to_string()),
57 Column::new("Status", p.status),
58 ],
59 }
60 }
61}
62
63#[derive(Builder, Clone)]
64pub struct PipelineBodyArgs {
65 pub from_to_page: Option<ListBodyArgs>,
66}
67
68impl PipelineBodyArgs {
69 pub fn builder() -> PipelineBodyArgsBuilder {
70 PipelineBodyArgsBuilder::default()
71 }
72}
73
74#[derive(Builder, Clone)]
75pub struct LintFilePathArgs {
76 pub path: String,
77}
78
79impl LintFilePathArgs {
80 pub fn builder() -> LintFilePathArgsBuilder {
81 LintFilePathArgsBuilder::default()
82 }
83}
84
85#[derive(Builder, Clone)]
86pub struct LintResponse {
87 pub valid: bool,
88 #[builder(default)]
89 pub merged_yaml: String,
90 pub errors: Vec<String>,
91}
92
93impl LintResponse {
94 pub fn builder() -> LintResponseBuilder {
95 LintResponseBuilder::default()
96 }
97}
98
99pub struct YamlBytes<'a>(&'a [u8]);
100
101impl YamlBytes<'_> {
102 pub fn new(data: &[u8]) -> YamlBytes<'_> {
103 YamlBytes(data)
104 }
105}
106
107impl Display for YamlBytes<'_> {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 let s = String::from_utf8_lossy(self.0);
110 write!(f, "{s}")
111 }
112}
113
114#[derive(Builder, Clone)]
115pub struct Runner {
116 pub id: i64,
117 pub active: bool,
118 pub description: String,
119 pub ip_address: String,
120 pub name: String,
121 pub online: bool,
122 pub paused: bool,
123 pub is_shared: bool,
124 pub runner_type: String,
125 pub status: String,
126}
127
128impl Runner {
129 pub fn builder() -> RunnerBuilder {
130 RunnerBuilder::default()
131 }
132}
133
134impl From<Runner> for DisplayBody {
135 fn from(r: Runner) -> DisplayBody {
136 DisplayBody {
137 columns: vec![
138 Column::new("ID", r.id.to_string()),
139 Column::new("Active", r.active.to_string()),
140 Column::new("Description", r.description),
141 Column::new("IP Address", r.ip_address),
142 Column::new("Name", r.name),
143 Column::new("Paused", r.paused.to_string()),
144 Column::new("Shared", r.is_shared.to_string()),
145 Column::new("Type", r.runner_type),
146 Column::new("Online", r.online.to_string()),
147 Column::new("Status", r.status.to_string()),
148 ],
149 }
150 }
151}
152
153impl Timestamp for Runner {
154 fn created_at(&self) -> String {
155 "1970-01-01T00:00:00Z".to_string()
157 }
158}
159
160#[derive(Builder, Clone)]
162pub struct RunnerMetadata {
163 pub id: i64,
164 pub run_untagged: bool,
165 pub tag_list: Vec<String>,
166 pub version: String,
167 pub architecture: String,
168 pub platform: String,
169 pub contacted_at: String,
170 pub revision: String,
171}
172
173impl RunnerMetadata {
174 pub fn builder() -> RunnerMetadataBuilder {
175 RunnerMetadataBuilder::default()
176 }
177}
178
179impl From<RunnerMetadata> for DisplayBody {
180 fn from(r: RunnerMetadata) -> DisplayBody {
181 DisplayBody {
182 columns: vec![
183 Column::new("ID", r.id.to_string()),
184 Column::new("Run untagged", r.run_untagged.to_string()),
185 Column::new("Tags", r.tag_list.join(", ")),
186 Column::new("Architecture", r.architecture),
187 Column::new("Platform", r.platform),
188 Column::new("Contacted at", r.contacted_at),
189 Column::new("Version", r.version),
190 Column::new("Revision", r.revision),
191 ],
192 }
193 }
194}
195
196#[derive(Builder, Clone)]
197pub struct RunnerListCliArgs {
198 pub status: RunnerStatus,
199 #[builder(default)]
200 pub tags: Option<String>,
201 #[builder(default)]
202 pub all: bool,
203 pub list_args: ListRemoteCliArgs,
204}
205
206impl RunnerListCliArgs {
207 pub fn builder() -> RunnerListCliArgsBuilder {
208 RunnerListCliArgsBuilder::default()
209 }
210}
211
212#[derive(Builder, Clone)]
213pub struct RunnerListBodyArgs {
214 pub list_args: Option<ListBodyArgs>,
215 pub status: RunnerStatus,
216 #[builder(default)]
217 pub tags: Option<String>,
218 #[builder(default)]
219 pub all: bool,
220}
221
222impl RunnerListBodyArgs {
223 pub fn builder() -> RunnerListBodyArgsBuilder {
224 RunnerListBodyArgsBuilder::default()
225 }
226}
227
228#[derive(Builder, Clone)]
229pub struct RunnerMetadataGetCliArgs {
230 pub id: i64,
231 pub get_args: GetRemoteCliArgs,
232}
233
234impl RunnerMetadataGetCliArgs {
235 pub fn builder() -> RunnerMetadataGetCliArgsBuilder {
236 RunnerMetadataGetCliArgsBuilder::default()
237 }
238}
239
240#[derive(Builder, Clone)]
241pub struct RunnerPostDataCliArgs {
242 pub description: Option<String>,
243 pub tags: Option<String>,
244 pub kind: RunnerType,
245 #[builder(default)]
246 pub run_untagged: bool,
247 #[builder(default)]
248 pub project_id: Option<i64>,
249 #[builder(default)]
250 pub group_id: Option<i64>,
251}
252
253impl RunnerPostDataCliArgs {
254 pub fn builder() -> RunnerPostDataCliArgsBuilder {
255 RunnerPostDataCliArgsBuilder::default()
256 }
257}
258
259fn create_runner<W: Write>(
260 remote: Arc<dyn CicdRunner>,
261 cli_args: RunnerPostDataCliArgs,
262 mut writer: W,
263) -> Result<()> {
264 let response = remote.create(cli_args)?;
265 writeln!(writer, "{response}")?;
266 Ok(())
267}
268
269#[derive(Builder, Clone)]
270pub struct RunnerRegistrationResponse {
271 pub id: i64,
272 pub token: String,
273 pub token_expiration: String,
274}
275
276impl RunnerRegistrationResponse {
277 pub fn builder() -> RunnerRegistrationResponseBuilder {
278 RunnerRegistrationResponseBuilder::default()
279 }
280}
281
282impl Display for RunnerRegistrationResponse {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 write!(
285 f,
286 "Runner ID: [{}], Runner Token: [{}], Token Expiration: [{}]",
287 self.id, self.token, self.token_expiration
288 )
289 }
290}
291
292#[derive(Clone, PartialEq, Debug)]
293pub enum RunnerType {
294 Instance,
295 Group,
296 Project,
297}
298
299impl Display for RunnerType {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 match self {
302 RunnerType::Instance => write!(f, "instance_type"),
303 RunnerType::Group => write!(f, "group_type"),
304 RunnerType::Project => write!(f, "project_type"),
305 }
306 }
307}
308
309#[derive(Clone, Copy, PartialEq, Debug)]
310pub enum RunnerStatus {
311 Online,
312 Offline,
313 Stale,
314 NeverContacted,
315 All,
316}
317
318impl Display for RunnerStatus {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 match self {
321 RunnerStatus::Online => write!(f, "online"),
322 RunnerStatus::Offline => write!(f, "offline"),
323 RunnerStatus::Stale => write!(f, "stale"),
324 RunnerStatus::NeverContacted => write!(f, "never_contacted"),
325 RunnerStatus::All => write!(f, "all"),
326 }
327 }
328}
329
330#[derive(Builder, Clone)]
331pub struct Job {
332 id: i64,
333 name: String,
334 branch: String,
335 url: String,
336 author_name: String,
337 commit_sha: String,
338 pipeline_id: i64,
339 runner_tags: Vec<String>,
340 stage: String,
341 status: String,
342 created_at: String,
343 started_at: String,
344 finished_at: String,
345 duration: String,
346}
347
348impl Job {
349 pub fn builder() -> JobBuilder {
350 JobBuilder::default()
351 }
352}
353
354impl From<Job> for DisplayBody {
355 fn from(j: Job) -> DisplayBody {
356 DisplayBody {
357 columns: vec![
358 Column::new("ID", j.id.to_string()),
359 Column::new("Name", j.name),
360 Column::new("Author Name", j.author_name),
361 Column::new("Branch", j.branch),
362 Column::new("Commit SHA", j.commit_sha),
363 Column::new("Pipeline ID", j.pipeline_id.to_string()),
364 Column::new("URL", j.url),
365 Column::new("Runner Tags", j.runner_tags.join(", ")),
366 Column::new("Stage", j.stage),
367 Column::new("Status", j.status),
368 Column::new("Created At", j.created_at),
369 Column::new("Started At", j.started_at),
370 Column::new("Finished At", j.finished_at),
371 Column::new("Duration", j.duration.to_string()),
372 ],
373 }
374 }
375}
376
377impl Timestamp for Job {
378 fn created_at(&self) -> String {
379 self.created_at.clone()
380 }
381}
382
383#[derive(Builder, Clone)]
386pub struct JobListCliArgs {
387 pub list_args: ListRemoteCliArgs,
388}
389
390impl JobListCliArgs {
391 pub fn builder() -> JobListCliArgsBuilder {
392 JobListCliArgsBuilder::default()
393 }
394}
395
396#[derive(Builder, Clone)]
397pub struct JobListBodyArgs {
398 pub list_args: Option<ListBodyArgs>,
399}
400
401impl JobListBodyArgs {
402 pub fn builder() -> JobListBodyArgsBuilder {
403 JobListBodyArgsBuilder::default()
404 }
405}
406
407pub fn execute(
408 options: PipelineOptions,
409 config: Arc<dyn ConfigProperties>,
410 domain: String,
411 path: String,
412) -> Result<()> {
413 match options {
414 PipelineOptions::Lint(args) => {
415 let remote = remote::get_cicd(domain, path, config, None, CacheType::File)?;
417 let file = std::fs::File::open(args.path)?;
418 let body = read_ci_file(file)?;
419 lint_ci_file(remote, &body, false, std::io::stdout())
420 }
421 PipelineOptions::MergedCi => {
422 let remote = remote::get_cicd(domain, path, config, None, CacheType::File)?;
424 let file = std::fs::File::open(".gitlab-ci.yml")?;
425 let body = read_ci_file(file)?;
426 lint_ci_file(remote, &body, true, std::io::stdout())
427 }
428 PipelineOptions::Chart(args) => {
429 let file = std::fs::File::open(".gitlab-ci.yml")?;
430 let body = read_ci_file(file)?;
431 let parser = YamlParser::new(load_yaml(&String::from_utf8_lossy(&body)));
432 let chart = generate_mermaid_stages_diagram(parser, args)?;
433 println!("{chart}");
434 Ok(())
435 }
436 PipelineOptions::List(cli_args) => {
437 let remote = remote::get_cicd(
438 domain,
439 path,
440 config,
441 Some(&cli_args.get_args.cache_args),
442 CacheType::File,
443 )?;
444 if cli_args.num_pages {
445 return num_cicd_pages(remote, std::io::stdout());
446 } else if cli_args.num_resources {
447 return num_cicd_resources(remote, std::io::stdout());
448 }
449 let from_to_args = remote::validate_from_to_page(&cli_args)?;
450 let body_args = PipelineBodyArgs::builder()
451 .from_to_page(from_to_args)
452 .build()?;
453 list_pipelines(remote, body_args, cli_args, std::io::stdout())
454 }
455 PipelineOptions::Jobs(options) => match options {
456 JobOptions::List(cli_args) => {
457 let remote = remote::get_cicd_job(
458 domain,
459 path,
460 config,
461 Some(&cli_args.list_args.get_args.cache_args),
462 CacheType::File,
463 )?;
464 let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
465 let body_args = JobListBodyArgs::builder().list_args(from_to_args).build()?;
466 if cli_args.list_args.num_pages {
467 return num_job_pages(remote, body_args, std::io::stdout());
468 }
469 if cli_args.list_args.num_resources {
470 return num_job_resources(remote, body_args, std::io::stdout());
471 }
472 list_jobs(remote, body_args, cli_args, std::io::stdout())
473 }
474 },
475 PipelineOptions::Runners(options) => match options {
476 RunnerOptions::List(cli_args) => {
477 let remote = remote::get_cicd_runner(
478 domain,
479 path,
480 config,
481 Some(&cli_args.list_args.get_args.cache_args),
482 CacheType::File,
483 )?;
484 let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
485 let tags = cli_args.tags.clone();
486 let body_args = RunnerListBodyArgs::builder()
487 .list_args(from_to_args)
488 .status(cli_args.status)
489 .tags(tags)
490 .all(cli_args.all)
491 .build()?;
492 if cli_args.list_args.num_pages {
493 return num_runner_pages(remote, body_args, std::io::stdout());
494 }
495 if cli_args.list_args.num_resources {
496 return num_runner_resources(remote, body_args, std::io::stdout());
497 }
498 list_runners(remote, body_args, cli_args, std::io::stdout())
499 }
500 RunnerOptions::Get(cli_args) => {
501 let remote = remote::get_cicd_runner(
502 domain,
503 path,
504 config,
505 Some(&cli_args.get_args.cache_args),
506 CacheType::File,
507 )?;
508 get_runner_details(remote, cli_args, std::io::stdout())
509 }
510 RunnerOptions::Create(cli_args) => {
511 let remote = remote::get_cicd_runner(domain, path, config, None, CacheType::None)?;
512 create_runner(remote, cli_args, std::io::stdout())
513 }
514 },
515 }
516}
517
518fn get_runner_details<W: Write>(
519 remote: Arc<dyn CicdRunner>,
520 cli_args: RunnerMetadataGetCliArgs,
521 mut writer: W,
522) -> Result<()> {
523 let runner = remote.get(cli_args.id)?;
524 display::print(&mut writer, vec![runner], cli_args.get_args)?;
525 Ok(())
526}
527
528fn list_runners<W: Write>(
529 remote: Arc<dyn CicdRunner>,
530 body_args: RunnerListBodyArgs,
531 cli_args: RunnerListCliArgs,
532 mut writer: W,
533) -> Result<()> {
534 common::list_runners(remote, body_args, cli_args, &mut writer)
535}
536
537fn list_jobs<W: Write>(
538 remote: Arc<dyn CicdJob>,
539 body_args: JobListBodyArgs,
540 cli_args: JobListCliArgs,
541 mut writer: W,
542) -> Result<()> {
543 common::list_jobs(remote, body_args, cli_args, &mut writer)
544}
545
546fn list_pipelines<W: Write>(
547 remote: Arc<dyn Cicd>,
548 body_args: PipelineBodyArgs,
549 cli_args: ListRemoteCliArgs,
550 mut writer: W,
551) -> Result<()> {
552 common::list_pipelines(remote, body_args, cli_args, &mut writer)
553}
554
555fn read_ci_file<R: Read>(mut reader: R) -> Result<Vec<u8>> {
556 let mut buf = Vec::new();
557 reader.read_to_end(&mut buf)?;
558 Ok(buf)
559}
560
561fn lint_ci_file<W: Write>(
562 remote: Arc<dyn Cicd>,
563 body: &[u8],
564 display_merged_ci_yaml: bool,
565 mut writer: W,
566) -> Result<()> {
567 let response = remote.lint(YamlBytes::new(body))?;
568 if response.valid {
569 if display_merged_ci_yaml {
570 let lines = response.merged_yaml.split('\n');
571 for line in lines {
572 if line.is_empty() {
573 continue;
574 }
575 writeln!(writer, "{line}")?;
576 }
577 return Ok(());
578 }
579 writeln!(writer, "File is valid.")?;
580 } else {
581 for error in response.errors {
582 writeln!(writer, "{error}")?;
583 }
584 return Err(error::gen("Linting failed."));
585 }
586 Ok(())
587}
588
589#[cfg(test)]
590mod test {
591 use std::io::Cursor;
592
593 use super::*;
594 use crate::{api_traits::NumberDeltaErr, error};
595
596 #[derive(Clone, Builder)]
597 struct PipelineMock {
598 #[builder(default = "vec![]")]
599 pipelines: Vec<Pipeline>,
600 #[builder(default = "false")]
601 error: bool,
602 #[builder(setter(into, strip_option), default)]
603 num_pages: Option<u32>,
604 #[builder(default)]
605 gitlab_ci_merged_yaml: String,
606 }
607
608 impl PipelineMock {
609 pub fn builder() -> PipelineMockBuilder {
610 PipelineMockBuilder::default()
611 }
612 }
613
614 impl Cicd for PipelineMock {
615 fn list(&self, _args: PipelineBodyArgs) -> Result<Vec<Pipeline>> {
616 if self.error {
617 return Err(error::gen("Error"));
618 }
619 let pp = self.pipelines.clone();
620 Ok(pp)
621 }
622
623 fn get_pipeline(&self, _id: i64) -> Result<Pipeline> {
624 let pp = self.pipelines.clone();
625 Ok(pp[0].clone())
626 }
627
628 fn num_pages(&self) -> Result<Option<u32>> {
629 if self.error {
630 return Err(error::gen("Error"));
631 }
632 Ok(self.num_pages)
633 }
634
635 fn num_resources(&self) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
636 todo!()
637 }
638
639 fn lint(&self, _body: YamlBytes) -> Result<LintResponse> {
640 if self.error {
641 return Ok(LintResponse::builder()
642 .valid(false)
643 .errors(vec!["YAML Error".to_string()])
644 .build()
645 .unwrap());
646 }
647 Ok(LintResponse::builder()
648 .valid(true)
649 .errors(vec![])
650 .merged_yaml(self.gitlab_ci_merged_yaml.clone())
651 .build()
652 .unwrap())
653 }
654 }
655
656 #[test]
657 fn test_list_pipelines() {
658 let pp_remote = PipelineMock::builder()
659 .pipelines(vec![
660 Pipeline::builder()
661 .id(123)
662 .status("success".to_string())
663 .web_url("https://gitlab.com/owner/repo/-/pipelines/123".to_string())
664 .branch("master".to_string())
665 .sha("1234567890abcdef".to_string())
666 .created_at("2020-01-01T00:00:00Z".to_string())
667 .updated_at("2020-01-01T00:01:00Z".to_string())
668 .duration(60)
669 .build()
670 .unwrap(),
671 Pipeline::builder()
672 .id(456)
673 .status("failed".to_string())
674 .web_url("https://gitlab.com/owner/repo/-/pipelines/456".to_string())
675 .branch("master".to_string())
676 .sha("1234567890abcdef".to_string())
677 .created_at("2020-01-01T00:00:00Z".to_string())
678 .updated_at("2020-01-01T00:01:01Z".to_string())
679 .duration(61)
680 .build()
681 .unwrap(),
682 ])
683 .build()
684 .unwrap();
685 let mut buf = Vec::new();
686 let body_args = PipelineBodyArgs::builder()
687 .from_to_page(None)
688 .build()
689 .unwrap();
690 let cli_args = ListRemoteCliArgs::builder().build().unwrap();
691 list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).unwrap();
692 assert_eq!(
693 String::from_utf8(buf).unwrap(),
694 "ID|URL|Branch|SHA|Created at|Updated at|Duration|Status\n\
695 123|https://gitlab.com/owner/repo/-/pipelines/123|master|1234567890abcdef|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|60|success\n\
696 456|https://gitlab.com/owner/repo/-/pipelines/456|master|1234567890abcdef|2020-01-01T00:00:00Z|2020-01-01T00:01:01Z|61|failed\n")
697 }
698
699 #[test]
700 fn test_list_pipelines_empty_warns_message() {
701 let pp_remote = PipelineMock::builder().build().unwrap();
702 let mut buf = Vec::new();
703
704 let body_args = PipelineBodyArgs::builder()
705 .from_to_page(None)
706 .build()
707 .unwrap();
708 let cli_args = ListRemoteCliArgs::builder().build().unwrap();
709 list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).unwrap();
710 assert_eq!("No resources found.\n", String::from_utf8(buf).unwrap(),)
711 }
712
713 #[test]
714 fn test_pipelines_empty_with_flush_option_no_warn_message() {
715 let pp_remote = PipelineMock::builder().build().unwrap();
716 let mut buf = Vec::new();
717 let body_args = PipelineBodyArgs::builder()
718 .from_to_page(None)
719 .build()
720 .unwrap();
721 let cli_args = ListRemoteCliArgs::builder().flush(true).build().unwrap();
722 list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).unwrap();
723 assert_eq!("", String::from_utf8(buf).unwrap(),)
724 }
725
726 #[test]
727 fn test_list_pipelines_error() {
728 let pp_remote = PipelineMock::builder().error(true).build().unwrap();
729 let mut buf = Vec::new();
730 let body_args = PipelineBodyArgs::builder()
731 .from_to_page(None)
732 .build()
733 .unwrap();
734 let cli_args = ListRemoteCliArgs::builder().build().unwrap();
735 assert!(list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).is_err());
736 }
737
738 #[test]
739 fn test_list_number_of_pipelines_pages() {
740 let pp_remote = PipelineMock::builder().num_pages(3_u32).build().unwrap();
741 let mut buf = Vec::new();
742 num_cicd_pages(Arc::new(pp_remote), &mut buf).unwrap();
743 assert_eq!("3\n", String::from_utf8(buf).unwrap(),)
744 }
745
746 #[test]
747 fn test_no_pages_available() {
748 let pp_remote = PipelineMock::builder().build().unwrap();
749 let mut buf = Vec::new();
750 num_cicd_pages(Arc::new(pp_remote), &mut buf).unwrap();
751 assert_eq!(
752 "Number of pages not available.\n",
753 String::from_utf8(buf).unwrap(),
754 )
755 }
756
757 #[test]
758 fn test_number_of_pages_error() {
759 let pp_remote = PipelineMock::builder().error(true).build().unwrap();
760 let mut buf = Vec::new();
761 assert!(num_cicd_pages(Arc::new(pp_remote), &mut buf).is_err());
762 }
763
764 #[test]
765 fn test_list_pipelines_no_headers() {
766 let pp_remote = PipelineMock::builder()
767 .pipelines(vec![
768 Pipeline::builder()
769 .id(123)
770 .status("success".to_string())
771 .web_url("https://gitlab.com/owner/repo/-/pipelines/123".to_string())
772 .branch("master".to_string())
773 .sha("1234567890abcdef".to_string())
774 .created_at("2020-01-01T00:00:00Z".to_string())
775 .updated_at("2020-01-01T00:01:00Z".to_string())
776 .duration(60)
777 .build()
778 .unwrap(),
779 Pipeline::builder()
780 .id(456)
781 .status("failed".to_string())
782 .web_url("https://gitlab.com/owner/repo/-/pipelines/456".to_string())
783 .branch("master".to_string())
784 .sha("1234567890abcdef".to_string())
785 .created_at("2020-01-01T00:00:00Z".to_string())
786 .updated_at("2020-01-01T00:01:00Z".to_string())
787 .duration(60)
788 .build()
789 .unwrap(),
790 ])
791 .build()
792 .unwrap();
793 let mut buf = Vec::new();
794 let body_args = PipelineBodyArgs::builder()
795 .from_to_page(None)
796 .build()
797 .unwrap();
798 let cli_args = ListRemoteCliArgs::builder()
799 .get_args(
800 GetRemoteCliArgs::builder()
801 .no_headers(true)
802 .build()
803 .unwrap(),
804 )
805 .build()
806 .unwrap();
807 list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).unwrap();
808 assert_eq!(
809 "123|https://gitlab.com/owner/repo/-/pipelines/123|master|1234567890abcdef|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|60|success\n\
810 456|https://gitlab.com/owner/repo/-/pipelines/456|master|1234567890abcdef|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|60|failed\n",
811 String::from_utf8(buf).unwrap(),
812 )
813 }
814
815 #[derive(Builder, Clone)]
816 struct RunnerMock {
817 #[builder(default = "vec![]")]
818 runners: Vec<Runner>,
819 #[builder(default)]
820 error: bool,
821 #[builder(default)]
822 one_runner: Option<RunnerMetadata>,
823 }
824
825 impl RunnerMock {
826 pub fn builder() -> RunnerMockBuilder {
827 RunnerMockBuilder::default()
828 }
829 }
830
831 impl CicdRunner for RunnerMock {
832 fn list(&self, _args: RunnerListBodyArgs) -> Result<Vec<Runner>> {
833 if self.error {
834 return Err(error::gen("Error"));
835 }
836 let rr = self.runners.clone();
837 Ok(rr)
838 }
839
840 fn get(&self, _id: i64) -> Result<RunnerMetadata> {
841 let rr = self.one_runner.as_ref().unwrap();
842 Ok(rr.clone())
843 }
844
845 fn num_pages(&self, _args: RunnerListBodyArgs) -> Result<Option<u32>> {
846 if self.error {
847 return Err(error::gen("Error"));
848 }
849 Ok(None)
850 }
851
852 fn num_resources(
853 &self,
854 _args: RunnerListBodyArgs,
855 ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
856 todo!()
857 }
858
859 fn create(&self, _args: RunnerPostDataCliArgs) -> Result<RunnerRegistrationResponse> {
860 Ok(RunnerRegistrationResponse::builder()
861 .id(1)
862 .token("token".to_string())
863 .token_expiration("2020-01-01T00:00:00Z".to_string())
864 .build()
865 .unwrap())
866 }
867 }
868
869 #[test]
870 fn test_list_runners() {
871 let runners = vec![
872 Runner::builder()
873 .id(1)
874 .active(true)
875 .description("Runner 1".to_string())
876 .ip_address("10.0.0.1".to_string())
877 .name("runner1".to_string())
878 .online(true)
879 .status("online".to_string())
880 .paused(false)
881 .is_shared(true)
882 .runner_type("shared".to_string())
883 .build()
884 .unwrap(),
885 Runner::builder()
886 .id(2)
887 .active(true)
888 .description("Runner 2".to_string())
889 .ip_address("10.0.0.2".to_string())
890 .name("runner2".to_string())
891 .online(true)
892 .status("online".to_string())
893 .paused(false)
894 .is_shared(true)
895 .runner_type("shared".to_string())
896 .build()
897 .unwrap(),
898 ];
899 let remote = RunnerMock::builder().runners(runners).build().unwrap();
900 let mut buf = Vec::new();
901 let body_args = RunnerListBodyArgs::builder()
902 .list_args(None)
903 .status(RunnerStatus::Online)
904 .build()
905 .unwrap();
906 let cli_args = RunnerListCliArgs::builder()
907 .status(RunnerStatus::Online)
908 .list_args(ListRemoteCliArgs::builder().build().unwrap())
909 .build()
910 .unwrap();
911 list_runners(Arc::new(remote), body_args, cli_args, &mut buf).unwrap();
912 assert_eq!(
913 "ID|Active|Description|IP Address|Name|Paused|Shared|Type|Online|Status\n\
914 1|true|Runner 1|10.0.0.1|runner1|false|true|shared|true|online\n\
915 2|true|Runner 2|10.0.0.2|runner2|false|true|shared|true|online\n",
916 String::from_utf8(buf).unwrap()
917 )
918 }
919
920 #[test]
921 fn test_no_runners_warn_user_with_message() {
922 let remote = RunnerMock::builder().build().unwrap();
923 let mut buf = Vec::new();
924 let body_args = RunnerListBodyArgs::builder()
925 .list_args(None)
926 .status(RunnerStatus::Online)
927 .build()
928 .unwrap();
929 let cli_args = RunnerListCliArgs::builder()
930 .status(RunnerStatus::Online)
931 .list_args(ListRemoteCliArgs::builder().build().unwrap())
932 .build()
933 .unwrap();
934 list_runners(Arc::new(remote), body_args, cli_args, &mut buf).unwrap();
935 assert_eq!("No resources found.\n", String::from_utf8(buf).unwrap())
936 }
937
938 #[test]
939 fn test_no_runners_found_with_flush_option_no_warn_message() {
940 let remote = RunnerMock::builder().build().unwrap();
941 let mut buf = Vec::new();
942 let body_args = RunnerListBodyArgs::builder()
943 .list_args(None)
944 .status(RunnerStatus::Online)
945 .build()
946 .unwrap();
947 let cli_args = RunnerListCliArgs::builder()
948 .status(RunnerStatus::Online)
949 .list_args(ListRemoteCliArgs::builder().flush(true).build().unwrap())
950 .build()
951 .unwrap();
952 list_runners(Arc::new(remote), body_args, cli_args, &mut buf).unwrap();
953 assert_eq!("", String::from_utf8(buf).unwrap())
954 }
955
956 #[test]
957 fn test_get_gitlab_runner_metadata() {
958 let runner_metadata = RunnerMetadata::builder()
959 .id(1)
960 .run_untagged(true)
961 .tag_list(vec!["tag1".to_string(), "tag2".to_string()])
962 .version("13.0.0".to_string())
963 .architecture("amd64".to_string())
964 .platform("linux".to_string())
965 .contacted_at("2020-01-01T00:00:00Z".to_string())
966 .revision("1234567890abcdef".to_string())
967 .build()
968 .unwrap();
969 let remote = RunnerMock::builder()
970 .one_runner(Some(runner_metadata))
971 .build()
972 .unwrap();
973 let mut buf = Vec::new();
974 let cli_args = RunnerMetadataGetCliArgs::builder()
975 .id(1)
976 .get_args(GetRemoteCliArgs::builder().build().unwrap())
977 .build()
978 .unwrap();
979 get_runner_details(Arc::new(remote), cli_args, &mut buf).unwrap();
980 assert_eq!(
981 "ID|Run untagged|Tags|Architecture|Platform|Contacted at|Version|Revision\n\
982 1|true|tag1, tag2|amd64|linux|2020-01-01T00:00:00Z|13.0.0|1234567890abcdef\n",
983 String::from_utf8(buf).unwrap()
984 )
985 }
986
987 fn gen_gitlab_ci_body() -> Vec<u8> {
988 b"image: alpine\n\
989 stages:\n\
990 - build\n\
991 - test\n\
992 build:\n\
993 stage: build\n\
994 script:\n\
995 - echo \"Building\"\n\
996 test:\n\
997 stage: test\n\
998 script:\n\
999 - echo \"Testing\"\n"
1000 .to_vec()
1001 }
1002
1003 #[test]
1004 fn test_read_gitlab_ci_file_contents() {
1005 let expected_body = gen_gitlab_ci_body();
1006 let buf = Cursor::new(&expected_body);
1007 let body = read_ci_file(buf).unwrap();
1008 assert_eq!(*expected_body, *body);
1009 }
1010
1011 #[test]
1012 fn test_lint_ci_file_success() {
1013 let mock_cicd = Arc::new(PipelineMock::builder().build().unwrap());
1014 let mut writer = Vec::new();
1015 let result = lint_ci_file(mock_cicd, &gen_gitlab_ci_body(), false, &mut writer);
1016 assert!(result.is_ok());
1017 assert_eq!(String::from_utf8(writer).unwrap(), "File is valid.\n");
1018 }
1019
1020 #[test]
1021 fn test_lint_ci_file_has_errors_prints_errors() {
1022 let mock_cicd = Arc::new(PipelineMock::builder().error(true).build().unwrap());
1023 let mut writer = Vec::new();
1024 let result = lint_ci_file(mock_cicd, &gen_gitlab_ci_body(), false, &mut writer);
1025 assert!(result.is_err());
1026 assert_eq!(String::from_utf8(writer).unwrap(), "YAML Error\n");
1027 }
1028
1029 #[test]
1030 fn test_get_merged_yaml_from_lint_response() {
1031 let response = LintResponse::builder()
1032 .valid(true)
1033 .merged_yaml("image: alpine\nstages:\n - build\n - test\nbuild:\n stage: build\n script:\n - echo \"Building\"\ntest:\n stage: test\n script:\n - echo \"Testing\"\n".to_string())
1034 .errors(vec![])
1035 .build()
1036 .unwrap();
1037 let mut writer = Vec::new();
1038 let mock_cicd = Arc::new(
1039 PipelineMock::builder()
1040 .gitlab_ci_merged_yaml(response.merged_yaml)
1041 .build()
1042 .unwrap(),
1043 );
1044
1045 let result = lint_ci_file(mock_cicd, &gen_gitlab_ci_body(), true, &mut writer);
1046 assert!(result.is_ok());
1047 let merged_gitlab_ci = r#"image: alpine
1048stages:
1049 - build
1050 - test
1051build:
1052 stage: build
1053 script:
1054 - echo "Building"
1055test:
1056 stage: test
1057 script:
1058 - echo "Testing"
1059"#;
1060 assert_eq!(merged_gitlab_ci, String::from_utf8(writer).unwrap());
1061 }
1062
1063 #[derive(Builder)]
1064 struct JobMock {
1065 #[builder(default)]
1066 jobs: Vec<Job>,
1067 #[builder(default)]
1068 error: bool,
1069 #[builder(default)]
1070 num_pages: Option<u32>,
1071 }
1072
1073 impl JobMock {
1074 pub fn builder() -> JobMockBuilder {
1075 JobMockBuilder::default()
1076 }
1077 }
1078
1079 impl CicdJob for JobMock {
1080 fn list(&self, _args: JobListBodyArgs) -> Result<Vec<Job>> {
1081 if self.error {
1082 return Err(error::gen("Error"));
1083 }
1084 let jj = self.jobs.clone();
1085 Ok(jj)
1086 }
1087
1088 fn num_pages(&self, _args: JobListBodyArgs) -> Result<Option<u32>> {
1089 if self.error {
1090 return Err(error::gen("Error"));
1091 }
1092 Ok(self.num_pages)
1093 }
1094
1095 fn num_resources(&self, _args: JobListBodyArgs) -> Result<Option<NumberDeltaErr>> {
1096 todo!()
1097 }
1098 }
1099
1100 #[test]
1101 fn test_list_pipeline_jobs() {
1102 let jobs = vec![
1103 Job::builder()
1104 .id(1)
1105 .name("job1".to_string())
1106 .branch("main".to_string())
1107 .author_name("user1".to_string())
1108 .commit_sha("1234567890abcdef".to_string())
1109 .pipeline_id(1)
1110 .url("https://gitlab.com/owner/repo/-/jobs/1".to_string())
1111 .runner_tags(vec!["tag1".to_string(), "tag2".to_string()])
1112 .stage("build".to_string())
1113 .status("success".to_string())
1114 .created_at("2020-01-01T00:00:00Z".to_string())
1115 .started_at("2020-01-01T00:01:00Z".to_string())
1116 .finished_at("2020-01-01T00:01:30Z".to_string())
1117 .duration("25".to_string())
1118 .build()
1119 .unwrap(),
1120 Job::builder()
1121 .id(2)
1122 .name("job2".to_string())
1123 .branch("main".to_string())
1124 .author_name("user2".to_string())
1125 .commit_sha("1234567890abcdef".to_string())
1126 .pipeline_id(1)
1127 .url("https://gitlab.com/owner/repo/-/jobs/2".to_string())
1128 .runner_tags(vec!["tag1".to_string(), "tag2".to_string()])
1129 .stage("test".to_string())
1130 .status("failed".to_string())
1131 .created_at("2020-01-01T00:00:00Z".to_string())
1132 .started_at("2020-01-01T00:01:00Z".to_string())
1133 .finished_at("2020-01-01T00:01:30Z".to_string())
1134 .duration("30".to_string())
1135 .build()
1136 .unwrap(),
1137 ];
1138 let remote = JobMock::builder().jobs(jobs).build().unwrap();
1139 let mut buf = Vec::new();
1140 let body_args = JobListBodyArgs::builder().list_args(None).build().unwrap();
1141 let cli_args = JobListCliArgs::builder()
1142 .list_args(ListRemoteCliArgs::builder().build().unwrap())
1143 .build()
1144 .unwrap();
1145 list_jobs(Arc::new(remote), body_args, cli_args, &mut buf).unwrap();
1146 assert_eq!(
1147"ID|Name|Author Name|Branch|Commit SHA|Pipeline ID|URL|Runner Tags|Stage|Status|Created At|Started At|Finished At|Duration\n1|job1|user1|main|1234567890abcdef|1|https://gitlab.com/owner/repo/-/jobs/1|tag1, tag2|build|success|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|2020-01-01T00:01:30Z|25\n2|job2|user2|main|1234567890abcdef|1|https://gitlab.com/owner/repo/-/jobs/2|tag1, tag2|test|failed|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|2020-01-01T00:01:30Z|30\n",
1148 String::from_utf8(buf).unwrap()
1149 );
1150 }
1151
1152 #[test]
1153 fn test_create_new_runner() {
1154 let remote = RunnerMock::builder().build().unwrap();
1155 let mut buf = Vec::new();
1156 let cli_args = RunnerPostDataCliArgs::builder()
1157 .description(Some("Runner 1".to_string()))
1158 .tags(Some("tag1,tag2".to_string()))
1159 .kind(RunnerType::Instance)
1160 .build()
1161 .unwrap();
1162 create_runner(Arc::new(remote), cli_args, &mut buf).unwrap();
1163 assert_eq!(
1164 "Runner ID: [1], Runner Token: [token], Token Expiration: [2020-01-01T00:00:00Z]\n",
1165 String::from_utf8(buf).unwrap()
1166 )
1167 }
1168}