1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use std::process::Command;

use lxd::{Container, Image, Location};
use serde_json;
use tempdir::TempDir;

use {Config, Sha384, Source, Store, sign_manifest};

/// A temporary structure used to generate a unique build environment
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
struct BuildEnvironmentConfig {
    /// The LXC base to use
    pub base: String,
    /// The commands to run to generate a build environment
    pub prepare: Vec<Vec<String>>,
}

fn prepare(config: &Config, location: &Location) -> io::Result<String> {
    let build_json = serde_json::to_string(&BuildEnvironmentConfig {
        base: config.base.clone(),
        prepare: config.prepare.clone(),
    }).map_err(|err| {
        io::Error::new(io::ErrorKind::Other, err)
    })?;

    let build_sha = Sha384::new(&mut build_json.as_bytes()).map_err(|err| {
        io::Error::new(io::ErrorKind::Other, err)
    })?;

    let build_sha_str = serde_json::to_string(&build_sha).map_err(|err| {
        io::Error::new(io::ErrorKind::Other, err)
    })?;

    let container_name = format!("buildchain-{}-prepare", config.name);
    let build_image = format!("buildchain-{}-{}", config.name, build_sha_str.trim_matches('"'));

    if Image::new(location.clone(), &build_image).is_ok() {
        println!("Build environment cached as {}", build_image);
    } else {
        let mut container = if config.privileged {
            println!("Create privileged container {} from {}", container_name, &config.base);
            unsafe { Container::new_privileged(location.clone(), &container_name, &config.base)? }
        } else {
            println!("Create container {} from {}", container_name, &config.base);
            Container::new(location.clone(), &container_name, &config.base)?
        };

        for command in config.prepare.iter() {
            let mut args = vec![];
            for arg in command.iter() {
                args.push(arg.as_str());
            }

            println!("Prepare command {:?}", args);
            container.exec(&args)?;
        }

        println!("Snapshot build environment as {}", build_image);
        let snapshot = container.snapshot(&build_image)?;

        println!("Publish build environment as {}", build_image);
        snapshot.publish(&build_image)?;
    }

    Ok(build_image)
}

fn run<P: AsRef<Path>, Q: AsRef<Path>>(config: &Config, location: &Location, build_image: &str, source_path: P, temp_path: Q) -> io::Result<()> {
    let source_path = source_path.as_ref();
    let temp_path = temp_path.as_ref();

    let container_name = format!("buildchain-{}-build", config.name);

    let mut container = if config.privileged {
        println!("Create privileged container {} from {}", container_name, build_image);
        unsafe { Container::new_privileged(location.clone(), &container_name, build_image)? }
    } else {
        println!("Create container {} from {}", container_name, build_image);
        Container::new(location.clone(), &container_name, build_image)?
    };

    println!("Push source");
    container.push(source_path, "/root", true)?;

    for command in config.build.iter() {
        let mut args = Vec::new();
        for arg in command.iter() {
            args.push(arg.as_str());
        }

        println!("Build command {:?}", args);
        container.exec(&args)?;
    }

    println!("Create artifact directory");
    container.exec(&["mkdir", "/root/artifacts"])?;

    for command in config.publish.iter() {
        let mut args = Vec::new();
        for arg in command.iter() {
            args.push(arg.as_str());
        }

        println!("Publish command {:?}", args);
        container.exec(&args)?;
    }

    println!("Pull artifacts");
    container.pull("/root/artifacts", temp_path, true)?;

    Ok(())
}

fn archive<P: AsRef<Path>, Q: AsRef<Path>>(source_path: P, dest_path: Q) -> io::Result<()> {
    let source_path = source_path.as_ref();
    let dest_path = dest_path.as_ref();

    let status = Command::new("tar")
        .arg("--create")
        .arg("--verbose")
        .arg("--sort=name")
        .arg("--owner=0")
        .arg("--group=0")
        .arg("--numeric-owner")
        .arg("--file").arg(dest_path)
        .arg("--directory").arg(source_path)
        .arg(".")
        .status()?;

    if status.success() {
        Ok(())
    } else {
        Err(io::Error::new(
            io::ErrorKind::Other,
            format!("tar failed with status: {}", status)
        ))
    }
}

pub struct BuildArguments<'a> {
    pub config_path: &'a str,
    pub output_path: &'a str,
    pub project_name: &'a str,
    pub branch_name: &'a str,
    pub remote_opt: Option<&'a str>,
    pub source_url: &'a str,
    pub source_kind: &'a str,
    pub use_pihsm: bool,
}


pub fn build<'a>(args: BuildArguments<'a>) -> Result<(), String> {
    let config_path = args.config_path;

    let temp_dir = match TempDir::new("buildchain") {
        Ok(dir) => dir,
        Err(err) => {
            return Err(format!("failed to create temporary directory: {}", err));
        }
    };

    let source = Source {
        kind: args.source_kind.to_string(),
        url: args.source_url.to_string()
    };

    let source_path = temp_dir.path().join("source");

    let source_time = match source.download(&source_path) {
        Ok(time) => time,
        Err(err) => {
            return Err(format!("failed to download source {:?}: {}", source, err));
        }
    };

    let mut file = match File::open(&source_path.join(&config_path)) {
        Ok(file) => file,
        Err(err) => {
            return Err(format!("failed to open config {}: {}", config_path, err));
        }
    };

    let mut string = String::new();
    match file.read_to_string(&mut string) {
        Ok(_) => (),
        Err(err) => {
            return Err(format!("failed to read config {}: {}", config_path, err));
        }
    }

    let config = match serde_json::from_str::<Config>(&string) {
        Ok(config) => config,
        Err(err) => {
            return Err(format!("failed to parse config {}: {}", config_path, err));
        }
    };

    let location = if let Some(remote) = args.remote_opt {
        println!("buildchain: building {} on {}", config.name, remote);
        Location::Remote(remote.to_string())
    } else {
        println!("buildchain: building {} locally", config.name);
        Location::Local
    };

    let build_image = match prepare(&config, &location) {
        Ok(build_image) => build_image,
        Err(err) => {
            return Err(format!("failed to prepare config {}: {}", config_path, err));
        }
    };

    match run(&config, &location, &build_image, &source_path, &temp_dir.path()) {
        Ok(()) => (),
        Err(err) => {
            return Err(format!("failed to run config {}: {}", config_path, err));
        }
    }

    let store = Store::new(&temp_dir);
    let manifest = store.import_artifacts(source_time)?;
    let manifest_bytes = match serde_json::to_vec_pretty(&manifest) {
        Ok(bytes) => bytes,
        Err(err) => {
            return Err(format!("failed to serialize manifest: {}", err));
        }
    };
    store.write_manifest(&manifest_bytes)?;
    if args.use_pihsm {
        let response = match sign_manifest(&manifest_bytes) {
            Ok(response) => response,
            Err(err) => {
                return Err(format!("failed to sign manifest: {}", err));
            }
        };
        store.write_tail(args.project_name, args.branch_name, &response)?;
    }
    store.remove_tmp_dir()?;

    match archive(&temp_dir, &args.output_path) {
        Ok(()) => {
            println!("buildchain: placed results in {}", args.output_path);
        },
        Err(err) => {
            return Err(format!("failed to move temporary directory {}: {}", temp_dir.as_ref().display(), err));
        }
    }

    Ok(())
}