# Introduction

`ambient` runs CI for a project in a VM.

# Acceptance criteria for `ambient`

## Smoke test

_Want:_ The `ambient` tool can show it's runtime
configuration.

_Why:_ If this doesn't work, there's no hope of anything
else working, either.

_Who:_ Lars


~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
when I run ambient config
then stdout contains ""executor": "ambient-execute-plan""
~~~

~~~{#config.yaml .file .yaml}
state: ambient_state
executor: ambient-execute-plan
image_store: image-store
uefi: true
~~~

## Reports its version

_Want:_ `ambient` can be queried for its version.

_Why:_ This is useful for diagnosing problems, and also acts as a
smoke test: if this works, we know the adapter is installed and can be
run.

~~~scenario
given an installed ambient
when I run ambient --version
then stdout matches regex ^ambient \d+\.\d+\.\d+@.*$
~~~

## Backwards compatible QEMU configuration

_Want:_ The `ambient` tool understands its own older configuration
files, but warns about them.

_Why:_ This make migration to the newer configuration format less
harsh.

_Who:_ Lars


~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from old-config.yaml
when I run ambient config --output config.json
then stderr contains "deprecated"
then stderr contains "cpus"
then stderr contains "memory"
then JSON file config.json contains qemu.cpus=8
then JSON file config.json contains qemu.memory=16 GB
~~~

~~~{#old-config.yaml .file .yaml}
cpus: 8
memory: "16 GB"
~~~

## Allows specifying a configuration file

_Want:_ The `ambient` tool allows the user to specify
which configuration file to use.

_Why:_ This is very convenient when one wants to try things
and temporarily not use the default configuration file. This happens
both for experimentation and for testing.

_Who:_ Lars


~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from  special.yaml
given file other.yaml
when I run env ambient --config other.yaml config
then stdout contains "xyzzy"
then stdout contains ""artifacts_max_size": "1 TB""
when I run env ambient --no-config --config other.yaml config
then stdout doesn't contain "xyzzy"
then stdout contains ""artifacts_max_size": "1 TB""
~~~

~~~{#special.yaml .file .yaml}
executor: xyzzy
~~~

~~~{#other.yaml .file .yaml}
artifacts_max_size: 1TB
~~~

## Notices a source directory that is not a directory

_Want:_ The `ambient` tool notices if a projects file
refers to a source directory that is not a directory.

_Why:_ It's better to catch errors early than let `tar` notice them later.

_Who:_ Lars


~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file bad-source.yaml
when I try to run env ambient projects --projects bad-source.yaml
then command fails
then stderr contains "bad-source.yaml"
then stderr contains "directory"
then stderr contains "/etc/passwd"
~~~

~~~{#bad-source.yaml .file .yaml}
projects:
  bad_source:
    image: ambient.qcow2
    source: /etc/passwd
    plan:
    - action: shell
      shell: |
        true
~~~

## Obeys TMPDIR

_Want:_ The default temporary directory is `$TMPDIR`, if set, or
`/tmp` otherwise.

_Why:_ This is expected Unix behavior.

_Who:_ Lars.

~~~scenario
given an installed ambient
given file special.yaml
when I run env --unset TMPDIR ambient config
then stdout contains ""tmpdir": "/tmp""
when I run env TMPDIR=/xyzzy ambient config
then stdout contains ""tmpdir": "/xyzzy""
~~~

## Sets environment variables

_Want:_ Ambient sets specific environment variables when running actions in
the VM. It may set others, but at least these.

_Why:_ This is expected Unix behavior.

_Who:_ Lars.

~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file env.yaml
given a directory srcdir
given a directory ambient-state
when I run ambient run --projects env.yaml
when I run ambient log env
then stdout contains "\\nHOME="
then stdout contains "\\nPATH="
then stdout contains "CARGO_TARGET_DIR="
then stdout contains "\\nCARGO_HOME="
~~~

~~~{#env.yaml .file .yaml}
projects:
  env:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        env
~~~


## Configures Git user

_Want:_ Ambient configures the Git user in the VM.

_Why:_ This is common in test suites.

_Who:_ Lars.

~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file git.yaml
given a directory srcdir
given a directory ambient-state
when I run ambient run --projects git.yaml
when I run ambient log git
then stdout contains "GITUSER: Ambient CI //"
then stdout contains "GITEMAIL: ambient@example.com //"
~~~

~~~{#git.yaml .file .yaml}
projects:
  git:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: git_config
    - action: shell
      shell: |
        echo "GITUSER: $(git config --global user.name) //"
        echo "GITEMAIL: $(git config --global user.email) //"
~~~


## Allows setting environment variables

_Want:_ Ambient allows setting environment variables for later actions.

_Why:_ This is common in test suites.

_Who:_ Lars.

~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file setenv.yaml
given a directory srcdir
when I run ambient run --projects setenv.yaml
when I run ambient log setenv
then stdout contains "foo=bar/"
~~~

~~~{#setenv.yaml .file .yaml}
projects:
  setenv:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: setenv
      set:
        FOO: bar
    - action: shell
      shell: |
        env
        echo "foo=$FOO/"
~~~


## Capture build log when running

_Want:_ The stdout/stderr output of the commands executed by
the CI run must be captured.

_Why:_ This is the primary way for the user to know what
happened during a run.

_Who:_ Lars.

Note that we can't use `.` for the current directory as the `source`
field in the project file due to how Subplot sets up a temporary data
directory for each scenario, and sets `TMPDIR` to that directory.
This would mean that `.` as `source` would create a tar archive in the
temporary directory that tries to add itself, which creates an
infinitely large tar archive.

~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file hello.yaml
given a directory srcdir
when I run ambient run --projects hello.yaml
then command is successful
~~~

~~~{#hello.yaml .file .yaml}
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        echo well hello there, friend
~~~

## Run fails if an action fails

_Want:_ If an action fails, the whole CI run fails.

_Why:_ This is fundemental to how a CI engine needs to work.

_Who:_ Lars

~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file hello-fails.yaml
given a directory srcdir
when I try to run ambient run --projects hello-fails.yaml
then command fails
~~~

~~~{#hello-fails.yaml .file .yaml}
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        false
~~~

## Store run and console log in project directory

_Want:_ The run and console logs should be stored persistently in the project
state directory, and there is a way to retrieve them later.

_Why:_ Run logs are often needed long after the run has happened. Console logs
as well.

_Who:_ Lars.

~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file hello.yaml
given a directory srcdir
when I run ambient run --projects hello.yaml
when I run ambient log hello
then stdout contains "hello there, friend"
when I run ambient log --console hello
then stdout contains "Ambient"
~~~


## Relative filenames in project files

_Want:_ When a project file has a relative filename, it's
relative to the directory containing the project file.

_Why:_ The user can then run `ambient run` from
anywhere, not just the source directory.

_Who:_ Lars.

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given a directory path/to/project/srcdir
given an Ambient VM image ambient.qcow2
when I run mv ambient.qcow2 path/to/project/
given file path/to/project/hello.yaml from hello.yaml
when I run ambient run --projects path/to/project/hello.yaml
then command is successful
~~~

## Working directory in pre- and post-plan actions

_Want:_ When a pre- or post-plan action is executed, the
current working directory should be the source directory.

_Why:_ Many actions can only usefully be executed in the
source directory.

_Who:_ Lars.

Note that we can't use `.` for the current directory as the `source`
field in the project file due to how Subplot sets up a temporary data
directory for each scenario, and sets `TMPDIR` to that directory.
This would mean that `.` as `source` would create a tar archive in the
temporary directory that tries to add itself, which creates an
infinitely large tar archive.

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given an Ambient VM image ambient.qcow2
given file cwd.yaml
given a directory path/to/project/srcdir
when I run ambient run --projects cwd.yaml
when I run ambient log pwd --format raw
then stdout matches regex cwd: /ci/src
~~~

~~~{#cwd.yaml .file .yaml}
projects:
  pwd:
    image: ambient.qcow2
    source: path/to/project/srcdir
    pre_plan:
    - action: pwd
    post_plan:
    - action: pwd
    plan:
    - action: shell
      shell: |
        pwd
~~~

## Run CI only for some projects

_Want:_ When the projects file has more than one project, user
can choose only specific ones to run CI for.

_Why:_ User may be only interested in one project right now.

_Who:_ Lars

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given an Ambient VM image ambient.qcow2
given file multi.yaml
given a directory foo
given a directory bar
when I run ambient run --dry-run --projects multi.yaml foo
then stderr contains "skip CI for foo"
then stdout doesn't contain "run CI for bar"
~~~

~~~{#multi.yaml .file .yaml}
projects:
  foo:
    image: ambient.qcow2
    source: foo
    plan:
    - action: shell
      shell: |
        echo foo project
  bar:
    image: ambient.qcow2
    source: bar
    plan:
    - action: shell
      shell: |
        echo bar project
~~~

## List names of projects

_Want:_ The user can list names of projects in a projects file.

_Why:_ This is handy for checking and scripting.

_Who:_ Lars

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given an Ambient VM image ambient.qcow2
given file multi.yaml
given a directory foo
given a directory bar
when I run ambient projects --oneline --projects multi.yaml
then stdout contains "{"projects":["bar","foo"]}"
~~~

## List names of actions

_Want:_ The user can list names of projects in a projects file.

_Why:_ This is handy for checking and scripting.

_Who:_ Lars

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given an Ambient VM image ambient.qcow2
when I run ambient actions
then stdout contains ""pre_actions": [\n"
then stdout contains ""actions": [\n"
then stdout contains ""post_actions": [\n"
~~~

## Cache persists between CI runs

_Want:_ Cache data is persisted between CI runs.

_Why:_ This allows incrementally building a project after
changes.

_Who:_ Lars

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given an Ambient VM image ambient.qcow2
given file cache.yaml
given a directory srcdir

when I run ambient run --projects cache.yaml
when I run ambient log hello
then stdout contains "counter is now 1."

when I run ambient run --projects cache.yaml
when I run ambient log hello
then stdout contains "counter is now 2."

when I run ambient run --projects cache.yaml
when I run ambient log hello
then stdout contains "counter is now 3."
~~~

~~~{#cache.yaml .file .yaml}
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        cache=/ci/cache
        counter="$cache/counter"
        if [ -e "$counter" ]; then
            n="$(cat "$counter")"
            n=$((n + 1))
            echo "$n" > "$counter"
        else
            echo 1 > "$counter"
        fi
        echo "counter is now $(cat "$counter")."
        find "$cache" -ls
~~~

<!-- FIXME: this needs a proper test, but that is tricky to without a remote server

## Publish files via rsync

_Want:_ Artifacts can be published via rsync to a server.

_Why:_ This allows publishing many kinds of things.

_Who:_ Lars

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given an Ambient VM image ambient.qcow2
given file rsync.yaml
given a directory srcdir
given file srcdir/data.dat from rsync.yaml

when I run ambient run --projects rsync.yaml --target /ci/artifacts/serverdir
when I run find -type f -ls
then file ambient_state/hello/artifacts/serverdir/hello.txt contains "hello, world"
~~~

~~~{#rsync.yaml .file .yaml}
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        mkdir -p /ci/artifacts/rsync
        echo hello, world > /ci/artifacts/rsync/hello.txt
        mkdir /ci/artifacts/serverdir
    post_plan:
    - action: rsync2
~~~
-->


## Check that `http_get` doesn't allow duplicate filenames

_Want:_ The `http_get` action should now allow duplicate filenames.

_Why:_ It's helpful to catch errors early.

_Who:_ Lars

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given an Ambient VM image ambient.qcow2
given file http_get_duplicates.yaml
given a directory srcdir

when I try to run ambient projects --projects http_get_duplicates.yaml
then command fails
then stderr contains "xyzzy"
~~~

~~~{#http_get_duplicates.yaml .file .yaml}
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    pre_plan:
    - action: http_get
      items:
      - url: http://example.com/foo
        filename: xyzzy
      - url: http://example.com/bar
        filename: xyzzy
~~~


## Allow bad plans by turning off linting

_Want:_ The user should allow turning off the linter.

_Why:_ If the linter is wrong for any reason, CI must still run.

_Who:_ Lars

Note that this scenario relies on the `http_get_duplicates.yaml` embedded file
to trigger a lint.

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given file no-linter.yaml
given an Ambient VM image ambient.qcow2
given file http_get_duplicates.yaml
given a directory srcdir

when I try to run ambient --config no-linter.yaml projects --projects http_get_duplicates.yaml
then command succeeds
~~~

~~~{#no-linter.yaml .file .yaml}
lint: false
~~~

## Verify an image is acceptable to Ambient

<!-- This is too slow for now.

_What:_ The user can use `ambient` to verify an image is
acceptable for use with Ambient.

_Why:_ This is needed so that a newly built image can be checked.

_Who:_ Lars

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given an Ambient VM image ambient.qcow2
when I run ambient image import default ambient.qcow2
then file image-store/default.qcow2 exists
when I run ambient image verify --name default --executor ambient-execute-plan
then command is successful
~~~

-->

## Manage VM images

_Want:_ `ambient` has an image store and the user can manage
images in the image store via the tool.

_Why:_ A user may need to have many images, perhaps with different
operating systems or versions of thereof, or with different
dependencies installed.

~~~scenario
given an installed ambient
given file .config/ambient/config.yaml from config.yaml
given file dummy.qcow2

when I run ambient image list
then stdout is exactly ""

when I run ambient image import default dummy.qcow2 -d mcdummy -u https://example.com/image.qcow2
then file image-store/default.qcow2 exists
when I run ambient image list
then stdout is exactly "default\n"

when I run ambient image show default
then stdout contains ""name": "default""
then stdout contains ""filename": "default.qcow2""
then stdout contains ""sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b""
then stdout contains ""description": "mcdummy""
then stdout contains ""url": "https://example.com/image.qcow2""
then stdout contains ""import_date":"
then stdout contains ""uefi": true"

when I run ambient image remove default
when I run ambient image list
then stdout is exactly ""
~~~

~~~{#dummy.qcow2 .file}
~~~


## Handles symlinks in source tree

_Want:_ When constructing the source tree for the VM, `ambient` does
not follow symbolic links.

_Why:_ We don't want a project to create a link a file outside the
source, but we do want to handle broken links. These are both best
handled by not following symbolic links, when constructing the tar
archive of the source tree.

~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file symlink.yaml
given a directory srcdir
when I run touch srcdir/README
when I run ln -s README srcdir/ok
when I run ln -s no-such-file srcdir/broken
when I run ln -s /etc/passwd srcdir/passwd
when I run ambient run --projects symlink.yaml
when I run ambient log hello
then stdout contains "./ok -> README"
then stdout contains "./broken -> no-such-file"
then stdout contains "./passwd -> /etc/passwd"
~~~

~~~{#symlink.yaml .file .yaml}
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        find . -ls
~~~

## Carries over designated environment variables to actual plan

_Want:_ A pre-plan action can specify environment variables to set when
executing the actual plan.

_Why:_ This avoids having the CI user to set them manually. For example, a
pre-plan action to download a Rust toolchain could set `CARGO_HOME` for actual
plan actions.

_How:_ We use the `dummy` action in the pre-plan, which sets `DUMMY=1` for
the actual plan, so we check that it's set.

~~~scenario
given an installed ambient
given an Ambient VM image ambient.qcow2
given file .config/ambient/config.yaml from config.yaml
given file carry-env.yaml
given a directory srcdir
when I run ambient run --projects carry-env.yaml
when I run ambient log hello
then stdout contains "DUMMY is set, hooray"
~~~

~~~{#carry-env.yaml .file .yaml}
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    pre_plan:
    - action: dummy
    plan:
    - action: shell
      shell: |
        if env | grep '^DUMMY=1$'; then
          echo DUMMY is set, hooray
        else
          echo DUMMY is not set, boohoo
        fi
~~~


# Acceptance criteria for `ambient-execute-plan`

`ambient-execute-plan` is the program that executes actions inside the
virtual machine, specifically actions in the "runnable plan". The
scenarios in this chapter verify that it work, without having to run a
full virtual machine to do so.

## Fails if an action fails

_Want:_ If an action fails, `ambient-execute-plan` should fail and not
execute any later actions.

_Why:_ Need this so that if something fails, a CI run will fail. This scenario
only verifies that the `ambient-execute-plan` program fails.

We use a `shell` and an `mkdir` action for this. One executes an other program (the
shell) and the other is implemented by `ambient-execute-plan` itself. If both work as
they should, we assume the rest of the actions are OK too.

~~~scenario
given an installed ambient

given file plan.yaml from shell-fails.yaml
when I try to run ambient-execute-plan
then command fails
then stdout matches regex program_failed.*shell-action A
then stdout doesn't match regex program_failed.*shell-action B

given file plan.yaml from mkdir-fails.yaml
when I try to run ambient-execute-plan
then command fails
then stdout doesn't contain "\nshell-action B"
~~~

~~~{#shell-fails.yaml .file .yaml}
steps:
- action: shell
  shell: |
    echo shell-action A
    exit 1
- action: shell
  shell: |
    exit 1
    echo shell-action B
source_dir: .
deps_dir: .
artifacts_dir: .
~~~

~~~{#mkdir-fails.yaml .file .yaml}
steps:
- action: mkdir
  pathname: /not-allowed
- action: shell
  shell: |
    echo shell-action B
    exit 1
source_dir: .
deps_dir: .
artifacts_dir: .
~~~


## Executes `shell` action

_Want:_ `action-execute-plan` can execute a `shell` action with a
shell script snippet.

~~~scenario
given an installed ambient
given file plan.yaml from shell-action.yaml
when I run ambient-execute-plan
then stdout contains ""stdout":"hello, world"
~~~

~~~{#shell-action.yaml .file .yaml}
steps:
- action: shell
  shell: |
    echo hello, world
source_dir: .
deps_dir: .
artifacts_dir: .
~~~

## Executes `pwd` action

_Want:_ `action-execute-plan` can execute a `pwd` action.

~~~scenario
given an installed ambient
given file plan.yaml from pwd-action.yaml
when I run ambient-execute-plan
then stdout contains "cwd: /tmp"
~~~

~~~{#pwd-action.yaml .file .yaml}
steps:
- action: pwd
source_dir: .
deps_dir: .
artifacts_dir: .
~~~

## Executes `mkdir` action

_Want:_ `action-execute-plan` can execute a `mkdir` action.

_Why:_ This is needed to set up the VM environment for running CI.

~~~scenario
given an installed ambient
given file plan.yaml from mkdir-action.yaml
when I run ambient-execute-plan
then stdout contains "./xyzzy"
~~~

~~~{#mkdir-action.yaml .file .yaml}
steps:
- action: mkdir
  pathname: xyzzy
- action: shell
  shell: |
    find -type d
source_dir: .
deps_dir: .
artifacts_dir: .
~~~

## Executes `tar_create` and `tar_extract` actions

_Want:_ `action-execute-plan` can execute a `tar_create` action and
its complement `tar_extract`.

_Why:_ This is needed to set up the VM environment for running CI.

~~~scenario
given an installed ambient
given file plan.yaml from tar-create-action.yaml
given a directory data
when I write "" to file data/foo.txt
when I run ambient-execute-plan
then file data2/foo.txt exists
when I run tar tvf tar.tar
then stdout contains "./foo.txt"
~~~

~~~{#tar-create-action.yaml .file .yaml}
steps:
- action: tar_create
  archive: tar.tar
  directory: data
- action: tar_extract
  archive: tar.tar
  directory: data2
source_dir: .
deps_dir: .
artifacts_dir: .
~~~

## Executes a custom action

_Want:_ `action-execute-plan` can execute a custom action provided in
the source tree under test.

_Why:_ This allows actions not built into Ambient.

~~~scenario
given an installed ambient
given file plan.yaml from custom-action.yaml
given file .ambient/hello from hello-action.sh
when I run chmod +x .ambient/hello
when I run ambient-execute-plan
then stdout contains "hey there"
~~~

~~~{#custom-action.yaml .file .yaml}
steps:
- action: custom
  name: hello
  args:
    greeting: hey
    whom: there
source_dir: .
deps_dir: .
artifacts_dir: .
~~~

~~~{#hello-action.sh .file .sh}
#!/bin/sh
args="$(cat)"
greeting="$(echo -n "$args" | jq -r .greeting)"
whom="$(echo -n "$args" | jq -r .whom)"
echo "$greeting $whom"
~~~

## Fails if a custom action fails

_Want:_ `action-execute-plan` fails if a script for the custom
action fails.


~~~scenario
given an installed ambient
given file plan.yaml from custom-action-fails.yaml
given file .ambient/hello from hello-action-fails.sh
when I run chmod +x .ambient/hello
when I try to run ambient-execute-plan
then command fails
~~~

~~~{#custom-action-fails.yaml .file .yaml}
steps:
- action: custom
  name: hello
  args:
    greeting: hey
    whom: there
source_dir: .
deps_dir: .
artifacts_dir: .
~~~

~~~{#hello-action-fails.sh .file .sh}
#!/bin/sh
exit 1
~~
