Learnlog

7:30am — What to do today

In my previous learnlog I built a tree of docker images able to be changed and rebuilt.

What I want to try do today is write a tests on the built containers so that if they don’t meet some constraints the build will fail and their dependents won’t be built.

Let’s boot up the previous days project and start reading.

8:30am — Yak Shaving

Yak shaved by cleaning up some of the images by copying the build tree of the valid DockerHub images.

Now we have the folders: src/containers/ubuntu-19.10 src/containers/buildpack-deps-19.10 src/containers/ruby-2.6.3

with dependencies looking like:

image

Also wrote a graph script that generates these ^ PNGs easily.

9:00am — Layers of Tests

There are three types of tests:

  1. Test for the rules you have written, e.g. in the bzl files.
  2. Test the source of a BUILD, e.g. unit tests.
  3. Test the generated output of the rule, e.g. testing the ruby version inside a docker container.

How to write tests for each are all mixed up together in the documentation complicating the issue further.

It looks like there is a native.test_suite function to for bzl tests. These are run with bazel test during the analysis phase. This is not what I am looking for because I need the docker containers to be built to test.

It seems that what I need is just a rule. To test the output I can just write a rule that depends on the outputted docker container.

11:30am — Many Rules

I ended up with a rule in docker.bzl: def _docker_exec_out_check_impl(ctx): name = ctx.attr.name image = ctx.attr.docker_image print(ctx.attr.docker_image.image_ref) check_output = ctx.outputs.check_output```` ctx.actions.run( executable = ctx.executable._docker_tool, inputs = [image.image_sha], arguments = [ check_output.path, "run", "-t", "--rm", image_name + ":bazel", "bash", "-c", "'" + ctx.attr.exec + "'", ], outputs = [check_output], )````docker_exec_out_check = rule( implementation = _docker_exec_out_check_impl, attrs = { "docker_image": attr.label( allow_single_file = True, mandatory = True, ), "exec": attr.string(), "contains": attr.string(), "_docker_tool": attr.label( executable = True, cfg = "host", allow_files = True, default = Label("//rules:docker"), ), }, outputs = { "check_output": "check_output" }, )

I wanted to call the rule docker_exec_out_test but the _test postfix is restricted.

This is not great for a few reasons

  1. "'" + ctx.attr.exec + "'” in the rule is auto-escaped so using bash -c is not practical.
  2. The rule is not in the path of its dependents, so if it fails it will not fail its dependent builds.

12:44pm —More Complications

Things are getting more complicated. Given this is a pretty simple thing to do, that usually is a signal I am doing it incorrectly.

Now I have these rules: ``# docker_exec_out_check takes a build docker image and
def _docker_exec_out_check_impl(ctx):
name = ctx.attr.name
image = ctx.attr.docker_image
image_name = image.image_name
check_output = ctx.outputs.check_output moar = ctx.actions.declare_file(name + "_check_output" ) ctx.actions.run(
executable = ctx.executable._docker_tool,
inputs = [image.image_sha],
arguments = [
moar.path,
“run”,
“-t”,
“–rm”,
image_name + “:bazel”,
ctx.attr.exec,
],
outputs = [moar],
)

    executable = ctx.executable._grep_tool,  
    inputs = [moar],  
    arguments = [  
      check_output.path,  
      moar.path,  
      "-e",  
      ctx.attr.contains  
    ],  
    outputs = [check_output],  
  )  

````docker_exec_out_check = rule(  
  implementation = _docker_exec_out_check_impl,  
  attrs = {  
    "docker_image": attr.label(  
      allow_single_file = True,  
      mandatory = True,  
    ),  
    "exec": attr.string(),  
    "contains": attr.string(),  
    "_docker_tool": attr.label(  
      executable = True,  
      cfg = "host",  
      allow_files = True,  
      default = Label("//rules:docker"),  
    ),````    "_grep_tool": attr.label(  
      executable = True,  
      cfg = "host",  
      allow_files = True,  
      default = Label("//rules:grep"),  
    ),  
  },  
  outputs = {  
    "check_output": "check_output"  
  },  
)``

`_grep_too` is a wrapper around grep, so once again I can output the standard out to a file. This does not even 100% working, and is very fragile.

I am thinking that relying on bash scripts to do the heavy lifting of validating and building the container is better. That is, I will leave Bazel as the glue and push the implementation down into bash more. This will also fix the dependent build problem.#### LUNCH#### 2:30 — One Rule to Rule them all

The deferred execution model of Bazel is really nice for dependency management as it lets you define the rules without actually executing them. But this can be very confusing when you have simple steps. So, putting all the logic for building and testing a docker container is much easier in a single script.

`docker.bash` is now:
``#!/bin/bash````set -e````NAME=$1  
DOCKER_FILE=$2  
DOCKER_FOLDER=$(dirname $DOCKER_FILE)  
OUTPUT_FILE=$3  
TEST_COMMAND=$4  
TEST_VALUE=$5````DOCKER_BUILD_OUT=$(docker build -q -t $NAME:bazel -f $DOCKER_FILE $DOCKER_FOLDER)````if [ -n "$TEST_COMMAND" ]; then  
    DOCKER_TEST=$(docker run -t --rm $NAME:bazel bash -c "$TEST_COMMAND")  
    echo $DOCKER_TEST | grep -e "$TEST_VALUE"  
fi````echo $NAME@$DOCKER_BUILD_OUT > $OUTPUT_FILE``

With the `docker_build` rule:
``def _docker_build(ctx):  
  froms = [f.image_digest for f in ctx.attr.froms]````  ctx.actions.run(  
    executable = ctx.executable._docker_tool,  
    inputs = ctx.files.dockerfile + froms,  
    arguments = [  
      ctx.attr.name,  
      ctx.file.dockerfile.path,  
      ctx.outputs.image_digest.path,  
      ctx.attr.test_command,  
      ctx.attr.test_value,  
    ],  
    outputs = [ctx.outputs.image_digest],  
  )````  return struct(  
    image_digest = ctx.outputs.image_digest,  
  )````docker_build = rule(  
  implementation = _docker_build,  
  attrs = {  
    "dockerfile": attr.label(  
      allow_single_file = True,  
      mandatory = True,  
    ),  
    "froms": attr.label_list(),  
    "test_command": attr.string(),  
    "test_value": attr.string(),  
    "_docker_tool": attr.label(  
      executable = True,  
      cfg = "host",  
      allow_files = True,  
      default = Label("//rules:docker"),  
    ),  
  },  
  outputs = {  
    "image_digest": "image_digest"  
  },  
)``

This is SOOOOO much simpler than before and easier to use and debug.

Now I want to try something really hard, build a ruby app with gems and put it in an image. I have created these files:
``/src/apps/test/BUILD  
/src/apps/test/server.rb  
/src/apps/test/Gemfile``

#### 4:30 — Distraction

Spent some of my time reading the `[rules_ruby](https://github.com/hvardhanx/bazel-ruby) `then got distracted by other work.

Todays code is available here [https://github.com/grahamjenson/bazel-docker-tree](https://github.com/grahamjenson/bazel-docker-tree)

Again, not sure if this is a useful format. But like taking notes in class, it makes it easier to remember where I am at and where I want to go. Will probably continue with this.