# 5. Contributor’s guide

This section is notes on contributing to Charliecloud development. Currently, it is messy and incomplete. Patches welcome!

It documents public stuff only. If you are on the core team at LANL, also consult the internal documentation and other resources.

- Workflow

  - Milestones

  - Peer review

  - Branching and merging

  - Miscellaneous issue and pull request notes

  - Continuous integration testing

  - GitHub tags

- Test suite

  - Timing the tests

  - `ch-test` complains about inconsistent versions

  - Writing a test image using the standard workflow

- Building RPMs

  - Dependencies

  - `rpmbuild` wrapper script

  - Gotchas and quirks

- Build `.ova` appliance file with Vagrant and VirtualBox

  - Remove old virtual machine

  - Build and provision

  - Snapshot for distribution

  - Test Charliecloud

  - Export appliance `.ova` file

- Style hints

  - Writing English

  - Documentation

  - Dependency policy

  - Variable conventions in shell scripts and `.bats` files

  - C code

- OCI technical notes

  - Gotchas

  - Bundle directory

  - `config.json`

  - State

  - Additional sources

Note

We’re interested in and will consider all good-faith contributions. While it does make things easier and faster if you follow the guidelines here, they are not required. We’ll either clean it up for you or walk you through any necessary changes.

## 5.1. Workflow

We try to keep procedures and the Git branching model simple. Right now, we’re pretty similar to Scott Chacon’s “GitHub Flow”: Master is stable; work on short-lived topic branches; use pull requests to ask for merging; keep issues organized with tags and milestones.

The standard workflow is:

> 1. Propose a change in an issue.

> 2. Tag the issue with its kind (bug, enhancement, question).

> 3. Get consensus on what to do and how to do it, with key information recorded in the issue.

> 4. Submit a PR that refers to the issue.

> 5. Assign the issue to a milestone.

> 6. Review/iterate.

> 7. Project lead merges.

Core team members may deliberate in public on GitHub or internally, whichever they are comfortable with, making sure to follow LANL policy and taking into account the probable desires of the recipient as well.

### 5.1.1. Milestones

We use milestones to organize what we plan to do next and what happened in a given release. There are two groups of milestones:

- `next` contains the issues that we plan to complete soon but have not yet landed on a specific release. Generally, we avoid putting PRs in here because of their ticking clocks.

- Each release has a milestone. These are dated with the target date for that release. We put an issue in when it has actually landed in that release or we are willing to delay that release until it does. We put a PR in when we think it’s reasonably likely to be merged for that release.

If an issue is assigned to a person, that means they are actively leading the work on it or will do so in the near future. Typically this happens when the issue ends up in `next`. Issues in a status of “I’ll get to this later” should not be assigned to a person.

### 5.1.2. Peer review

Issues and pull requests. The standard workflow is to introduce a change in an issue, get consensus on what to do, and then create a pull request (PR) for the implementation. The issue, not the PR, should be tagged and milestoned so a given change shows up only once in the various views.

If consensus is obtained through other means (e.g., in-person discussion), then open a PR directly. In this case, the PR should be tagged and milestoned, since there is no issue.

Address a single concern. When possible, issues and PRs should address completely one self-contained change. If there are multiple concerns, make separate issues and/or PRs. For example, PRs should not tidy unrelated code, and non-essential complications should be split into a follow-on issue.

Documentation and tests first. The best practice for significant changes is to draft documentation and/or tests first, get feedback on that, and then implement the code. Reviews of the form “you need a completely different approach” are no fun.

Tests must pass. PRs will not be merged until they pass the tests. While this most saliently includes CI, the tests should also pass on your development box as well as all relevant clusters (if appropriate for the changes).

No close keywords in PRs. While GitHub will interpret issue-closing keywords (variations on “closes”, “fixes”, and “resolves”) in PR descriptions, don’t use this feature, because often the specific issues a PR closes change over time, and we don’t want to have to edit the description to deal with that. We also want this information in only one place (the commit log). Instead, use “addresses”, and we’ll edit the keywords into the commit message(s) at merge time if needed.

PR review procedure. When your PR is ready for review — which may or may not be when you want it considered for merging — do one or both of:

- Request review from the person(s) you want to look at it. If you think it may be ready for merge, that should include the project lead. The purpose of requsting review is so the person is notified you need their help.

- If you think it may be ready to merge (even if you’re not sure), then also tag the PR `ready to merge`. The purpose of this is so the project lead can see which PRs are ready to consider for merging. If the project lead decides it’s ready, they will merge; otherwise, they’ll untag.

In both cases, the person from whom you requested review now owns the branch, and you should stop work on it unless and until you get it back.

Do not hesitate to pester your reviewer if you haven’t heard back promptly.

Special case 1: Often, the review consists of code changes, and the reviewer will want you to assess those changes. GitHub doesn’t let you request review from the PR submitter, so this must be done with a comment, either online or offline.

Special case 2: GitHub will not let you request review from external people, so this needs to be done with a comment too. Generally you should ask the original bug reporter to review, to make sure it solves their problem.

Use multi-comment reviews. Review comments should all be packaged up into a single review; click Start a review rather than Add single comment. Then the PR author gets only a single notification instead of one for every comment you make, and it’s clear when they branch is theirs again.

### 5.1.3. Branching and merging

Don’t commit directly to master. Even the project lead doesn’t do this. While it may appear that some trivial fixes are being committed to the master directly, what’s really happening is that these are prototyped on a branch and then fast-forward merged after the tests pass.

Merging to master. Only the project lead should do this.

Branch merge procedure. Generally, branches are merged in the GitHub web interface with the Squash and merge button, which is `git merge --squash` under the hood. This squashes the branch into a single commit on master. Commit message example:

    PR #268 from @j-ogas: remove ch-docker-run (closes #258)

If the branch closes multiple issues and it’s reasonable to separate those issues into independent commits, then the branch is rebased, interactively squashed, and force-pushed into a tidy history with close instructions, then merged in the web interface with Create a merge commit. Example history and commit messages:

    * 18aa2b8 merge PR #254 from @j-ogas and me: Dockerfile.openmpi: use snapshot
    |\
    | * 79fa89a upgrade to ibverbs 20.0-1 (closes #250)
    | * 385ce16 Dockerfile.debian9: use snapshot.debian.org (closes #249)
    |/
    * 322df2f ...

The reason to prefer merge via web interface is that GitHub often doesn’t notice merges done on the command line.

After merge, the branch is deleted via the web interface.

Branch history tidiness. Commit frequently at semantically relevant times, and keep in mind that this history will probably be squashed per above. It is not necessary to rebase or squash to keep branch history tidy. But, don’t go crazy. Commit messages like “try 2” and “fix CI again” are a bad sign; so are carefully proofread ones. Commit messages that are brief, technically relevant, and quick to write are what you want on feature branches.

Keep branches up to date. Merge master into your branch, rather than rebasing. This lets you resolve conflicts once rather than multiple times as rebase works through a stack of commits.

Note that PRs with merge conflicts will generally not be merged. Resolve conflicts before asking for merge.

Remove obsolete branches. Keep your repo free of old branches with `git branch -d` (or `-D`) and `git fetch --prune --all`.

### 5.1.4. Miscellaneous issue and pull request notes

Acknowledging issues. Issues and PRs submitted from outside should be acknowledged promptly, including adding or correcting tags.

Closing issues. We close issues when we’ve taken the requested action, decided not to take action, resolved the question, or actively determined an issue is obsolete. It is OK for “stale” issues to sit around indefinitely awaiting this. Unlike many projects, we do not automatically close issues just because they’re old.

Closing PR. Stale PRs, on the other hand, are to be avoided due to bit rot. We try to either merge or reject PRs in a timely manner.

Re-opening issues. Closed issues can be re-opened if new information arises, for example a `worksforme` issue with new reproduction steps.

### 5.1.5. Continuous integration testing

Quality of testing. Tagged versions currently get more testing for various reasons. We are working to improve testing for normal commits on master, but full parity is probably unlikely.

Cycles budget. The resource is there for your use, so take advantage of it, but be mindful of the various costs of this compute time.

Things you can do include testing locally first, cancelling jobs you know will fail or that won’t give you additional information, and not pushing every commit (CI tests only the most recent commit in a pushed group).

Iterating. When trying to make CI happy, force-push or squash-merge. Don’t submit a PR with half a dozen “fix CI” commits.

Purging Docker cache. `misc/docker-clean.sh` can be used to purge your Docker cache, either by removing all tags or deleting all containers and images. The former is generally preferred, as it lets you update only those base images that have actually changed (the ones that haven’t will be re-tagged).

### 5.1.6. GitHub tags

#### 5.1.6.1. What kind of issue is it?

`bug`

  Problem of some kind that needs to be fixed; i.e., something doesn’t work. This includes usability and documentation problems. Should have steps to reproduce with expected and actual behavior.

`enhancement`

  Things work, but it would be better if something was different. For example, a new feature proposal or refactoring. Should have steps to reproduce with desired and actual behavior.

`help wanted`

  The core team does not plan to address this issue, perhaps because we don’t know how, but we think it would be good to address it. We hope someone from the community will volunteer.

`key issue`

  A particularly important or notable issue.

`question`

  Support request that does not report a problem or ask for a change. Close these after the question is answered or several days with no activity.

#### 5.1.6.2. What do we plan to do about it?

For all of these, leave other tags in place, e.g. `bug`.

`deferred`

  No plans to do this, but not rejected. These issues stay open, because we do not consider the deferred state resolved. Submitting PRs on these issues is risky; you probably want to argue successfully that it should be done before starting work on it.

`duplicate`

  Same as some other previously reported issue. In addition to this tag, duplicates should refer to the other issue and be closed.

`obsolete`

  No longer relevant, moot, etc. Close.

`erroneous`

  Not a Charliecloud issue; close. Use caution when blaming a problem on user error. Often (or usually) there is a documentation or usability bug that caused the “user error”.

`ready to merge`

  PRs only. Adding this tag speculates that the PR is complete and requests it be considered for merging to master. If the project lead requests changes, they’ll remove the tag. Re-add it when you’re ready to try again. Lead removes tag after merging.

`wontfix`

  We are not going to do this, and we won’t merge PRs. Close issue after tagging, though sometimes you’ll want to leave a few days to allow for further discussion to catch mistaken tags.

`worksforme`

  We cannot reproduce the issue. Typical workflow is to tag, then wait a few days for clarification before closing.

## 5.2. Test suite

### 5.2.1. Timing the tests

The `ts` utility from `moreutils` is quite handy. The following prepends each line with the elapsed time since the previous line:

    $ ch-test -s quick | ts -i '%M:%.S'

Note: a skipped test isn’t free; I see ~0.15 seconds to do a skip.

### 5.2.2. `ch-test` complains about inconsistent versions

There are multiple ways to ask Charliecloud for its version number. These should all give the same result. If they don’t, `ch-test` will fail. Typically, something needs to be rebuilt. Recall that `configure` contains the version number as a constant, so a common way to get into this situation is to change Git branches without rebuilding it.

Charliecloud is small enough to just rebuild everything with:

    $ ./autogen.sh && ./configure && make clean && make

### 5.2.3. Writing a test image using the standard workflow

#### 5.2.3.1. Summary

The Charliecloud test suite has a workflow that can build images by two methods:

1. From a Dockerfile, using `ch-build`.

2. By running a custom script.

To create an image that will be built and unpacked and/or mounted, create a file in `examples` (if the image recipe is useful as an example) or `test` (if not) called `{Dockerfile,Build}.foo`. This will create an image tagged `foo`. Additional tests can be added to the test suite Bats files.

To create an image with its own tests, documentation, etc., create a directory in `examples`. In this directory, place `{Dockerfile,Build}[.foo]` to build the image and `test.bats` with your tests. For example, the file `examples/foo/Dockerfile` will create an image tagged `foo`, and `examples/foo/Dockerfile.bar` tagged `foo-bar`. These images also get the build and unpack/mount tests.

Additional directories can be symlinked into `examples` and will be integrated into the test suite. This allows you to create a site-specific test suite. `ch-test` finds tests at any directory depth; e.g. `examples/foo/bar/Dockerfile.baz` will create a test image tagged `bar-baz`.

Image tags in the test suite must be unique.

Order of processing; within each item, alphabetical order:

1. Dockerfiles in `test`.

2. `Build` files in `test`.

3. Dockerfiles in `examples`.

4. `Build` files in `examples`.

The purpose of doing `Build` second is so they can leverage what has already been built by a Dockerfile, which is often more straightforward.

#### 5.2.3.2. How to specify when to include and exclude a test image

Each of these image build files must specify its scope for building and running, which must be greater than or equal than the scope of all tests in any corresponding `.bats` files. Exactly one of the following strings must appear:

    ch-test-scope: quick
    ch-test-scope: standard
    ch-test-scope: full

Other stuff on the line (e.g., comment syntax) is ignored.

Optional test modification directives are:

> `ch-test-arch-exclude: ARCH`

>   If the output of `uname -m` matches `ARCH`, skip the file.

> `ch-test-builder-exclude: BUILDER`

>   If using `BUILDER`, skip the file.

> `ch-test-builder-include: BUILDER`

>   If specified, run only if using `BUILDER`. Can be repeated to include multiple builders. If specified zero times, all builders are included.

> `ch-test-need-sudo`

>   Run only if user has sudo.

#### 5.2.3.3. How to write a `Dockerfile` recipe

It’s a standard Dockerfile.

#### 5.2.3.4. How to write a `Build` recipe

This is an arbitrary script or program that builds the image. It gets three command line arguments:

> - `$1`: Absolute path to directory containing `Build`.

> - `$2`: Absolute path and name of output image, without extension. This can be either:

>   - Tarball compressed with gzip or xz; append `.tar.gz` or `.tar.xz` to `$2`. If `ch-test --pack-fmt=squash`, then this tarball will be unpacked and repacked as a SquashFS. Therefore, only use tarball output if the image build process naturally produces it and you would have to unpack it to get a directory (e.g., `docker export`).

>   - Directory; use `$2` unchanged. The contents of this directory will be packed without any enclosing directory, so if you want an enclosing directory, include one. Hidden (dot) files in `$2` will be ignored.

> - `$3`: Absolute path to temporary directory for use by the script. This can be used for whatever and need no be cleaned up; the test harness will delete it.

Other requirements:

> - The script may write only in two directories: (a) the parent directory of `$2` and (b) `$3`. Specifically, it may not write to the current working directory. Everything written to the parent directory of `$2` must have a name starting with `$(basename $2)`.

> - The first entry in `$PATH` will be the Charliecloud under test, i.e., bare `ch-*` commands will be the right ones.

> - Any programming language is permitted. To be included in the Charliecloud source code, a language already in the test suite dependencies is required.

> - The script must test for its dependencies and fail with appropriate error message and exit code if something is missing. To be included in the Charliecloud source code, all dependencies must be something we are willing to install and test.

> - Exit codes:

>   - 0: Image successfully created.

>   - 65: One or more dependencies were not met.

>   - 126 or 127: No interpreter available for script language (the shell takes care of this).

>   - else: An error occurred.

## 5.3. Building RPMs

We maintain `.spec` files and infrastructure for building RPMs in the Charliecloud source code. This is for two purposes:

> 1. We maintain our own Fedora RPMs (see packaging guidelines).

> 2. We want to be able to build an RPM of any commit.

Item 2 is tested; i.e., if you break the RPM build, the test suite will fail.

This section describes how to build the RPMs and the pain we’ve hopefully abstracted away.

### 5.3.1. Dependencies

> - charliecloud

> - Python 3.4+

> - Either:

>   - the provided example `centos7` or `centos8` image

>   - a RHEL/CentOS 7 or newer container image with (note there are different python version names for the listed packages in RHEL/CentOS 8): * autoconf * automake * gcc * make * python36 * python36-sphinx * python36-sphinx_rtd_theme * rpm-build * rpmlint * rsync

### 5.3.2. `rpmbuild` wrapper script

While building the Charliecloud RPMs is not too weird, we provide a script to streamline it. The purpose is to (a) make it easy to build versions not matching the working directory, (b) use an arbitrary `rpmbuild` directory, and (c) build in a Charliecloud container for non-RPM-based environments.

The script must be run from the root of a Charliecloud Git working directory.

Usage:

    $ packaging/fedora/build [OPTIONS] IMAGE VERSION

Options:

> - `--install` : Install the RPMs after building into the build environment.

> - `--rpmbuild=DIR` : Use RPM build directory root `DIR` (default: `~/rpmbuild`).

For example, to build a version 0.9.7 RPM from the CentOS 7 image provided with the test suite, on any system, and leave the results in `~/rpmbuild/RPMS` (note that the test suite would also build the necessary image diretory:

    $ bin/ch-build2dir -t centos7 -f ./examples/Dockerfile.centos7 ./examples $CH_TEST_IMGDIR
    $ packaging/fedora/build ${CH_TEST_IMGDIR}/centos7 0.9.7-1

To build a pre-release RPM of Git HEAD using the CentOS 7 image:

    $ bin/ch-build2dir -t centos7 -f ./examples/Dockerfile.centos7 ./examples $CH_TEST_IMGDIR
    $ packaging/fedora/build ${CH_TEST_IMGDIR}/centos7 HEAD

### 5.3.3. Gotchas and quirks

#### 5.3.3.1. RPM versions and releases

If `VERSION` is `HEAD`, then the RPM version will be the content of `VERSION.full` for that commit, including Git gobbledygook, and the RPM release will be `0`. Note that such RPMs cannot be reliably upgraded because their version numbers are unordered.

Otherwise, `VERSION` should be a released Charliecloud version followed by a hyphen and the desired RPM release, e.g. `0.9.7-3`.

Other values of `VERSION` (e.g., a branch name) may work but are not supported.

#### 5.3.3.2. Packaged source code and RPM build config come from different commits

The spec file, `build` script, `.rpmlintrc`, etc. come from the working directory, but the package source is from the specified commit. This is what enables us to make additional RPM releases for a given Charliecloud release (e.g. 0.9.7-2).

Corollaries of this policy are that RPM build configuration can be any or no commit, and it’s not possible to create an RPM of uncommitted source code.

#### 5.3.3.3. Changelog maintenance

The spec file contains a manually maintained changelog. Add a new entry for each new RPM release; do not include the Charliecloud release notes.

For released versions, `build` verifies that the most recent changelog entry matches the given `VERSION` argument. The timestamp is not automatically verified.

For other Charliecloud versions, `build` adds a generic changelog entry with the appropriate version stating that it’s a pre-release RPM.

## 5.4. Build `.ova` appliance file with Vagrant and VirtualBox

This section uses Vagrant and the VirtualBox GUI to create a `.ova` file that you can provide to end users as described in Installation. You should read the section on building the VM with Vagrant there as well.

### 5.4.1. Remove old virtual machine

Each time we create a new image to distribute, we start from scratch rather than updating the old image. Therefore, we must remove the old image:

    $ cd packaging/vagrant
    $ vagrant destroy

### 5.4.2. Build and provision

The most important differences with this build procedure have to do with login. A second user `charlie` is created and endowed with passwordless `sudo`; SSH will allow login with password; and the console will automatically log in `charlie`. You need to reboot for the latter to take effect (which is done in the next step).

    $ vagrant up
    $ vagrant provision --provision-with=ova

### 5.4.3. Snapshot for distribution

We want to distribute a small appliance file, but one that passes the tests. Running the tests greatly bloats the appliance. Therefore, we’ll take a snapshot of the powered-off VM named `exportme`, run the tests, and then roll back to the snapshot before exporting.

    $ vagrant halt
    $ VBoxManage modifyvm charliebox --defaultfrontend default
    $ vagrant snapshot save exportme

Note

If you wish to use the appliance yourself, and you prefer to use plain VirtualBox instead of Vagrant, now is a good time to clone the VM in the GUI. The clone will be protected from Vagrant’s attentions later.

### 5.4.4. Test Charliecloud

Restart and test:

    $ vagrant up --provision-with=test

Also: Show the console in the VirtualBox GUI and make sure `charlie` is logged in.

### 5.4.5. Export appliance `.ova` file

This creates a `.ova` file, which is a standard way to package a virtual machine image with metadata. Some else can then import it into their own VirtualBox, as described above. (In principle, other virtual machine emulators should work as well, but we haven’t tried.)

These steps are done in the VirtualBox GUI because I haven’t figured out a way to produce a `.ova` in Vagrant, only Vagrant “boxes”.

1. Shut down the VM (you can just power it off).

2. Restore the snapshot exportme. (Don’t use `vagrant shapshot restore` because it boots the snapshot and runs the provisioners again.)

3. File → Export appliance.

4. Select your VM, charliebox. Click Continue.

5. Configure the export:

   - Format: OVF 2.0. (Note: Changing this menu resets the filename.)

   - File: Directory and filename you want. (The install procedure above uses `charliecloud_centos7.ova`.)

   - Write manifest file: unchecked

6. Click Continue.

7. Check the descriptive information and click Export. (For example, maybe you want to put the Charliecloud version in the Version field.)

8. Distribute the resulting file, which should be about 800–900MiB.

## 5.5. Style hints

We haven’t written down a comprehensive style guide. Generally, follow the style of the surrounding code, think in rectangles rather than lines of code or text, and avoid CamelCase.

Note that Reid is very picky about style, so don’t feel singled out if he complains (or even updates this section based on your patch!). He tries to be nice about it.

### 5.5.1. Writing English

- When describing what something does (e.g., your PR or a command), use the imperative mood, i.e., write the orders you are giving rather than describe what the thing does. For example, do:

  > Inject files from the host into an image directory.
  > Add `--join-pid` option to `ch-run`.

  Do not (indicative mood):

  > Injects files from the host into an image directory.
  > Adds `--join-pid` option to `ch-run`.

- Use sentence case for titles, not title case.

- If it’s not a sentence, start with a lower-case character.

- Use spell check. Keep your personal dictionary updated so your editor is not filled with false positives.

### 5.5.2. Documentation

Heading underline characters:

> 1. Asterisk, `*`, e.g. “5. Contributor’s guide”

> 2. Equals, `=`, e.g. “5.7 OCI technical notes”

> 3. Hyphen, `-`, e.g. “5.7.1 Gotchas”

> 4. Tilde, `~`, e.g. “5.7.1.1 Namespaces” (try to avoid)

### 5.5.3. Dependency policy

Specific dependencies (prerequisites) are stated elsewhere in the documentation. This section describes our policy on which dependencies are acceptable.

#### 5.5.3.1. Generally

All dependencies must be stated and justified in the documentation.

We want Charliecloud to run on as many systems as practical, so we work hard to keep dependencies minimal. However, because Charliecloud depends on new-ish kernel features, we do depend on standards of similar vintage.

Core functionality should be available even on small systems with basic Linux distributions, so dependencies for run-time and build-time are only the bare essentials. Exceptions, to be used judiciously:

> - Features that add convenience rather than functionality may have additional dependencies that are reasonably expected on most systems where the convenience would be used.

> - Features that only work if some other software is present (example: the Docker wrapper scripts) can add dependencies of that other software.

The test suite is tricky, because we need a test framework and to set up complex test fixtures. We have not yet figured out how to do this at reasonable expense with dependencies as tight as run- and build-time, so there are systems that do support Charliecloud but cannot run the test suite.

Building the documentation needs Sphinx features that have not made their way into common distributions (i.e., RHEL), so we use recent versions of Sphinx and provide a source distribution with pre-built documentation.

Building the RPMs should work on RPM-based distributions with a kernel new enough to support Charliecloud. You might need to install additional packages (but not from third-party repositories).

#### 5.5.3.2. `curl` vs. `wget`

For URL downloading in shell code, including Dockerfiles, use `wget -nv`.

Both work fine for our purposes, and we need to use one or the other consistently. According to Debian’s popularity contest, 99.88% of reporting systems have `wget` installed, vs. about 44% for `curl`. On the other hand, `curl` is in the minimal install of CentOS 7 while `wget` is not.

For now, Reid just picked `wget` because he likes it better.

### 5.5.4. Variable conventions in shell scripts and `.bats` files

- Separate words with underscores.

- User-configured environment variables: all uppercase, `CH_TEST_` prefix. Do not use in individual `.bats` files; instead, provide an intermediate variable.

- Variables local to a given file: lower case, no prefix.

- Bats: set in `common.bash` and then used in `.bats` files: lower case, `ch_` prefix.

- Surround lower-case variables expanded in strings with curly braces, unless they’re the only thing in the string. E.g.:

      "${foo}/bar"  # yes
      "$foo"        # yes
      "$foo/bar"    # no
      "${foo}"      # no

- Quote the entire string instead of just the variable when practical:

      "${foo}/bar"  # yes
      "${foo}"/bar  # no
      "$foo"/bar    # no

- Don’t quote variable assignments or other places where not needed (e.g., case statements). E.g.:

      foo=${bar}/baz    # yes
      foo="${bar}/baz"  # no

### 5.5.5. C code

#### 5.5.5.1. `const`

The `const` keyword is used to indicate that variables are read-only. It has a variety of uses; in Charliecloud, we use it for function pointer arguments to state whether or not the object pointed to will be altered by the function. For example:

    void foo(const char *in, char *out)

is a function that will not alter the string pointed to by `in` but may alter the string pointed to by `out`. (Note that `char const` is equivalent to `const char`, but we use the latter order because that’s what appears in GCC error messages.)

We do not use `const` on local variables or function arguments passed by value. One could do this to be more clear about what is and isn’t mutable, but it adds quite a lot of noise to the source code, and in our evaluations didn’t catch any bugs. We also do not use it on double pointers (e.g., `char **out` used when a function allocates a string and sets the caller’s pointer to point to it), because so far those are all out-arguments and C has confusing rules about double pointers and `const`.

## 5.6. OCI technical notes

This section describes our analysis of the Open Container Initiative (OCI) specification and implications for our implementation in `ch-run-oci`. Anything relevant for users goes in that man page; here is for technical details. The main goals are to guide Charliecloud development and provide and opportunity for peer-review of our work.

Currently, `ch-run-oci` is only tested with Buildah. These notes describe what we are seeing from Buildah’s runtime expectations.

### 5.6.1. Gotchas

#### 5.6.1.1. Namespaces

Buildah sets up its own user and mount namespaces before invoking the runtime, though it does not change the root directory. We do not understand why. In particular, this means that you cannot see the container root filesystem it provides without joining those namespaces. To do so:

1. Export `CH_RUN_OCI_LOGFILE` with some logfile path.

2. Export `CH_RUN_OCI_DEBUG_HANG` with the step you want to examine (e.g., `create`).

3. Run `ch-build -b buildah`.

4. Make note of the PID in the logfile.

5. `$ nsenter -U -m -t $PID bash`

#### 5.6.1.2. Supervisor process and maintaining state

OCI (and thus Buildah) expects a process that exists throughout the life of the container. This conflicts with Charliecloud’s lack of a supervisor process.

FIXME

### 5.6.2. Bundle directory

- OCI documentation (very incomplete): https://github.com/opencontainers/runtime-spec/blob/master/bundle.md

The bundle directory defines the container and is used to communicate between Buildah and the runtime. The root filesystem (`mnt/rootfs`) is mounted within Buildah’s namespaces, so you’ll want to join them before examination.

`ch-run-oci` has restrictions on bundle directory path so it can be inferred from the container ID (see the man page). This lets us store state in the bundle directory instead of maintaining a second location for container state.

Example:

    # cd /tmp/buildah265508516
    # ls -lR . | head -40
    .:
    total 12
    -rw------- 1 root root 3138 Apr 25 16:39 config.json
    d--------- 2 root root   40 Apr 25 16:39 empty
    -rw-r--r-- 1 root root  200 Mar  9  2015 hosts
    d--x------ 3 root root   60 Apr 25 16:39 mnt
    -rw-r--r-- 1 root root   79 Apr 19 20:23 resolv.conf

    ./empty:
    total 0

    ./mnt:
    total 0
    drwxr-x--- 19 root root 380 Apr 25 16:39 rootfs

    ./mnt/rootfs:
    total 0
    drwxr-xr-x  2 root root 1680 Apr  8 14:30 bin
    drwxr-xr-x  2 root root   40 Apr  8 14:30 dev
    drwxr-xr-x 15 root root  720 Apr  8 14:30 etc
    drwxr-xr-x  2 root root   40 Apr  8 14:30 home
    [...]

Observations:

1. The weird permissions on `empty` (000) and `mnt` (100) persist within the namespaces, so you’ll want to be namespace root to look around.

2. `hosts` and `resolv.conf` are identical to the host’s.

3. `empty` is still an empty directory with in the namespaces. What is this for?

4. `mnt/rootfs` contains the container root filesystem. It is a tmpfs. No other new filesystems are mounted within the namespaces.

### 5.6.3. `config.json`

- OCI documentation:

  - https://github.com/opencontainers/runtime-spec/blob/master/config.md

  - https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md

This is the meat of the container configuration. Below is an example `config.json` along with commentary and how it maps to `ch-run` arguments. This was pretty-printed with `jq . config.json`, and we re-ordered the keys to match the documentation.

There are a number of additional keys that appear in the documentation but not in this example. These are all unsupported, either by ignoring them or throwing an error. The `ch-run-oci` man page documents comprehensively what OCI features are and are not supported.

    {
      "ociVersion": "1.0.0",

We validate that this is “1.0.0”.

    "root": {
      "path": "/tmp/buildah115496812/mnt/rootfs"
    },

Path to root filesystem; maps to `NEWROOT`. If key `readonly` is `false` or absent, add `--write`.

    "mounts": [
      {
        "destination": "/dev",
        "type": "tmpfs",
        "source": "/dev",
        "options": [
          "private",
          "strictatime",
          "noexec",
          "nosuid",
          "mode=755",
          "size=65536k"
        ]
      },
      {
        "destination": "/dev/mqueue",
        "type": "mqueue",
        "source": "mqueue",
        "options": [
          "private",
          "nodev",
          "noexec",
          "nosuid"
        ]
      },
      {
        "destination": "/dev/pts",
        "type": "devpts",
        "source": "pts",
        "options": [
          "private",
          "noexec",
          "nosuid",
          "newinstance",
          "ptmxmode=0666",
          "mode=0620"
        ]
      },
      {
        "destination": "/dev/shm",
        "type": "tmpfs",
        "source": "shm",
        "options": [
          "private",
          "nodev",
          "noexec",
          "nosuid",
          "mode=1777",
          "size=65536k"
        ]
      },
      {
        "destination": "/proc",
        "type": "proc",
        "source": "/proc",
        "options": [
          "private",
          "nodev",
          "noexec",
          "nosuid"
        ]
      },
      {
        "destination": "/sys",
        "type": "bind",
        "source": "/sys",
        "options": [
          "rbind",
          "private",
          "nodev",
          "noexec",
          "nosuid",
          "ro"
        ]
      },
      {
        "destination": "/etc/hosts",
        "type": "bind",
        "source": "/tmp/buildah115496812/hosts",
        "options": [
          "rbind"
        ]
      },
      {
        "destination": "/etc/resolv.conf",
        "type": "bind",
        "source": "/tmp/buildah115496812/resolv.conf",
        "options": [
          "rbind"
        ]
      }
    ],

This says what filesystems to mount in the container. It is a mix; it has tmpfses, bind-mounts of both files and directories, and other non-device-backed filesystems. The docs suggest a lot of flexibility, including stuff that won’t work in an unprivileged user namespace (e.g., filesystems backed by a block device).

The things that matter seem to be the same as Charliecloud defaults. Therefore, for now we just ignore mounts.

We do add `--no-home` in OCI mode.

    "process": {
      "terminal": true,

This says that Buildah wants a pseudoterminal allocated. Charliecloud does not currently support that, so we error in this case.

However, Buildah can be persuaded to set this `false` if you redirect its standard input from `/dev/null`, which is the current workaround. Things work fine.

    "cwd": "/",

Maps to `--cd`.

    "args": [
      "/bin/sh",
      "-c",
      "apk add --no-cache bc"
    ],

Maps to `CMD [ARG ...]`. Note that we do not run `ch-run` via the shell, so there aren’t worries about shell parsing.

    "env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "https_proxy=http://proxyout.lanl.gov:8080",
      "no_proxy=localhost,127.0.0.1,.lanl.gov",
      "HTTP_PROXY=http://proxyout.lanl.gov:8080",
      "HTTPS_PROXY=http://proxyout.lanl.gov:8080",
      "NO_PROXY=localhost,127.0.0.1,.lanl.gov",
      "http_proxy=http://proxyout.lanl.gov:8080"
    ],

Environment for the container. The spec does not say whether this is the complete environment or whether it should be added to some default environment.

We treat it as a complete environment, i.e., place the variables in a file and then `--unset-env='*' --set-env=FILE`.

    "rlimits": [
      {
        "type": "RLIMIT_NOFILE",
        "hard": 1048576,
        "soft": 1048576
      }
    ]

Process limits Buildah wants us to set with `setrlimit(2)`. Ignored.

    "capabilities": {
      ...
    },

Long list of capabilities that Buildah wants. Ignored. (Charliecloud provides security by remaining an unprivileged process.)

      "user": {
        "uid": 0,
        "gid": 0
      },
    },

Maps to `--uid=0 --gid=0`.

    "linux": {
      "namespaces": [
        {
          "type": "pid"
        },
        {
          "type": "ipc"
        },
        {
          "type": "mount"
        },
        {
          "type": "user"
        }
      ],

Namespaces that Buildah wants. Ignored; Charliecloud just does user and mount.

    "uidMappings": [
      {
        "hostID": 0,
        "containerID": 0,
        "size": 1
      },
      {
        "hostID": 1,
        "containerID": 1,
        "size": 65536
      }
    ],
    "gidMappings": [
      {
        "hostID": 0,
        "containerID": 0,
        "size": 1
      },
      {
        "hostID": 1,
        "containerID": 1,
        "size": 65536
      }
    ],

Describes the identity map between the namespace and host. Buildah wants it much larger than Charliecloud’s single entry and asks for container root to be host root, which we can’t do. Ignored.

    "maskedPaths": [
      "/proc/acpi",
      "/proc/kcore",
      ...
    ],
    "readonlyPaths": [
      "/proc/asound",
      "/proc/bus",
      ...
    ]

Spec says to “mask over the provided paths ... so they cannot be read” and “sed the provided paths as readonly”. Ignored. (Unprivileged user namespace protects us.)

      }
    }

End of example.

### 5.6.4. State

The OCI spec does not say how the JSON document describing state should be given to the caller. Buildah is happy to get it on the runtime’s standard output.

`ch-run-oci` provides an OCI compliant state document. Status `creating` will never be returned, because the create operation is essentially a no-op, and annotations are not supported, so the `annotations` key will never be given.

### 5.6.5. Additional sources

- `buildah` man page: https://github.com/containers/buildah/blob/master/docs/buildah.md

- `buildah bud` man page: https://github.com/containers/buildah/blob/master/docs/buildah-bud.md

- `runc create` man page: https://raw.githubusercontent.com/opencontainers/runc/master/man/runc-create.8.md

- https://github.com/opencontainers/runtime-spec/blob/master/runtime.md