Fork me on GitHub

plash build


Usage

plash build --macro1 ar1 arg2 --macro2 arg1 ...

Description

Builds an image. Command line options are evaluated as macros. Use `plash
help-macros` to list all available macros.

Example


$ plash build -f ubuntu --run 'touch a'
--> touch a
--:
66

$ plash build -f ubuntu:warty --run 'touch a' --layer --run 'touch b'
--> touch b
--:
67

$ plash build -f ubuntu:warty --apt nmap
--> apt-get update
Hit:1 http://security.ubuntu.com/ubuntu bionic-security InRelease
Get:2 http://archive.ubuntu.com/ubuntu bionic InRelease [235 kB]
<snip>
Setting up nmap (7.60-1ubuntu2) ...
Processing triggers for libc-bin (2.26-0ubuntu2) ...
--:
68

Tested Behaviour

#!/bin/bash
set -xeu

• Building an image with a simple macro succeeds

plash build -f 1 --invalidate-layer

• Building an image with with multiple layers succeeds

id=$(plash build -f 1 -l --invalidate-layer -l --invalidate-layer -l --invalidate-layer)
plash nodepath $id
plash nodepath $id | grep -E "^$(plash data)/layer/0/[0-9]+/[0-9]+/[0-9]+/[0-9]+$"


• A file created while building indeed appears in the builded image

cont=$(plash build -f 1 --invalidate-layer)
newcont=$(plash build -f $cont -x 'touch /hellow')
plash run $newcont stat /hellow

• Building two times the same leads to the second build being cached

cont=$(plash build -f 1 --invalidate-layer)
stderr=$(mktemp)
plash build -f $cont -x 'touch /a' 2> $stderr
test -s $stderr # its not empty
plash build -f $cont -x 'touch /a' 2> $stderr
(! test -s $stderr) # its empty

• If the actual build command fails with any non-zero exit code, `plash build` return the exit code 1

cont=$(plash build -f 1 --invalidate-layer)
set +e
plash build -f $cont -x 'exit 42'
test 1 -eq $? || exit 1
set -e

• Building a non-existent image leads to a non-zero exit code

(! plash build -f 999)

• Building on a base container with no macros given returns that base container

out=$(plash build -f 1)
test "$out" = 1

• Using a unknown macro leads to a non-zero exit code

(! plash build --my-bad-opiton)

Source Code


#define USAGE "usage: plash build --macro1 ar1 arg2 --macro2 arg1 ...\n"

#define _GNU_SOURCE
#define MAX_BYTES_PER_LAYER 1024 * 4

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include <plash.h>

#define PLASH_HINT_IMAGE "### plash hint: image="
#define PLASH_HINT_LAYER "### plash hint: layer"

// djb2 non-cryptografic hash function found here:
// http://www.cse.yorku.ca/~oz/hash.html
unsigned long hash(char *str) {
  unsigned long hash = 5381;
  int c;

  while ((c = *str++))
    hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

  return hash;
}

char *call_plash_create(char *image_id, char *shell_input) {
  //// run plash create to create this layer
  FILE *create_stdin, *create_stdout;
  pid_t create_pid = pl_spawn_process(
      (char *[]){"/proc/self/exe", "create", image_id, "sh", NULL},
      &create_stdin, &create_stdout, NULL);

  // send shell input to plash create
  fputs(shell_input, create_stdin);
  fclose(create_stdin);

  // exit program if plash create failed
  int create_exit;
  if ((create_exit = pl_wait(create_pid)))
    exit(create_exit);

  // get the image id plash create spitted out
  image_id = pl_nextline(create_stdout);
  if (image_id == NULL)
    pl_fatal("plash create did not print the image id");
  image_id = strdup(image_id);
  if (image_id == NULL)
    pl_fatal("strdup");

  fclose(create_stdout);

  // print some ASCII to the user
  fputs("---\n", stderr);

  return image_id;
}

char *cached_call_plash_create(char *base_image_id, char *shell_input) {
  char *image_id;
  char *cached_image_id;
  char *cache_key = NULL;
  asprintf(&cache_key, "layer:%s:%lu\n", base_image_id, hash(shell_input)) ||
      pl_fatal("asprintf");
  cached_image_id = pl_call("map", cache_key);
  if (strcmp(cached_image_id, "") != 0) {
    return cached_image_id;
  } else {
    image_id = call_plash_create(base_image_id, shell_input);
    pl_call("map", cache_key, image_id);
    return image_id;
  }
}

int build_main(int argc, char *argv[]) {
  char create_stdin_buf[MAX_BYTES_PER_LAYER];
  char *image_id;
  char *base_image_id;
  char *line;

  // mold args for plash eval process
  char *args[argc + 1];
  size_t i = 0;
  args[i++] = "/proc/self/exe";
  args[i++] = "eval";
  argv++;
  while (*argv)
    args[i++] = *(argv++);
  args[i++] = NULL;

  // run plash eval to get build shell script
  FILE *eval_stdout;
  pid_t eval_pid = pl_spawn_process(args, NULL, &eval_stdout, NULL);

  int eval_exit;
  if ((eval_exit = pl_wait(eval_pid)))
    exit(eval_exit);

  // read first line
  line = pl_nextline(eval_stdout);

  // First line must be the image id hint
  if (line == NULL ||
      strncmp(line, PLASH_HINT_IMAGE, strlen(PLASH_HINT_IMAGE)) != 0) {
    pl_fatal("First thing given must be the base image to use");
  }

  // parse image id from first output line. We need to know which is the base
  // image id in order to start building
  base_image_id = line + strlen(PLASH_HINT_IMAGE);
  base_image_id = strdup(base_image_id);
  if (base_image_id == NULL)
    pl_fatal("strdup");
  image_id = base_image_id;

  // go trough all lines returned by plash eval
  while (!feof(eval_stdout)) {

    line = pl_nextline(eval_stdout);

    // This is an empty layer, skip it.
    if (line == NULL || (strcmp(line, PLASH_HINT_LAYER) == 0))
      continue;

    FILE *create_stdin =
        fmemopen(create_stdin_buf, sizeof(create_stdin_buf), "w");
    if (create_stdin == NULL)
      pl_fatal("fmemopen");

    // some extras before evaluating the build shell script
    fputs("PS4='--> '\n", create_stdin);

    // Hack for ubuntu, where for whatever reason PATH is not exported;
    fputs("export PATH\n", create_stdin);

    fputs("set -ex\n", create_stdin);

    //// redirect all lines from the eval subcommand to create subcommand. Stop
    // if a new layer is found
    fputs(line, create_stdin);
    fputs("\n", create_stdin);
    while ((line = pl_nextline(eval_stdout)) &&
           strcmp(line, PLASH_HINT_LAYER) != 0) {
      fputs(line, create_stdin);
      fputs("\n", create_stdin);
    }

    // flush stuff in create_stdin to appear in create_stdin_buf
    fflush(create_stdin);

    // get our new image_id where we can build our next layer on top of it.
    image_id = cached_call_plash_create(image_id, create_stdin_buf);
  }

  if (strcmp(base_image_id, image_id) == 0) {
    // This happens of "plash build -f 1" invocations. Let's just validate that
    // image is correct.
    pl_call("nodepath", image_id);
  }
  puts(image_id);
  return EXIT_SUCCESS;
}