Grcov report - container_build.rs

1

    use std::io::{

2

      BufRead as _,

3

      BufReader,

4

    };

5

    use std::path::Path;

6

    use std::process::Command as ProcessCommand;

7

    use std::thread;

8

9

    use anyhow::Context as _;

10

    use git2::Repository;

11

    use serde::Deserialize;

12

    use which::which;

13

14

    use crate::defaults::{

15

      default_shell,

16

      default_verbose,

17

    };

18

    use crate::schema::{

19

      get_output_handler,

20

      is_shell_command,

21

      is_template_command,

22

      TaskContext,

23

    };

24

    use crate::{

25

      get_template_command_value,

26

      handle_output,

27

      run_shell_command,

28

    };

29

30

    #[derive(Debug, Deserialize)]

31

    pub struct ContainerBuildArgs {

32

      /// The image name to build

33

      pub image_name: String,

34

35

      /// Defines the path to a directory to build the container

36

      pub context: String,

37

38

      /// The containerfile or dockerfile to use

39

      #[serde(default)]

40

      pub containerfile: Option<String>,

41

42

      /// The tags to apply to the container image

43

      #[serde(default)]

44

      pub tags: Option<Vec<String>>,

45

46

      /// Build arguments to pass to the container

47

      #[serde(default)]

48

      pub build_args: Option<Vec<String>>,

49

50

      /// Labels to apply to the container image

51

      #[serde(default)]

52

      pub labels: Option<Vec<String>>,

53

54

      /// Generate a Software Bill of Materials (SBOM) for the container image

55

      #[serde(default)]

56

      pub sbom: bool,

57

58

      /// Do not use cache when building the container

59

      #[serde(default)]

60

      pub no_cache: bool,

61

62

      /// Always remove intermediate containers

63

      #[serde(default)]

64

      pub force_rm: bool,

65

    }

66

67

    #[derive(Debug, Deserialize)]

68

    pub struct ContainerBuild {

69

      /// The command to run in the container

70

      pub container_build: ContainerBuildArgs,

71

72

      /// Show verbose output

73

      #[serde(default)]

74

      pub verbose: Option<bool>,

75

    }

76

77

    #[allow(dead_code)]

78

    impl ContainerBuild {

79

      pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {

80

        assert!(!self.container_build.context.is_empty());

81

82

        let verbose = self.verbose.or(context.verbose).unwrap_or(default_verbose());

83

84

        let stdout = get_output_handler(verbose);

85

        let stderr = get_output_handler(verbose);

86

87

        let container_runtime = which("docker")

88

          .or_else(|_| which("podman"))

89

          .with_context(|| "Failed to find docker or podman")?;

90

91

        let mut cmd = ProcessCommand::new(container_runtime);

92

        cmd.arg("build").stdout(stdout).stderr(stderr);

93

94

        if self.container_build.sbom {

95

          cmd.arg("--sbom=true");

96

        }

97

98

        if self.container_build.no_cache {

99

          cmd.arg("--no-cache=true");

100

        }

101

102

        if self.container_build.force_rm {

103

          cmd.arg("--force-rm=true");

104

        }

105

106

        if let Some(build_args) = &self.container_build.build_args {

107

          for arg in build_args {

108

            cmd.arg("--build-arg").arg(arg);

109

          }

110

        }

111

112

        if let Some(labels) = &self.container_build.labels {

113

          for label in labels {

114

            let label = self.get_label(context, label.trim())?;

115

            cmd.arg("--label").arg(label);

116

          }

117

        }

118

119

        if let Some(tags) = &self.container_build.tags {

120

          for tag in tags {

121

            let tag = self.get_tag(context, tag.trim())?;

122

            let tag = format!("{}:{}", &self.container_build.image_name, tag);

123

            cmd.arg("-t").arg(tag);

124

          }

125

        } else {

126

          let tag = format!("{}:latest", &self.container_build.image_name);

127

          cmd.arg("-t").arg(tag);

128

        }

129

130

        if let Some(containerfile) = &self.container_build.containerfile {

131

          cmd.arg("-f").arg(containerfile);

132

        } else {

133

          let dockerfile = format!("{}/Dockerfile", &self.container_build.context);

134

          let containerfile = format!("{}/Containerfile", &self.container_build.context);

135

136

          // Check for Dockerfile and Containerfile

137

          if Path::new(&dockerfile).exists() {

138

            cmd.arg("-f").arg(dockerfile);

139

          } else if Path::new(&containerfile).exists() {

140

            cmd.arg("-f").arg(containerfile);

141

          } else {

142

            anyhow::bail!("Failed to find Dockerfile or Containerfile in context");

143

          }

144

        }

145

146

        let build_path: &str = &self.container_build.context;

147

        cmd.arg(build_path);

148

149

        let cmd_str = format!("{:?}", cmd);

150

        context.multi.println(cmd_str)?;

151

152

        // Inject environment variables in both container and command

153

        for (key, value) in context.env_vars.iter() {

154

          cmd.env(key, value);

155

        }

156

157

        log::trace!("Running command: {:?}", cmd);

158

159

        let mut cmd = cmd.spawn()?;

160

        if verbose {

161

          handle_output!(cmd.stdout, context);

162

          handle_output!(cmd.stderr, context);

163

        }

164

165

        let status = cmd.wait()?;

166

        if !status.success() {

167

          anyhow::bail!("Container build failed");

168

        }

169

170

        Ok(())

171

      }

172

173

      fn shell(&self, context: &TaskContext) -> String {

174

        context.shell.clone().unwrap_or(default_shell())

175

      }

176

177

      fn get_tag(&self, context: &TaskContext, tag_in: &str) -> anyhow::Result<String> {

178

        let verbose = self.verbose.or(context.verbose).unwrap_or(default_verbose());

179

180

        if is_shell_command(tag_in)? {

181

          let shell: &str = &self.shell(context);

182

          let output = run_shell_command!(tag_in, shell, verbose);

183

          Ok(output)

184

        } else if is_template_command(tag_in)? {

185

          let output = get_template_command_value!(tag_in, context);

186

          Ok(output)

187

        } else {

188

          Ok(tag_in.to_string())

189

        }

190

      }

191

192

      fn get_label(&self, context: &TaskContext, label_in: &str) -> anyhow::Result<String> {

193

        use chrono::prelude::*;

194

195

        let verbose = self.verbose.or(context.verbose).unwrap_or(default_verbose());

196

197

        if let Some((key, value)) = label_in.split_once('=') {

198

          match value {

199

            "MK_NOW" => {

200

              // Create formatted time in +%Y-%m-%dT%H:%M:%S%z format

201

              let now: DateTime<Local> = Local::now();

202

              let now = now.format("%Y-%m-%dT%H:%M:%S%z").to_string();

203

              Ok(format!("{}={}", key, now))

204

            },

205

            "MK_GIT_REVISION" => {

206

              let revision = self.get_git_revision().unwrap_or_else(|_| "unknown".to_string());

207

              Ok(format!("{}={}", key, revision))

208

            },

209

            "MK_GIT_REMOTE_ORIGIN" => {

210

              let remote_url = self

211

                .get_git_remote_origin()

212

                .unwrap_or_else(|_| "unknown".to_string());

213

              Ok(format!("{}={}", key, remote_url))

214

            },

215

            _ => {

216

              let value = if is_shell_command(value)? {

217

                let shell: &str = &context.shell.clone().unwrap_or(default_shell());

218

                run_shell_command!(value, shell, verbose)

219

              } else if is_template_command(value)? {

220

                get_template_command_value!(value, context)

221

              } else {

222

                value.to_string()

223

              };

224

225

              Ok(format!("{}={}", key, value))

226

            },

227

          }

228

        } else {

229

          Ok(label_in.to_string())

230

        }

231

      }

232

233

      fn get_git_revision(&self) -> anyhow::Result<String> {

234

        let repo = Repository::open(".").context("Failed to open git repository")?;

235

        let head = repo.head().context("Failed to get git HEAD reference")?;

236

        let commit = head

237

          .peel_to_commit()

238

          .context("Failed to resolve git HEAD commit")?;

239

        Ok(commit.id().to_string())

240

      }

241

242

      fn get_git_remote_origin(&self) -> anyhow::Result<String> {

243

        let repo = Repository::open(".").context("Failed to open git repository")?;

244

        let remote = repo

245

          .find_remote("origin")

246

          .context("Failed to find git remote origin")?;

247

        let url = remote.url().context("Failed to get git remote URL")?;

248

        Ok(url.to_string())

249

      }

250

    }

251

252

    #[cfg(test)]

253

    mod test {

254

      use anyhow::Ok;

255

256

      use super::*;

257
18

258
18

      #[test]

259
20

      fn test_container_build_1() -> anyhow::Result<()> {

260
20

        let yaml = r#"

261
20

          container_build:

262
20

            image_name: my-image

263
20

            context: .

264
20

            tags:

265
20

              - latest

266
20

            labels:

267
20

              - "org.opencontainers.image.created=MK_NOW"

268
20

              - "org.opencontainers.image.revision=MK_GIT_REVISION"

269
20

              - "org.opencontainers.image.source=MK_GIT_REMOTE_ORIGIN"

270
20

            sbom: true

271
20

            no_cache: true

272
20

            force_rm: true

273
20

          verbose: false

274
20

        "#;

275
20

        let container_build = serde_yaml::from_str::<ContainerBuild>(yaml)?;

276
18

277
20

        assert_eq!(container_build.verbose, Some(false));

278
20

        assert_eq!(container_build.container_build.image_name, "my-image");

279
20

        assert_eq!(container_build.container_build.context, ".");

280
20

        assert_eq!(

281
20

          container_build.container_build.tags,

282
20

          Some(vec!["latest".to_string()])

283
20

        );

284
20

        assert_eq!(

285
20

          container_build.container_build.labels,

286
20

          Some(vec![

287
20

            "org.opencontainers.image.created=MK_NOW".to_string(),

288
20

            "org.opencontainers.image.revision=MK_GIT_REVISION".to_string(),

289
20

            "org.opencontainers.image.source=MK_GIT_REMOTE_ORIGIN".to_string(),

290
20

          ])

291
20

        );

292
20

        assert!(container_build.container_build.sbom);

293
20

        assert!(container_build.container_build.no_cache);

294
2

        assert!(container_build.container_build.force_rm);

295
2

        Ok(())

296
20

      }

297
18

298
18

      #[test]

299
20

      fn test_container_build_2() -> anyhow::Result<()> {

300
20

        let yaml = r#"

301
20

          container_build:

302
20

            image_name: my-image

303
20

            context: .

304
20

        "#;

305
20

        let container_build = serde_yaml::from_str::<ContainerBuild>(yaml)?;

306
18

307
20

        assert_eq!(container_build.verbose, None);

308
20

        assert_eq!(container_build.container_build.image_name, "my-image");

309
20

        assert_eq!(container_build.container_build.context, ".");

310
20

        assert_eq!(container_build.container_build.tags, None,);

311
20

        assert_eq!(container_build.container_build.labels, None,);

312
20

        assert!(!container_build.container_build.sbom);

313
2

        assert!(!container_build.container_build.no_cache);

314
2

        assert!(!container_build.container_build.force_rm);

315
18

316
20

        Ok(())

317
20

      }

318
18

319
18

      #[test]

320
20

      fn test_container_build_3() -> anyhow::Result<()> {

321
20

        let yaml = r#"

322
20

          container_build:

323
20

            image_name: docker.io/my-image/my-image

324
20

            context: /hello/world

325
20

        "#;

326
20

        let container_build = serde_yaml::from_str::<ContainerBuild>(yaml)?;

327
18

328
20

        assert_eq!(container_build.verbose, None);

329
20

        assert_eq!(

330
20

          container_build.container_build.image_name,

331
20

          "docker.io/my-image/my-image"

332
20

        );

333
20

        assert_eq!(container_build.container_build.context, "/hello/world");

334
20

        assert_eq!(container_build.container_build.tags, None,);

335
2

        assert_eq!(container_build.container_build.labels, None,);

336
2

        assert!(!container_build.container_build.sbom);

337
2

        assert!(!container_build.container_build.no_cache);

338
2

        assert!(!container_build.container_build.force_rm);

339

340
2

        Ok(())

341
2

      }

342

    }