mirror of
https://github.com/ohwgiles/laminar.git
synced 2026-10-16 16:22:47 +00:00
Compare commits
9 Commits
feature/co
...
0.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7e6094d01 | ||
|
|
6f366e21cc | ||
|
|
2bf04d8157 | ||
|
|
40b708c594 | ||
|
|
b8ab43b508 | ||
|
|
3fde38c6b8 | ||
|
|
304ef797b8 | ||
|
|
e6f338ceb2 | ||
|
|
0384fc9a0a |
@@ -86,14 +86,15 @@ generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js
|
|||||||
# (see resources.cpp where these are fetched)
|
# (see resources.cpp where these are fetched)
|
||||||
|
|
||||||
set(LAMINARD_CORE_SOURCES
|
set(LAMINARD_CORE_SOURCES
|
||||||
src/database.cpp
|
|
||||||
src/server.cpp
|
|
||||||
src/laminar.cpp
|
|
||||||
src/conf.cpp
|
src/conf.cpp
|
||||||
|
src/database.cpp
|
||||||
|
src/laminar.cpp
|
||||||
|
src/leader.cpp
|
||||||
src/http.cpp
|
src/http.cpp
|
||||||
src/resources.cpp
|
src/resources.cpp
|
||||||
src/rpc.cpp
|
src/rpc.cpp
|
||||||
src/run.cpp
|
src/run.cpp
|
||||||
|
src/server.cpp
|
||||||
laminar.capnp.c++
|
laminar.capnp.c++
|
||||||
index_html_size.h
|
index_html_size.h
|
||||||
)
|
)
|
||||||
@@ -111,14 +112,15 @@ set(BUILD_TESTS FALSE CACHE BOOL "Build tests")
|
|||||||
if(BUILD_TESTS)
|
if(BUILD_TESTS)
|
||||||
find_package(GTest REQUIRED)
|
find_package(GTest REQUIRED)
|
||||||
include_directories(${GTEST_INCLUDE_DIRS} src)
|
include_directories(${GTEST_INCLUDE_DIRS} src)
|
||||||
add_executable(laminar-tests ${LAMINARD_CORE_SOURCES} ${COMPRESSED_BINS} test/main.cpp test/laminar-functional.cpp test/unit-conf.cpp test/unit-database.cpp test/unit-run.cpp)
|
add_executable(laminar-tests ${LAMINARD_CORE_SOURCES} ${COMPRESSED_BINS} test/main.cpp test/laminar-functional.cpp test/unit-conf.cpp test/unit-database.cpp)
|
||||||
target_link_libraries(laminar-tests ${GTEST_LIBRARY} capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
target_link_libraries(laminar-tests ${GTEST_LIBRARY} capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(SYSTEMD_UNITDIR lib/systemd/system CACHE PATH "Path to systemd unit files")
|
set(SYSTEMD_UNITDIR lib/systemd/system CACHE PATH "Path to systemd unit files")
|
||||||
set(BASH_COMPLETIONS_DIR usr/share/bash-completion/completions CACHE PATH "Path to bash completions directory")
|
set(BASH_COMPLETIONS_DIR usr/share/bash-completion/completions CACHE PATH "Path to bash completions directory")
|
||||||
set(ZSH_COMPLETIONS_DIR usr/share/zsh/site-functions CACHE PATH "Path to zsh completions directory")
|
set(ZSH_COMPLETIONS_DIR usr/share/zsh/site-functions CACHE PATH "Path to zsh completions directory")
|
||||||
install(TARGETS laminard laminarc RUNTIME DESTINATION usr/bin)
|
install(TARGETS laminard RUNTIME DESTINATION usr/sbin)
|
||||||
|
install(TARGETS laminarc RUNTIME DESTINATION usr/bin)
|
||||||
install(FILES etc/laminar.service DESTINATION ${SYSTEMD_UNITDIR})
|
install(FILES etc/laminar.service DESTINATION ${SYSTEMD_UNITDIR})
|
||||||
install(FILES etc/laminar.conf DESTINATION etc)
|
install(FILES etc/laminar.conf DESTINATION etc)
|
||||||
install(FILES etc/laminarc-completion.bash DESTINATION ${BASH_COMPLETIONS_DIR} RENAME laminarc)
|
install(FILES etc/laminarc-completion.bash DESTINATION ${BASH_COMPLETIONS_DIR} RENAME laminarc)
|
||||||
|
|||||||
@@ -34,3 +34,7 @@ sudo make install
|
|||||||
## Packaging for distributions
|
## Packaging for distributions
|
||||||
|
|
||||||
The `pkg` directory contains shell scripts which use docker to build native packages (deb,rpm) for common Linux distributions. Note that these are very simple packages which may not completely conform to the distribution's packaging guidelines, however they may serve as a starting point for creating an official package, or may be useful if the official package lags.
|
The `pkg` directory contains shell scripts which use docker to build native packages (deb,rpm) for common Linux distributions. Note that these are very simple packages which may not completely conform to the distribution's packaging guidelines, however they may serve as a starting point for creating an official package, or may be useful if the official package lags.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Issues and pull requests via GitHub are most welcome. All pull requests must adhere to the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||||
136
UserManual.md
136
UserManual.md
@@ -96,10 +96,6 @@ Laminar uses Sever Sent Events to provide a responsive, auto-updating display wi
|
|||||||
|
|
||||||
If you use a reverse proxy to host Laminar at a subfolder instead of a subdomain root, the `<base href>` needs to be updated to ensure all links point to their proper targets. This can be done by setting `LAMINAR_BASE_URL` in `/etc/laminar.conf`.
|
If you use a reverse proxy to host Laminar at a subfolder instead of a subdomain root, the `<base href>` needs to be updated to ensure all links point to their proper targets. This can be done by setting `LAMINAR_BASE_URL` in `/etc/laminar.conf`.
|
||||||
|
|
||||||
## Set the page title
|
|
||||||
|
|
||||||
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. For further WebUI customization, consider using a [custom style sheet](#Customizing-the-WebUI).
|
|
||||||
|
|
||||||
## More configuration options
|
## More configuration options
|
||||||
|
|
||||||
See the [reference section](#Service-configuration-file)
|
See the [reference section](#Service-configuration-file)
|
||||||
@@ -327,8 +323,6 @@ Then in `example.run`
|
|||||||
echo $foo # prints "bar"
|
echo $foo # prints "bar"
|
||||||
```
|
```
|
||||||
|
|
||||||
This works because laminarc reads `$JOB` and `$NUM` and passes them to the laminar daemon as part of the `set` request. (It is thus possible to set environment variables on other jobs by overriding these variables, but this is not very useful).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Archiving artefacts
|
# Archiving artefacts
|
||||||
@@ -439,7 +433,7 @@ echo Initializing workspace
|
|||||||
git clone git@example.com:company/project.git .
|
git clone git@example.com:company/project.git .
|
||||||
```
|
```
|
||||||
|
|
||||||
**CAUTION**: By default, laminar permits multiple simultaneous runs of the same job. If a job can **modify** the workspace, this might result in inconsistent builds when simultaneous runs access the same content. This is unlikely to be an issue for nightly builds, but for SCM-triggered builds it will be. To solve this, use [nodes](#Nodes-and-Tags) to restrict simultaneous execution of jobs, or consider [flock](https://linux.die.net/man/1/flock).
|
**CAUTION**: By default, laminar permits multiple simultaneous runs of the same job. If a job can **modify** the workspace, this might result in inconsistent builds when simultaneous runs access the same content. This is unlikely to be an issue for nightly builds, but for SCM-triggered builds it will be. To solve this, use [contexts](#Contexts) to restrict simultaneous execution of jobs, or consider [flock](https://linux.die.net/man/1/flock).
|
||||||
|
|
||||||
The following example uses [flock](https://linux.die.net/man/1/flock) to efficiently share a git repository workspace between multiple simultaneous builds:
|
The following example uses [flock](https://linux.die.net/man/1/flock) to efficiently share a git repository workspace between multiple simultaneous builds:
|
||||||
|
|
||||||
@@ -490,62 +484,65 @@ TIMEOUT=120
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Nodes and Tags
|
# Contexts
|
||||||
|
|
||||||
In Laminar, a *node* is an abstract concept allowing more fine-grained control over job execution scheduling. Each node can be defined to support an integer number of *executors*, which defines how many runs can be executed simultaneously.
|
In Laminar, each run of a job is associated with a context. The context defines an integer number of *executors*, which is the amount of runs which the context will accept simultaneously. A context may also provide additional environment variables.
|
||||||
|
|
||||||
A typical example would be to allow only a few concurrent CPU-intensive jobs (such as compilation), while simultaneously allowing many more less-intensive jobs (such as monitoring or remote jobs). To create a node named `build` with 3 executors, create the file `/var/lib/laminar/cfg/nodes/build.conf` with the following content:
|
Uses for this feature include limiting the amount of concurrent CPU-intensive jobs (such as compilation); and controlling access to jobs [executed remotely](#Remote-jobs).
|
||||||
|
|
||||||
```
|
If no contexts are defined, Laminar will behave as if there is a single context named "default", with `6` executors. This is a reasonable default that allows simple setups to work without any consideration of contexts.
|
||||||
EXECUTORS=3
|
|
||||||
```
|
|
||||||
|
|
||||||
To associate jobs with nodes, laminar uses *tags*. Tags may be applied to nodes and jobs. If a node has tags, only jobs with a matching tag will be executed on it. If a node has no tags, it will accept any job. To tag a node, add them to `/var/lib/laminar/cfg/nodes/NODENAME.conf`:
|
## Defining a context
|
||||||
|
|
||||||
```
|
To create a context named "my-env" which only allows a single run at once, create `/var/lib/laminar/cfg/contexts/my-env.conf` with the content:
|
||||||
EXECUTORS=3
|
|
||||||
TAGS=tag1,tag2
|
|
||||||
```
|
|
||||||
|
|
||||||
To add a tag to a job, add the following to `/var/lib/laminar/cfg/jobs/JOBNAME.conf`:
|
|
||||||
|
|
||||||
```
|
|
||||||
TAGS=tag2
|
|
||||||
```
|
|
||||||
|
|
||||||
If Laminar cannot find any node configuration, it will assume a single node with 6 executors and no tags.
|
|
||||||
|
|
||||||
## Grouping jobs with tags
|
|
||||||
|
|
||||||
Tags are also used to group jobs in the web UI. Each tag will presented as a tab in the "Jobs" page.
|
|
||||||
|
|
||||||
## Node scripts
|
|
||||||
|
|
||||||
If `/var/lib/laminar/cfg/nodes/NODENAME.before` exists, it will be executed before the run script of a job scheduled to that node. Similarly, if `/var/lib/laminar/cfg/nodes/NODENAME.after` exists, it will be executed after the run script of a job scheduled to that node.
|
|
||||||
|
|
||||||
## Node environment
|
|
||||||
|
|
||||||
If `/var/lib/laminar/cfg/nodes/NODENAME.env` exists and can be parsed as a list of `KEY=VALUE` pairs, these variables will be exposed as part of the run's environment.
|
|
||||||
|
|
||||||
# Remote jobs
|
|
||||||
|
|
||||||
Laminar provides no specific support, `bash`, `ssh` and possibly NFS are all you need. For example, consider two identical target devices on which test jobs can be run in parallel. You might create a [node](#Nodes-and-Tags) for each, `/var/lib/laminar/cfg/nodes/target{1,2}.conf` with a common tag:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
EXECUTORS=1
|
EXECUTORS=1
|
||||||
TAGS=remote-target
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In each node's `.env` file, set the individual device's IP address:
|
## Associating a job with a context
|
||||||
|
|
||||||
|
When trying to start a job, laminar will wait until the job can be matched to a context which has at least one free executor. You can define which contexts the job will associate with by setting, for example,
|
||||||
|
|
||||||
|
```
|
||||||
|
CONTEXTS=my-env-*,special_context
|
||||||
|
```
|
||||||
|
|
||||||
|
in `/var/lib/laminar/cfg/jobs/JOB.conf`. For each of the patterns in the comma-separated list `CONTEXTS`, Laminar will iterate over the known contexts and associate the run with the first context with free executors. Patterns are [glob expressions](http://man7.org/linux/man-pages/man7/glob.7.html).
|
||||||
|
|
||||||
|
If `CONTEXTS` is empty or absent (or if `JOB.conf` doesn't exist), laminar will behave as if `CONTEXTS=default` were defined.
|
||||||
|
|
||||||
|
## Adding environment to a context
|
||||||
|
|
||||||
|
Append desired environment variables to `/var/lib/laminar/cfg/contexts/CONTEXT_NAME.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
DUT_IP=192.168.3.2
|
||||||
|
FOO=bar
|
||||||
|
```
|
||||||
|
|
||||||
|
This environment will then be available the run script of jobs associated with this context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Remote jobs
|
||||||
|
|
||||||
|
Laminar provides no specific support, `bash`, `ssh` and possibly NFS are all you need. For example, consider two identical target devices on which test jobs can be run in parallel. You might create a [context](#Contexts) for each, `/var/lib/laminar/cfg/contexts/target{1,2}.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
EXECUTORS=1
|
||||||
|
```
|
||||||
|
|
||||||
|
In each context's `.env` file, set the individual device's IP address:
|
||||||
|
|
||||||
```
|
```
|
||||||
TARGET_IP=192.168.0.123
|
TARGET_IP=192.168.0.123
|
||||||
```
|
```
|
||||||
|
|
||||||
And tag the job accordingly in `/var/lib/laminar/cfg/jobs/myproject-test.conf`:
|
And mark the job accordingly in `/var/lib/laminar/cfg/jobs/myproject-test.conf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
TAGS=remote-target
|
CONTEXTS=target*
|
||||||
```
|
```
|
||||||
|
|
||||||
This means the job script `/var/lib/laminar/cfg/jobs/myproject-test.run` can be generic:
|
This means the job script `/var/lib/laminar/cfg/jobs/myproject-test.run` can be generic:
|
||||||
@@ -579,8 +576,48 @@ EOF
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Colours in log output
|
||||||
|
|
||||||
|
Laminar's frontend supports ANSI colours using the [ansi-up library](https://github.com/drudru/ansi_up). Unfortunately, there is no standard way of convincing applications to output colours when not connected to a tty. It is recommended to set [CLICOLOR_FORCE=1](https://bixense.com/clicolors/) in Laminar's [global environment file](#Environment-variables), plus any of the following environment variables that may be relevant (please submit more):
|
||||||
|
|
||||||
|
* git: `GIT_CONFIG_PARAMETERS='color.status=always' 'color.ui=always'`
|
||||||
|
* google test: `GTEST_COLOR=1`
|
||||||
|
* grep: `GREP_OPTIONS=--color=always`
|
||||||
|
|
||||||
|
More intrusive options for other common tools which do not support enabling colours via environment variable:
|
||||||
|
|
||||||
|
* gcc and clang: Add `-fdiagnostics-color=always` to compile flags
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Customizing the WebUI
|
# Customizing the WebUI
|
||||||
|
|
||||||
|
## Organising jobs into groups
|
||||||
|
|
||||||
|
*Groups* may be used to organise the "Jobs" page into tabs. Edit `/var/lib/laminar/cfg/groups.conf` and define the matched jobs as a [javascript regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Builds=compile-\w+
|
||||||
|
My Fav Jobs=^(target-foo-(build|deploy)|run-benchmarks)$
|
||||||
|
All=.*
|
||||||
|
```
|
||||||
|
|
||||||
|
Changes to this file are detected immediately and will be visible on next page refresh.
|
||||||
|
|
||||||
|
## Adding a description to a job
|
||||||
|
|
||||||
|
Edit `/var/lib/laminar/cfg/jobs/$JOBNAME.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
DESCRIPTION=Anything here will appear on the job page in the frontend <em>unescaped</em>.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting the page title
|
||||||
|
|
||||||
|
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. Laminar must be restarted for this change to take effect.
|
||||||
|
|
||||||
|
## Custom stylesheet
|
||||||
|
|
||||||
If it exists, the file `/var/lib/laminar/custom/style.css` will be served by laminar and may be used to change the appearance of Laminar's WebUI.
|
If it exists, the file `/var/lib/laminar/custom/style.css` will be served by laminar and may be used to change the appearance of Laminar's WebUI.
|
||||||
|
|
||||||
This directory is also a good place to add any extra assets needed for this customization, but note that in this case you will need to serve this directory directly from your [HTTP reverse proxy](#Service-configuration) (highly recommended).
|
This directory is also a good place to add any extra assets needed for this customization, but note that in this case you will need to serve this directory directly from your [HTTP reverse proxy](#Service-configuration) (highly recommended).
|
||||||
@@ -614,15 +651,13 @@ Laminar will serve a job's current status as a pretty badge at the url `/badge/J
|
|||||||
|
|
||||||
## Script execution order
|
## Script execution order
|
||||||
|
|
||||||
When `$JOB` is triggered on `$NODE`, the following scripts (relative to `$LAMINAR_HOME/cfg`) may be executed:
|
When `$JOB` is triggered, the following scripts (relative to `$LAMINAR_HOME/cfg`) may be executed:
|
||||||
|
|
||||||
- `jobs/$JOB.init` if the [workspace](#Data-sharing-and-Workspaces) did not exist
|
- `jobs/$JOB.init` if the [workspace](#Data-sharing-and-Workspaces) did not exist
|
||||||
- `before`
|
- `before`
|
||||||
- `nodes/$NODE.before`
|
|
||||||
- `jobs/$JOB.before`
|
- `jobs/$JOB.before`
|
||||||
- `jobs/$JOB.run`
|
- `jobs/$JOB.run`
|
||||||
- `jobs/$JOB.after`
|
- `jobs/$JOB.after`
|
||||||
- `nodes/$NODE.after`
|
|
||||||
- `after`
|
- `after`
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
@@ -635,13 +670,14 @@ The following variables are available in run scripts:
|
|||||||
- `LAST_RESULT` string previous run status
|
- `LAST_RESULT` string previous run status
|
||||||
- `WORKSPACE` path to this job's workspace
|
- `WORKSPACE` path to this job's workspace
|
||||||
- `ARCHIVE` path to this run's archive
|
- `ARCHIVE` path to this run's archive
|
||||||
|
- `CONTEXT` the context of this run
|
||||||
|
|
||||||
In addition, `$LAMINAR_HOME/cfg/scripts` is prepended to `$PATH`. See [helper scripts](#Helper-scripts).
|
In addition, `$LAMINAR_HOME/cfg/scripts` is prepended to `$PATH`. See [helper scripts](#Helper-scripts).
|
||||||
|
|
||||||
Laminar will also export variables in the form `KEY=VALUE` found in these files:
|
Laminar will also export variables in the form `KEY=VALUE` found in these files:
|
||||||
|
|
||||||
- `env`
|
- `env`
|
||||||
- `nodes/$NODE.env`
|
- `contexts/$CONTEXT.env`
|
||||||
- `jobs/$JOB.env`
|
- `jobs/$JOB.env`
|
||||||
|
|
||||||
Finally, variables supplied on the command-line call to `laminarc queue`, `laminarc start` or `laminarc run` will be available. See [parameterized runs](#Parameterized-runs)
|
Finally, variables supplied on the command-line call to `laminarc queue`, `laminarc start` or `laminarc run` will be available. See [parameterized runs](#Parameterized-runs)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
User=laminar
|
User=laminar
|
||||||
EnvironmentFile=-/etc/laminar.conf
|
EnvironmentFile=-/etc/laminar.conf
|
||||||
ExecStart=/usr/bin/laminard
|
ExecStart=/usr/sbin/laminard
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ make
|
|||||||
|
|
||||||
%files
|
%files
|
||||||
%{_bindir}/laminarc
|
%{_bindir}/laminarc
|
||||||
%{_bindir}/laminard
|
%{_sbindir}/laminard
|
||||||
%{_unitdir}/laminar.service
|
%{_unitdir}/laminar.service
|
||||||
%config(noreplace) %{_sysconfdir}/laminar.conf
|
%config(noreplace) %{_sysconfdir}/laminar.conf
|
||||||
%{_datarootdir}/bash-completion/completions/laminarc
|
%{_datarootdir}/bash-completion/completions/laminarc
|
||||||
@@ -72,7 +72,7 @@ make
|
|||||||
%post
|
%post
|
||||||
echo Creating laminar user with home in %{_sharedstatedir}/laminar
|
echo Creating laminar user with home in %{_sharedstatedir}/laminar
|
||||||
useradd -r -d %{_sharedstatedir}/laminar -s %{_sbindir}/nologin laminar
|
useradd -r -d %{_sharedstatedir}/laminar -s %{_sbindir}/nologin laminar
|
||||||
mkdir -p %{_sharedstatedir}/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p %{_sharedstatedir}/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: %{_sharedstatedir}/laminar
|
chown -R laminar: %{_sharedstatedir}/laminar
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ make
|
|||||||
|
|
||||||
%files
|
%files
|
||||||
%{_bindir}/laminarc
|
%{_bindir}/laminarc
|
||||||
%{_bindir}/laminard
|
%{_sbindir}/laminard
|
||||||
%{_unitdir}/laminar.service
|
%{_unitdir}/laminar.service
|
||||||
%config(noreplace) %{_sysconfdir}/laminar.conf
|
%config(noreplace) %{_sysconfdir}/laminar.conf
|
||||||
%{_datarootdir}/bash-completion/completions/laminarc
|
%{_datarootdir}/bash-completion/completions/laminarc
|
||||||
@@ -69,7 +69,7 @@ make
|
|||||||
%post
|
%post
|
||||||
echo Creating laminar user with home in %{_sharedstatedir}/laminar
|
echo Creating laminar user with home in %{_sharedstatedir}/laminar
|
||||||
useradd -r -d %{_sharedstatedir}/laminar -s %{_sbindir}/nologin laminar
|
useradd -r -d %{_sharedstatedir}/laminar -s %{_sbindir}/nologin laminar
|
||||||
mkdir -p %{_sharedstatedir}/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p %{_sharedstatedir}/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: %{_sharedstatedir}/laminar
|
chown -R laminar: %{_sharedstatedir}/laminar
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ cat <<EOF > laminar/DEBIAN/postinst
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo Creating laminar user with home in /var/lib/laminar
|
echo Creating laminar user with home in /var/lib/laminar
|
||||||
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
||||||
mkdir -p /var/lib/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p /var/lib/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: /var/lib/laminar
|
chown -R laminar: /var/lib/laminar
|
||||||
EOF
|
EOF
|
||||||
chmod +x laminar/DEBIAN/postinst
|
chmod +x laminar/DEBIAN/postinst
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ cat <<EOF > laminar/DEBIAN/postinst
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo Creating laminar user with home in /var/lib/laminar
|
echo Creating laminar user with home in /var/lib/laminar
|
||||||
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
||||||
mkdir -p /var/lib/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p /var/lib/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: /var/lib/laminar
|
chown -R laminar: /var/lib/laminar
|
||||||
EOF
|
EOF
|
||||||
chmod +x laminar/DEBIAN/postinst
|
chmod +x laminar/DEBIAN/postinst
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ cat <<EOF > laminar/DEBIAN/postinst
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo Creating laminar user with home in /var/lib/laminar
|
echo Creating laminar user with home in /var/lib/laminar
|
||||||
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
||||||
mkdir -p /var/lib/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p /var/lib/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: /var/lib/laminar
|
chown -R laminar: /var/lib/laminar
|
||||||
EOF
|
EOF
|
||||||
chmod +x laminar/DEBIAN/postinst
|
chmod +x laminar/DEBIAN/postinst
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ cat <<EOF > laminar/DEBIAN/postinst
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo Creating laminar user with home in /var/lib/laminar
|
echo Creating laminar user with home in /var/lib/laminar
|
||||||
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
||||||
mkdir -p /var/lib/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p /var/lib/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: /var/lib/laminar
|
chown -R laminar: /var/lib/laminar
|
||||||
EOF
|
EOF
|
||||||
chmod +x laminar/DEBIAN/postinst
|
chmod +x laminar/DEBIAN/postinst
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#define EXIT_BAD_ARGUMENT 1
|
#define EXIT_BAD_ARGUMENT 1
|
||||||
#define EXIT_OPERATION_FAILED 2
|
#define EXIT_OPERATION_FAILED 2
|
||||||
@@ -169,21 +170,10 @@ int main(int argc, char** argv) {
|
|||||||
fprintf(stderr, "Usage %s set param=value\n", argv[0]);
|
fprintf(stderr, "Usage %s set param=value\n", argv[0]);
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
auto req = laminar.setRequest();
|
if(char* pipeNum = getenv("__LAMINAR_SETENV_PIPE")) {
|
||||||
char* eq = strchr(argv[2], '=');
|
write(atoi(pipeNum), argv[2], strlen(argv[2]));
|
||||||
char* job = getenv("JOB");
|
|
||||||
char* num = getenv("RUN");
|
|
||||||
if(job && num && eq) {
|
|
||||||
char* name = argv[2];
|
|
||||||
*eq++ = '\0';
|
|
||||||
char* val = eq;
|
|
||||||
req.getRun().setJob(job);
|
|
||||||
req.getRun().setBuildNum(atoi(num));
|
|
||||||
req.getParam().setName(name);
|
|
||||||
req.getParam().setValue(val);
|
|
||||||
req.send().wait(waitScope);
|
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Missing $JOB or $RUN or param is not in the format key=value\n");
|
fprintf(stderr, "Must be run from within a laminar job\n");
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
} else if(strcmp(argv[1], "abort") == 0) {
|
} else if(strcmp(argv[1], "abort") == 0) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015 Oliver Giles
|
/// Copyright 2015-2019 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -16,28 +16,36 @@
|
|||||||
/// You should have received a copy of the GNU General Public License
|
/// You should have received a copy of the GNU General Public License
|
||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
///
|
///
|
||||||
#ifndef LAMINAR_NODE_H_
|
#ifndef LAMINAR_CONTEXT_H_
|
||||||
#define LAMINAR_NODE_H_
|
#define LAMINAR_CONTEXT_H_
|
||||||
|
|
||||||
|
#include <fnmatch.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
class Run;
|
class Run;
|
||||||
|
|
||||||
// Represents a group of executors. Currently almost unnecessary POD
|
// Represents a context within which a Run will be executed. Allows applying
|
||||||
// abstraction, but may be enhanced in the future to support e.g. tags
|
// a certain environment to a set of Jobs, or setting a limit on the number
|
||||||
class Node {
|
// of parallel Runs
|
||||||
|
class Context {
|
||||||
public:
|
public:
|
||||||
Node() {}
|
Context() {}
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
int numExecutors;
|
int numExecutors;
|
||||||
int busyExecutors = 0;
|
int busyExecutors = 0;
|
||||||
std::set<std::string> tags;
|
|
||||||
|
|
||||||
// Attempts to queue the given run to this node. Returns true if succeeded.
|
bool canQueue(std::set<std::string>& patterns) {
|
||||||
bool queue(const Run& run);
|
if(busyExecutors >= numExecutors)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for(std::string pattern : patterns) {
|
||||||
|
if(fnmatch(pattern.c_str(), name.c_str(), FNM_EXTMATCH) == 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#endif // LAMINAR_NODE_H_
|
#endif // LAMINAR_CONTEXT_H_
|
||||||
@@ -268,8 +268,8 @@ Http::Http(Laminar &li) :
|
|||||||
|
|
||||||
Http::~Http()
|
Http::~Http()
|
||||||
{
|
{
|
||||||
KJ_ASSERT(logWatchers.size() == 0);
|
LASSERT(logWatchers.size() == 0);
|
||||||
KJ_ASSERT(eventPeers.size() == 0);
|
LASSERT(eventPeers.size() == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
kj::Promise<void> Http::startServer(kj::Timer& timer, kj::Own<kj::ConnectionReceiver>&& listener)
|
kj::Promise<void> Http::startServer(kj::Timer& timer, kj::Own<kj::ConnectionReceiver>&& listener)
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ interface LaminarCi {
|
|||||||
queue @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult);
|
queue @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult);
|
||||||
start @1 (jobName :Text, params :List(JobParam)) -> (result :MethodResult, buildNum :UInt32);
|
start @1 (jobName :Text, params :List(JobParam)) -> (result :MethodResult, buildNum :UInt32);
|
||||||
run @2 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32);
|
run @2 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32);
|
||||||
set @3 (run :Run, param :JobParam) -> (result :MethodResult);
|
listQueued @3 () -> (result :List(Text));
|
||||||
listQueued @4 () -> (result :List(Text));
|
listRunning @4 () -> (result :List(Run));
|
||||||
listRunning @5 () -> (result :List(Run));
|
listKnown @5 () -> (result :List(Text));
|
||||||
listKnown @6 () -> (result :List(Text));
|
abort @6 (run :Run) -> (result :MethodResult);
|
||||||
abort @7 (run :Run) -> (result :MethodResult);
|
|
||||||
|
|
||||||
struct Run {
|
struct Run {
|
||||||
job @0 :Text;
|
job @0 :Text;
|
||||||
|
|||||||
231
src/laminar.cpp
231
src/laminar.cpp
@@ -76,6 +76,11 @@ Laminar::Laminar(Server &server, Settings settings) :
|
|||||||
{
|
{
|
||||||
LASSERT(settings.home[0] == '/');
|
LASSERT(settings.home[0] == '/');
|
||||||
|
|
||||||
|
if(fsHome->exists(homePath/"cfg"/"nodes")) {
|
||||||
|
LLOG(ERROR, "Found node configuration directory cfg/nodes. Nodes have been deprecated, please migrate to contexts. Laminar will now exit.");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
archiveUrl = settings.archive_url;
|
archiveUrl = settings.archive_url;
|
||||||
if(archiveUrl.back() != '/')
|
if(archiveUrl.back() != '/')
|
||||||
archiveUrl.append("/");
|
archiveUrl.append("/");
|
||||||
@@ -104,8 +109,9 @@ Laminar::Laminar(Server &server, Settings settings) :
|
|||||||
loadConfiguration();
|
loadConfiguration();
|
||||||
// config change may allow stuck jobs to dequeue
|
// config change may allow stuck jobs to dequeue
|
||||||
assignNewJobs();
|
assignNewJobs();
|
||||||
}).addPath((homePath/"cfg"/"nodes").toString(true).cStr())
|
}).addPath((homePath/"cfg"/"contexts").toString(true).cStr())
|
||||||
.addPath((homePath/"cfg"/"jobs").toString(true).cStr());
|
.addPath((homePath/"cfg"/"jobs").toString(true).cStr())
|
||||||
|
.addPath((homePath/"cfg").toString(true).cStr()); // for groups.conf
|
||||||
|
|
||||||
srv.listenRpc(*rpc, settings.bind_rpc);
|
srv.listenRpc(*rpc, settings.bind_rpc);
|
||||||
srv.listenHttp(*http, settings.bind_http);
|
srv.listenHttp(*http, settings.bind_http);
|
||||||
@@ -283,7 +289,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
const std::shared_ptr<Run> run = *it;
|
const std::shared_ptr<Run> run = *it;
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("number", run->build);
|
j.set("number", run->build);
|
||||||
j.set("node", run->node->name);
|
j.set("context", run->context->name);
|
||||||
j.set("started", run->startedAt);
|
j.set("started", run->startedAt);
|
||||||
j.set("result", to_string(RunState::RUNNING));
|
j.set("result", to_string(RunState::RUNNING));
|
||||||
j.set("reason", run->reason());
|
j.set("reason", run->reason());
|
||||||
@@ -311,7 +317,8 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.set("number", build).set("started", started);
|
j.set("number", build).set("started", started);
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
});
|
});
|
||||||
|
auto desc = jobDescriptions.find(scope.job);
|
||||||
|
j.set("description", desc == jobDescriptions.end() ? "" : desc->second);
|
||||||
} else if(scope.type == MonitorScope::ALL) {
|
} else if(scope.type == MonitorScope::ALL) {
|
||||||
j.startArray("jobs");
|
j.startArray("jobs");
|
||||||
db->stmt("SELECT name,number,startedAt,completedAt,result FROM builds b JOIN (SELECT name n,MAX(number) l FROM builds GROUP BY n) q ON b.name = q.n AND b.number = q.l")
|
db->stmt("SELECT name,number,startedAt,completedAt,result FROM builds b JOIN (SELECT name n,MAX(number) l FROM builds GROUP BY n) q ON b.name = q.n AND b.number = q.l")
|
||||||
@@ -322,11 +329,6 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.set("result", to_string(RunState(result)));
|
j.set("result", to_string(RunState(result)));
|
||||||
j.set("started", started);
|
j.set("started", started);
|
||||||
j.set("completed", completed);
|
j.set("completed", completed);
|
||||||
j.startArray("tags");
|
|
||||||
for(const str& t: jobTags[name]) {
|
|
||||||
j.String(t.c_str());
|
|
||||||
}
|
|
||||||
j.EndArray();
|
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
});
|
});
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
@@ -335,24 +337,23 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", run->name);
|
j.set("name", run->name);
|
||||||
j.set("number", run->build);
|
j.set("number", run->build);
|
||||||
j.set("node", run->node->name);
|
j.set("context", run->context->name);
|
||||||
j.set("started", run->startedAt);
|
j.set("started", run->startedAt);
|
||||||
j.startArray("tags");
|
|
||||||
for(const str& t: jobTags[run->name]) {
|
|
||||||
j.String(t.c_str());
|
|
||||||
}
|
|
||||||
j.EndArray();
|
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
}
|
}
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
|
j.startObject("groups");
|
||||||
|
for(const auto& group : jobGroups)
|
||||||
|
j.set(group.first.c_str(), group.second);
|
||||||
|
j.EndObject();
|
||||||
} else { // Home page
|
} else { // Home page
|
||||||
j.startArray("recent");
|
j.startArray("recent");
|
||||||
db->stmt("SELECT * FROM builds ORDER BY completedAt DESC LIMIT 15")
|
db->stmt("SELECT * FROM builds ORDER BY completedAt DESC LIMIT 15")
|
||||||
.fetch<str,uint,str,time_t,time_t,time_t,int>([&](str name,uint build,str node,time_t,time_t started,time_t completed,int result){
|
.fetch<str,uint,str,time_t,time_t,time_t,int>([&](str name,uint build,str context,time_t,time_t started,time_t completed,int result){
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", name)
|
j.set("name", name)
|
||||||
.set("number", build)
|
.set("number", build)
|
||||||
.set("node", node)
|
.set("context", context)
|
||||||
.set("started", started)
|
.set("started", started)
|
||||||
.set("completed", completed)
|
.set("completed", completed)
|
||||||
.set("result", to_string(RunState(result)))
|
.set("result", to_string(RunState(result)))
|
||||||
@@ -364,7 +365,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", run->name);
|
j.set("name", run->name);
|
||||||
j.set("number", run->build);
|
j.set("number", run->build);
|
||||||
j.set("node", run->node->name);
|
j.set("context", run->context->name);
|
||||||
j.set("started", run->startedAt);
|
j.set("started", run->startedAt);
|
||||||
db->stmt("SELECT completedAt - startedAt FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
|
db->stmt("SELECT completedAt - startedAt FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
|
||||||
.bind(run->name)
|
.bind(run->name)
|
||||||
@@ -383,10 +384,10 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.EndArray();
|
j.EndArray();
|
||||||
int execTotal = 0;
|
int execTotal = 0;
|
||||||
int execBusy = 0;
|
int execBusy = 0;
|
||||||
for(const auto& it : nodes) {
|
for(const auto& it : contexts) {
|
||||||
const std::shared_ptr<Node>& node = it.second;
|
const std::shared_ptr<Context>& context = it.second;
|
||||||
execTotal += node->numExecutors;
|
execTotal += context->numExecutors;
|
||||||
execBusy += node->busyExecutors;
|
execBusy += context->busyExecutors;
|
||||||
}
|
}
|
||||||
j.set("executorsTotal", execTotal);
|
j.set("executorsTotal", execTotal);
|
||||||
j.set("executorsBusy", execBusy);
|
j.set("executorsBusy", execBusy);
|
||||||
@@ -488,51 +489,41 @@ bool Laminar::loadConfiguration() {
|
|||||||
if(const char* ndirs = getenv("LAMINAR_KEEP_RUNDIRS"))
|
if(const char* ndirs = getenv("LAMINAR_KEEP_RUNDIRS"))
|
||||||
numKeepRunDirs = static_cast<uint>(atoi(ndirs));
|
numKeepRunDirs = static_cast<uint>(atoi(ndirs));
|
||||||
|
|
||||||
std::set<std::string> knownNodes;
|
std::set<std::string> knownContexts;
|
||||||
|
|
||||||
KJ_IF_MAYBE(nodeDir, fsHome->tryOpenSubdir(kj::Path{"cfg","nodes"})) {
|
KJ_IF_MAYBE(contextsDir, fsHome->tryOpenSubdir(kj::Path{"cfg","contexts"})) {
|
||||||
for(kj::Directory::Entry& entry : (*nodeDir)->listEntries()) {
|
for(kj::Directory::Entry& entry : (*contextsDir)->listEntries()) {
|
||||||
if(!entry.name.endsWith(".conf"))
|
if(!entry.name.endsWith(".conf"))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
StringMap conf = parseConfFile((homePath/"cfg"/"nodes"/entry.name).toString(true).cStr());
|
StringMap conf = parseConfFile((homePath/"cfg"/"contexts"/entry.name).toString(true).cStr());
|
||||||
|
|
||||||
std::string nodeName(entry.name.cStr(), entry.name.findLast('.').orDefault(0));
|
std::string name(entry.name.cStr(), entry.name.findLast('.').orDefault(0));
|
||||||
auto existingNode = nodes.find(nodeName);
|
auto existing = contexts.find(name);
|
||||||
std::shared_ptr<Node> node = existingNode == nodes.end() ? nodes.emplace(nodeName, std::shared_ptr<Node>(new Node)).first->second : existingNode->second;
|
std::shared_ptr<Context> context = existing == contexts.end() ? contexts.emplace(name, std::shared_ptr<Context>(new Context)).first->second : existing->second;
|
||||||
node->name = nodeName;
|
context->name = name;
|
||||||
node->numExecutors = conf.get<int>("EXECUTORS", 6);
|
context->numExecutors = conf.get<int>("EXECUTORS", 6);
|
||||||
|
|
||||||
std::string tagString = conf.get<std::string>("TAGS");
|
knownContexts.insert(name);
|
||||||
std::set<std::string> tagList;
|
|
||||||
if(!tagString.empty()) {
|
|
||||||
std::istringstream iss(tagString);
|
|
||||||
std::string tag;
|
|
||||||
while(std::getline(iss, tag, ','))
|
|
||||||
tagList.insert(tag);
|
|
||||||
}
|
|
||||||
std::swap(node->tags, tagList);
|
|
||||||
|
|
||||||
knownNodes.insert(nodeName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove any nodes whose config files disappeared.
|
// remove any contexts whose config files disappeared.
|
||||||
// if there are no known nodes, take care not to remove and re-add the default node
|
// if there are no known contexts, take care not to remove and re-add the default context.
|
||||||
for(auto it = nodes.begin(); it != nodes.end();) {
|
for(auto it = contexts.begin(); it != contexts.end();) {
|
||||||
if((it->first == "" && knownNodes.size() == 0) || knownNodes.find(it->first) != knownNodes.end())
|
if((it->first == "default" && knownContexts.size() == 0) || knownContexts.find(it->first) != knownContexts.end())
|
||||||
it++;
|
it++;
|
||||||
else
|
else
|
||||||
it = nodes.erase(it);
|
it = contexts.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add a default node
|
// add a default context
|
||||||
if(nodes.empty()) {
|
if(contexts.empty()) {
|
||||||
LLOG(INFO, "Creating a default node with 6 executors");
|
LLOG(INFO, "Creating a default context with 6 executors");
|
||||||
std::shared_ptr<Node> node(new Node);
|
std::shared_ptr<Context> context(new Context);
|
||||||
node->name = "";
|
context->name = "default";
|
||||||
node->numExecutors = 6;
|
context->numExecutors = 6;
|
||||||
nodes.emplace("", node);
|
contexts.emplace("default", context);
|
||||||
}
|
}
|
||||||
|
|
||||||
KJ_IF_MAYBE(jobsDir, fsHome->tryOpenSubdir(kj::Path{"cfg","jobs"})) {
|
KJ_IF_MAYBE(jobsDir, fsHome->tryOpenSubdir(kj::Path{"cfg","jobs"})) {
|
||||||
@@ -543,19 +534,29 @@ bool Laminar::loadConfiguration() {
|
|||||||
|
|
||||||
std::string jobName(entry.name.cStr(), entry.name.findLast('.').orDefault(0));
|
std::string jobName(entry.name.cStr(), entry.name.findLast('.').orDefault(0));
|
||||||
|
|
||||||
std::string tags = conf.get<std::string>("TAGS");
|
std::string ctxPtns = conf.get<std::string>("CONTEXTS");
|
||||||
if(!tags.empty()) {
|
|
||||||
std::istringstream iss(tags);
|
|
||||||
std::set<std::string> tagList;
|
|
||||||
std::string tag;
|
|
||||||
while(std::getline(iss, tag, ','))
|
|
||||||
tagList.insert(tag);
|
|
||||||
jobTags[jobName] = tagList;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if(!ctxPtns.empty()) {
|
||||||
|
std::istringstream iss(ctxPtns);
|
||||||
|
std::set<std::string> ctxPtnList;
|
||||||
|
std::string ctx;
|
||||||
|
while(std::getline(iss, ctx, ','))
|
||||||
|
ctxPtnList.insert(ctx);
|
||||||
|
jobContexts[jobName].swap(ctxPtnList);
|
||||||
|
}
|
||||||
|
std::string desc = conf.get<std::string>("DESCRIPTION");
|
||||||
|
if(!desc.empty()) {
|
||||||
|
jobDescriptions[jobName] = desc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobGroups.clear();
|
||||||
|
KJ_IF_MAYBE(groupsConf, fsHome->tryOpenFile(kj::Path{"cfg","groups.conf"}))
|
||||||
|
jobGroups = parseConfFile((homePath/"cfg"/"groups.conf").toString(true).cStr());
|
||||||
|
if(jobGroups.empty())
|
||||||
|
jobGroups["All Jobs"] = ".*";
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +566,10 @@ std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the job has no contexts (maybe there is no .conf file at all), add the default context
|
||||||
|
if(jobContexts[name].empty())
|
||||||
|
jobContexts.at(name).insert("default");
|
||||||
|
|
||||||
std::shared_ptr<Run> run = std::make_shared<Run>(name, kj::mv(params), homePath.clone());
|
std::shared_ptr<Run> run = std::make_shared<Run>(name, kj::mv(params), homePath.clone());
|
||||||
queuedJobs.push_back(run);
|
queuedJobs.push_back(run);
|
||||||
|
|
||||||
@@ -581,48 +586,25 @@ std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Laminar::abort(std::string job, uint buildNum) {
|
bool Laminar::abort(std::string job, uint buildNum) {
|
||||||
if(Run* run = activeRun(job, buildNum)) {
|
if(Run* run = activeRun(job, buildNum))
|
||||||
run->abort(true);
|
return run->abort();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Laminar::abortAll() {
|
void Laminar::abortAll() {
|
||||||
for(std::shared_ptr<Run> run : activeJobs) {
|
for(std::shared_ptr<Run> run : activeJobs) {
|
||||||
run->abort(false);
|
run->abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Laminar::nodeCanQueue(const Node& node, std::string jobName) const {
|
|
||||||
// if a node is too busy, it can't take the job
|
|
||||||
if(node.busyExecutors >= node.numExecutors)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// if the node has no tags, allow the build
|
|
||||||
if(node.tags.size() == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
auto it = jobTags.find(jobName);
|
|
||||||
// if the job has no tags, it cannot be run on this node
|
|
||||||
if(it == jobTags.end())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// otherwise, allow the build if job and node have a tag in common
|
|
||||||
for(const std::string& tag : it->second) {
|
|
||||||
if(node.tags.find(tag) != node.tags.end())
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
||||||
for(auto& sn : nodes) {
|
for(auto& sc : contexts) {
|
||||||
std::shared_ptr<Node> node = sn.second;
|
std::shared_ptr<Context> ctx = sc.second;
|
||||||
|
|
||||||
if(nodeCanQueue(*node.get(), run->name) && run->configure(buildNums[run->name] + 1, node, *fsHome)) {
|
if(ctx->canQueue(jobContexts.at(run->name))) {
|
||||||
node->busyExecutors++;
|
kj::Promise<RunState> onRunFinished = run->start(buildNums[run->name] + 1, ctx, *fsHome,[this](kj::Maybe<pid_t>& pid){return srv.onChildExit(pid);});
|
||||||
|
|
||||||
|
ctx->busyExecutors++;
|
||||||
// set the last known result if exists
|
// set the last known result if exists
|
||||||
db->stmt("SELECT result FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
|
db->stmt("SELECT result FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
|
||||||
.bind(run->name)
|
.bind(run->name)
|
||||||
@@ -630,17 +612,24 @@ bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
|||||||
run->lastResult = RunState(result);
|
run->lastResult = RunState(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actually schedules the Run steps
|
kj::Promise<void> exec = srv.readDescriptor(run->output_fd, [this, run](const char*b, size_t n){
|
||||||
kj::Promise<void> exec = handleRunStep(run.get()).then([=]{
|
// handle log output
|
||||||
runFinished(run.get());
|
std::string s(b, n);
|
||||||
|
run->log += s;
|
||||||
|
http->notifyLog(run->name, run->build, s, false);
|
||||||
|
}).then([run, p = kj::mv(onRunFinished)]() mutable {
|
||||||
|
// wait until leader reaped
|
||||||
|
return kj::mv(p);
|
||||||
|
}).then([this, run](RunState){
|
||||||
|
handleRunFinished(run.get());
|
||||||
});
|
});
|
||||||
if(run->timeout > 0) {
|
if(run->timeout > 0) {
|
||||||
exec = exec.attach(srv.addTimeout(run->timeout, [r=run.get()](){
|
exec = exec.attach(srv.addTimeout(run->timeout, [r=run.get()](){
|
||||||
r->abort(true);
|
r->abort();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
srv.addTask(kj::mv(exec));
|
srv.addTask(kj::mv(exec));
|
||||||
LLOG(INFO, "Started job on node", run->name, run->build, node->name);
|
LLOG(INFO, "Started job", run->name, run->build, ctx->name);
|
||||||
|
|
||||||
// update next build number
|
// update next build number
|
||||||
buildNums[run->name]++;
|
buildNums[run->name]++;
|
||||||
@@ -660,11 +649,6 @@ bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
|||||||
.fetch<uint>([&](uint etc){
|
.fetch<uint>([&](uint etc){
|
||||||
j.set("etc", time(nullptr) + etc);
|
j.set("etc", time(nullptr) + etc);
|
||||||
});
|
});
|
||||||
j.startArray("tags");
|
|
||||||
for(const str& t: jobTags[run->name]) {
|
|
||||||
j.String(t.c_str());
|
|
||||||
}
|
|
||||||
j.EndArray();
|
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
http->notifyEvent(j.str(), run->name.c_str());
|
http->notifyEvent(j.str(), run->name.c_str());
|
||||||
return true;
|
return true;
|
||||||
@@ -685,34 +669,10 @@ void Laminar::assignNewJobs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kj::Promise<void> Laminar::handleRunStep(Run* run) {
|
void Laminar::handleRunFinished(Run * r) {
|
||||||
if(run->step()) {
|
std::shared_ptr<Context> ctx = r->context;
|
||||||
// no more steps
|
|
||||||
return kj::READY_NOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
kj::Promise<int> exited = srv.onChildExit(run->current_pid);
|
ctx->busyExecutors--;
|
||||||
// promise is fulfilled when the process is reaped. But first we wait for all
|
|
||||||
// output from the pipe (Run::output_fd) to be consumed.
|
|
||||||
return srv.readDescriptor(run->output_fd, [this,run](const char*b,size_t n){
|
|
||||||
// handle log output
|
|
||||||
std::string s(b, n);
|
|
||||||
run->log += s;
|
|
||||||
http->notifyLog(run->name, run->build, s, false);
|
|
||||||
}).then([p = std::move(exited)]() mutable {
|
|
||||||
// wait until the process is reaped
|
|
||||||
return kj::mv(p);
|
|
||||||
}).then([this, run](int status){
|
|
||||||
run->reaped(status);
|
|
||||||
// next step in Run
|
|
||||||
return handleRunStep(run);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void Laminar::runFinished(Run * r) {
|
|
||||||
std::shared_ptr<Node> node = r->node;
|
|
||||||
|
|
||||||
node->busyExecutors--;
|
|
||||||
LLOG(INFO, "Run completed", r->name, to_string(r->result));
|
LLOG(INFO, "Run completed", r->name, to_string(r->result));
|
||||||
time_t completedAt = time(nullptr);
|
time_t completedAt = time(nullptr);
|
||||||
|
|
||||||
@@ -731,7 +691,7 @@ void Laminar::runFinished(Run * r) {
|
|||||||
|
|
||||||
std::string reason = r->reason();
|
std::string reason = r->reason();
|
||||||
db->stmt("INSERT INTO builds VALUES(?,?,?,?,?,?,?,?,?,?,?,?)")
|
db->stmt("INSERT INTO builds VALUES(?,?,?,?,?,?,?,?,?,?,?,?)")
|
||||||
.bind(r->name, r->build, node->name, r->queuedAt, r->startedAt, completedAt, int(r->result),
|
.bind(r->name, r->build, ctx->name, r->queuedAt, r->startedAt, completedAt, int(r->result),
|
||||||
maybeZipped, logsize, r->parentName, r->parentBuild, reason)
|
maybeZipped, logsize, r->parentName, r->parentBuild, reason)
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
@@ -746,11 +706,6 @@ void Laminar::runFinished(Run * r) {
|
|||||||
.set("started", r->startedAt)
|
.set("started", r->startedAt)
|
||||||
.set("result", to_string(r->result))
|
.set("result", to_string(r->result))
|
||||||
.set("reason", r->reason());
|
.set("reason", r->reason());
|
||||||
j.startArray("tags");
|
|
||||||
for(const str& t: jobTags[r->name]) {
|
|
||||||
j.String(t.c_str());
|
|
||||||
}
|
|
||||||
j.EndArray();
|
|
||||||
j.startArray("artifacts");
|
j.startArray("artifacts");
|
||||||
populateArtifacts(j, r->name, r->build);
|
populateArtifacts(j, r->name, r->build);
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
|
|||||||
@@ -21,15 +21,15 @@
|
|||||||
|
|
||||||
#include "run.h"
|
#include "run.h"
|
||||||
#include "monitorscope.h"
|
#include "monitorscope.h"
|
||||||
#include "node.h"
|
#include "context.h"
|
||||||
#include "database.h"
|
#include "database.h"
|
||||||
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <kj/filesystem.h>
|
#include <kj/filesystem.h>
|
||||||
#include <kj/async-io.h>
|
#include <kj/async-io.h>
|
||||||
|
|
||||||
// Node name to node object map
|
// Context name to context object map
|
||||||
typedef std::unordered_map<std::string, std::shared_ptr<Node>> NodeMap;
|
typedef std::unordered_map<std::string, std::shared_ptr<Context>> ContextMap;
|
||||||
|
|
||||||
struct Server;
|
struct Server;
|
||||||
class Json;
|
class Json;
|
||||||
@@ -105,9 +105,7 @@ private:
|
|||||||
bool loadConfiguration();
|
bool loadConfiguration();
|
||||||
void assignNewJobs();
|
void assignNewJobs();
|
||||||
bool tryStartRun(std::shared_ptr<Run> run, int queueIndex);
|
bool tryStartRun(std::shared_ptr<Run> run, int queueIndex);
|
||||||
kj::Promise<void> handleRunStep(Run *run);
|
void handleRunFinished(Run*);
|
||||||
void runFinished(Run*);
|
|
||||||
bool nodeCanQueue(const Node&, std::string jobName) const;
|
|
||||||
// expects that Json has started an array
|
// expects that Json has started an array
|
||||||
void populateArtifacts(Json& out, std::string job, uint num) const;
|
void populateArtifacts(Json& out, std::string job, uint num) const;
|
||||||
|
|
||||||
@@ -120,13 +118,17 @@ private:
|
|||||||
|
|
||||||
std::unordered_map<std::string, uint> buildNums;
|
std::unordered_map<std::string, uint> buildNums;
|
||||||
|
|
||||||
std::unordered_map<std::string, std::set<std::string>> jobTags;
|
std::unordered_map<std::string, std::set<std::string>> jobContexts;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> jobDescriptions;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> jobGroups;
|
||||||
|
|
||||||
Settings settings;
|
Settings settings;
|
||||||
RunSet activeJobs;
|
RunSet activeJobs;
|
||||||
Database* db;
|
Database* db;
|
||||||
Server& srv;
|
Server& srv;
|
||||||
NodeMap nodes;
|
ContextMap contexts;
|
||||||
kj::Path homePath;
|
kj::Path homePath;
|
||||||
kj::Own<const kj::Directory> fsHome;
|
kj::Own<const kj::Directory> fsHome;
|
||||||
uint numKeepRunDirs;
|
uint numKeepRunDirs;
|
||||||
|
|||||||
295
src/leader.cpp
Normal file
295
src/leader.cpp
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
///
|
||||||
|
/// Copyright 2019 Oliver Giles
|
||||||
|
///
|
||||||
|
/// This file is part of Laminar
|
||||||
|
///
|
||||||
|
/// Laminar is free software: you can redistribute it and/or modify
|
||||||
|
/// it under the terms of the GNU General Public License as published by
|
||||||
|
/// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
/// (at your option) any later version.
|
||||||
|
///
|
||||||
|
/// Laminar is distributed in the hope that it will be useful,
|
||||||
|
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
/// GNU General Public License for more details.
|
||||||
|
///
|
||||||
|
/// You should have received a copy of the GNU General Public License
|
||||||
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
///
|
||||||
|
#include "log.h"
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <queue>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <kj/async-io.h>
|
||||||
|
#include <kj/async-unix.h>
|
||||||
|
#include <kj/filesystem.h>
|
||||||
|
|
||||||
|
#include "run.h"
|
||||||
|
|
||||||
|
// short syntax helper for kj::Path
|
||||||
|
template<typename T>
|
||||||
|
inline kj::Path operator/(const kj::Path& p, const T& ext) {
|
||||||
|
return p.append(ext);
|
||||||
|
}
|
||||||
|
template<typename T>
|
||||||
|
inline kj::Path operator/(const kj::PathPtr& p, const T& ext) {
|
||||||
|
return p.append(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Script {
|
||||||
|
kj::Path path;
|
||||||
|
kj::Path cwd;
|
||||||
|
bool runOnAbort;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Leader final : public kj::TaskSet::ErrorHandler {
|
||||||
|
public:
|
||||||
|
Leader(kj::AsyncIoContext& ioContext, kj::Filesystem& fs, const char* jobName, uint runNumber);
|
||||||
|
RunState run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void taskFailed(kj::Exception&& exception) override;
|
||||||
|
kj::Promise<void> step(std::queue<Script>& scripts);
|
||||||
|
kj::Promise<void> reapChildProcesses();
|
||||||
|
kj::Promise<void> readEnvPipe(kj::AsyncInputStream* stream, char* buffer);
|
||||||
|
|
||||||
|
kj::TaskSet tasks;
|
||||||
|
RunState result;
|
||||||
|
kj::AsyncIoContext& ioContext;
|
||||||
|
const kj::Directory& home;
|
||||||
|
kj::PathPtr rootPath;
|
||||||
|
std::string jobName;
|
||||||
|
uint runNumber;
|
||||||
|
pid_t currentGroupId;
|
||||||
|
pid_t currentScriptPid;
|
||||||
|
std::queue<Script> scripts;
|
||||||
|
int setEnvPipe[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
Leader::Leader(kj::AsyncIoContext &ioContext, kj::Filesystem &fs, const char *jobName, uint runNumber) :
|
||||||
|
tasks(*this),
|
||||||
|
result(RunState::SUCCESS),
|
||||||
|
ioContext(ioContext),
|
||||||
|
home(fs.getCurrent()),
|
||||||
|
rootPath(fs.getCurrentPath()),
|
||||||
|
jobName(jobName),
|
||||||
|
runNumber(runNumber)
|
||||||
|
{
|
||||||
|
tasks.add(ioContext.unixEventPort.onSignal(SIGTERM).then([this](siginfo_t) {
|
||||||
|
while(scripts.size() && (!scripts.front().runOnAbort))
|
||||||
|
scripts.pop();
|
||||||
|
// TODO: probably shouldn't do this if we are already in a runOnAbort script
|
||||||
|
kill(-currentGroupId, SIGTERM);
|
||||||
|
// TODO: wait a few seconds for exit, then send KILL?
|
||||||
|
}));
|
||||||
|
|
||||||
|
pipe(setEnvPipe);
|
||||||
|
auto event = ioContext.lowLevelProvider->wrapInputFd(setEnvPipe[0], kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP);
|
||||||
|
auto buffer = kj::heapArrayBuilder<char>(1024);
|
||||||
|
tasks.add(readEnvPipe(event, buffer.asPtr().begin()).attach(kj::mv(event), kj::mv(buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
RunState Leader::run()
|
||||||
|
{
|
||||||
|
kj::Path cfgDir{"cfg"};
|
||||||
|
|
||||||
|
// create the run directory
|
||||||
|
kj::Path rd{"run",jobName,std::to_string(runNumber)};
|
||||||
|
bool createWorkdir = true;
|
||||||
|
KJ_IF_MAYBE(ls, home.tryLstat(rd)) {
|
||||||
|
LASSERT(ls->type == kj::FsNode::Type::DIRECTORY);
|
||||||
|
LLOG(WARNING, "Working directory already exists, removing", rd.toString());
|
||||||
|
if(home.tryRemove(rd) == false) {
|
||||||
|
LLOG(WARNING, "Failed to remove working directory");
|
||||||
|
createWorkdir = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(createWorkdir && home.tryOpenSubdir(rd, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT) == nullptr) {
|
||||||
|
LLOG(ERROR, "Could not create working directory", rd.toString());
|
||||||
|
return RunState::FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an archive directory
|
||||||
|
kj::Path archive = kj::Path{"archive",jobName,std::to_string(runNumber)};
|
||||||
|
if(home.exists(archive)) {
|
||||||
|
LLOG(WARNING, "Archive directory already exists", archive.toString());
|
||||||
|
} else if(home.tryOpenSubdir(archive, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT) == nullptr) {
|
||||||
|
LLOG(ERROR, "Could not create archive directory", archive.toString());
|
||||||
|
return RunState::FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a workspace for this job if it doesn't exist
|
||||||
|
kj::Path ws{"run",jobName,"workspace"};
|
||||||
|
if(!home.exists(ws)) {
|
||||||
|
home.openSubdir(ws, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT);
|
||||||
|
// prepend the workspace init script
|
||||||
|
if(home.exists(cfgDir/"jobs"/(jobName+".init")))
|
||||||
|
scripts.push({cfgDir/"jobs"/(jobName+".init"), kj::mv(ws), false});
|
||||||
|
}
|
||||||
|
|
||||||
|
// add scripts
|
||||||
|
// global before-run script
|
||||||
|
if(home.exists(cfgDir/"before"))
|
||||||
|
scripts.push({cfgDir/"before", rd.clone(), false});
|
||||||
|
// job before-run script
|
||||||
|
if(home.exists(cfgDir/"jobs"/(jobName+".before")))
|
||||||
|
scripts.push({cfgDir/"jobs"/(jobName+".before"), rd.clone(), false});
|
||||||
|
// main run script. must exist.
|
||||||
|
scripts.push({cfgDir/"jobs"/(jobName+".run"), rd.clone(), false});
|
||||||
|
// job after-run script
|
||||||
|
if(home.exists(cfgDir/"jobs"/(jobName+".after")))
|
||||||
|
scripts.push({cfgDir/"jobs"/(jobName+".after"), rd.clone(), true});
|
||||||
|
// global after-run script
|
||||||
|
if(home.exists(cfgDir/"after"))
|
||||||
|
scripts.push({cfgDir/"after", rd.clone(), true});
|
||||||
|
|
||||||
|
// Start executing scripts
|
||||||
|
return step(scripts).then([this](){
|
||||||
|
return result;
|
||||||
|
}).wait(ioContext.waitScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Leader::taskFailed(kj::Exception &&exception)
|
||||||
|
{
|
||||||
|
LLOG(ERROR, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<void> Leader::step(std::queue<Script> &scripts)
|
||||||
|
{
|
||||||
|
if(scripts.empty())
|
||||||
|
return kj::READY_NOW;
|
||||||
|
|
||||||
|
Script currentScript = kj::mv(scripts.front());
|
||||||
|
scripts.pop();
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if(pid == 0) { // child
|
||||||
|
// unblock all signals
|
||||||
|
sigset_t mask;
|
||||||
|
sigfillset(&mask);
|
||||||
|
sigprocmask(SIG_UNBLOCK, &mask, nullptr);
|
||||||
|
|
||||||
|
// create a new process group to help us deal with any wayward forks
|
||||||
|
setpgid(0, 0);
|
||||||
|
|
||||||
|
std::string buildNum = std::to_string(runNumber);
|
||||||
|
|
||||||
|
LSYSCALL(chdir(currentScript.cwd.toString(false).cStr()));
|
||||||
|
|
||||||
|
setenv("RESULT", to_string(result).c_str(), true);
|
||||||
|
|
||||||
|
// pass the pipe through a variable to allow laminarc to send new env back
|
||||||
|
char pipeNum[4];
|
||||||
|
sprintf(pipeNum, "%d", setEnvPipe[1]);
|
||||||
|
setenv("__LAMINAR_SETENV_PIPE", pipeNum, 1);
|
||||||
|
|
||||||
|
fprintf(stderr, "[laminar] Executing %s\n", currentScript.path.toString().cStr());
|
||||||
|
kj::String execPath = (rootPath/currentScript.path).toString(true);
|
||||||
|
|
||||||
|
execl(execPath.cStr(), execPath.cStr(), NULL);
|
||||||
|
fprintf(stderr, "[laminar] Failed to execute %s\n", currentScript.path.toString().cStr());
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentScriptPid = pid;
|
||||||
|
currentGroupId = pid;
|
||||||
|
|
||||||
|
return reapChildProcesses().then([&](){
|
||||||
|
return step(scripts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<void> Leader::reapChildProcesses()
|
||||||
|
{
|
||||||
|
return ioContext.unixEventPort.onSignal(SIGCHLD).then([this](siginfo_t) -> kj::Promise<void> {
|
||||||
|
while(true) {
|
||||||
|
int status;
|
||||||
|
errno = 0;
|
||||||
|
pid_t pid = waitpid(-1, &status, WNOHANG);
|
||||||
|
if(pid == -1 && errno == ECHILD) {
|
||||||
|
// all children exited
|
||||||
|
return kj::READY_NOW;
|
||||||
|
} else if(pid == 0) {
|
||||||
|
// child processes are still running
|
||||||
|
if(currentScriptPid) {
|
||||||
|
// We could get here if a more deeply nested process was reparented to us
|
||||||
|
// before the primary script executed. Quietly wait until the process we're
|
||||||
|
// waiting for is done
|
||||||
|
return reapChildProcesses();
|
||||||
|
}
|
||||||
|
// Otherwise, reparented orphans are on borrowed time
|
||||||
|
// TODO list wayward processes?
|
||||||
|
fprintf(stderr, "[laminar] sending SIGHUP to adopted child processes\n");
|
||||||
|
kill(-currentGroupId, SIGHUP);
|
||||||
|
return ioContext.provider->getTimer().afterDelay(5*kj::SECONDS).then([this]{
|
||||||
|
fprintf(stderr, "[laminar] sending SIGKILL to process group %d\n", currentGroupId);
|
||||||
|
// TODO: should we mark the job as failed if we had to kill reparented processes?
|
||||||
|
kill(-currentGroupId, SIGKILL);
|
||||||
|
return reapChildProcesses();
|
||||||
|
}).exclusiveJoin(reapChildProcesses());
|
||||||
|
} else if(pid == currentScriptPid) {
|
||||||
|
// the script we were waiting for is done
|
||||||
|
// if we already marked as failed, preserve that
|
||||||
|
if(result == RunState::SUCCESS) {
|
||||||
|
if(WIFSIGNALED(status) && (WTERMSIG(status) == SIGTERM || WTERMSIG(status) == SIGKILL))
|
||||||
|
result = RunState::ABORTED;
|
||||||
|
else if(WEXITSTATUS(status) != 0)
|
||||||
|
result = RunState::FAILED;
|
||||||
|
}
|
||||||
|
currentScriptPid = 0;
|
||||||
|
} else {
|
||||||
|
// some reparented process was reaped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<void> Leader::readEnvPipe(kj::AsyncInputStream *stream, char *buffer) {
|
||||||
|
return stream->tryRead(buffer, 1, 1024).then([this,stream,buffer](size_t sz) {
|
||||||
|
if(sz > 0) {
|
||||||
|
buffer[sz] = '\0';
|
||||||
|
if(char* eq = strchr(buffer, '=')) {
|
||||||
|
*eq++ = '\0';
|
||||||
|
setenv(buffer, eq, 1);
|
||||||
|
}
|
||||||
|
return readEnvPipe(stream, kj::mv(buffer));
|
||||||
|
}
|
||||||
|
return kj::Promise<void>(kj::READY_NOW);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int leader_main(void) {
|
||||||
|
auto ioContext = kj::setupAsyncIo();
|
||||||
|
auto fs = kj::newDiskFilesystem();
|
||||||
|
|
||||||
|
kj::UnixEventPort::captureSignal(SIGTERM);
|
||||||
|
// Don't use captureChildExit or onChildExit because they don't provide a way to
|
||||||
|
// reap orphaned child processes. Stick with the more fundamental onSignal.
|
||||||
|
kj::UnixEventPort::captureSignal(SIGCHLD);
|
||||||
|
|
||||||
|
// Becoming a subreaper means any descendent process whose parent process disappears
|
||||||
|
// will be reparented to this one instead of init (or higher layer subreaper).
|
||||||
|
// We do this so that the run will wait until all descedents exit before executing
|
||||||
|
// the next step.
|
||||||
|
prctl(PR_SET_CHILD_SUBREAPER, 1, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
// Become the leader of a new process group. This is so that all child processes
|
||||||
|
// will also get a kill signal when the run is aborted
|
||||||
|
setpgid(0, 0);
|
||||||
|
|
||||||
|
// Environment inherited from main laminard process
|
||||||
|
const char* jobName = getenv("JOB");
|
||||||
|
std::string name(jobName);
|
||||||
|
uint runNumber = atoi(getenv("RUN"));
|
||||||
|
|
||||||
|
if(!jobName || !runNumber)
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
|
||||||
|
Leader leader(ioContext, *fs, jobName, runNumber);
|
||||||
|
|
||||||
|
// Parent process will cast back to RunState
|
||||||
|
return int(leader.run());
|
||||||
|
}
|
||||||
36
src/leader.h
Normal file
36
src/leader.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
///
|
||||||
|
/// Copyright 2019 Oliver Giles
|
||||||
|
///
|
||||||
|
/// This file is part of Laminar
|
||||||
|
///
|
||||||
|
/// Laminar is free software: you can redistribute it and/or modify
|
||||||
|
/// it under the terms of the GNU General Public License as published by
|
||||||
|
/// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
/// (at your option) any later version.
|
||||||
|
///
|
||||||
|
/// Laminar is distributed in the hope that it will be useful,
|
||||||
|
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
/// GNU General Public License for more details.
|
||||||
|
///
|
||||||
|
/// You should have received a copy of the GNU General Public License
|
||||||
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
///
|
||||||
|
#ifndef LAMINAR_LEADER_H_
|
||||||
|
#define LAMINAR_LEADER_H_
|
||||||
|
|
||||||
|
// Main function for the leader process which is responsible for
|
||||||
|
// executing all the scripts which make up a Run. Separating this
|
||||||
|
// into its own process allows for a cleaner process tree view,
|
||||||
|
// where it's obvious which script belongs to which run of which
|
||||||
|
// job, and allows this leader process to act as a subreaper for
|
||||||
|
// any wayward child processes.
|
||||||
|
|
||||||
|
// This could have been implemented as a separate process, but
|
||||||
|
// instead we just fork & exec /proc/self/exe from the main laminar
|
||||||
|
// daemon, and distinguish based on argv[0]. This saves installing
|
||||||
|
// another binary and avoids some associated pitfalls.
|
||||||
|
|
||||||
|
int leader_main(void);
|
||||||
|
|
||||||
|
#endif // LAMINAR_LEADER_H_
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
///
|
///
|
||||||
#include "laminar.h"
|
#include "laminar.h"
|
||||||
|
#include "leader.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
@@ -40,9 +41,10 @@ constexpr const char* INTADDR_HTTP_DEFAULT = "*:8080";
|
|||||||
constexpr const char* ARCHIVE_URL_DEFAULT = "/archive/";
|
constexpr const char* ARCHIVE_URL_DEFAULT = "/archive/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
|
if(argv[0][0] == '{')
|
||||||
|
return leader_main();
|
||||||
|
|
||||||
for(int i = 1; i < argc; ++i) {
|
for(int i = 1; i < argc; ++i) {
|
||||||
if(strcmp(argv[i], "-v") == 0) {
|
if(strcmp(argv[i], "-v") == 0) {
|
||||||
kj::_::Debug::setLogLevel(kj::_::Debug::Severity::INFO);
|
kj::_::Debug::setLogLevel(kj::_::Debug::Severity::INFO);
|
||||||
|
|||||||
@@ -207,8 +207,8 @@
|
|||||||
<input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
|
<input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li :class="{'active':tag==null}"><a href v-on:click.prevent="tag = null">All Jobs</a></li>
|
<li v-show="ungrouped.length" :class="{'active':group==null}"><a href v-on:click.prevent="group = null">Ungrouped Jobs</a></li>
|
||||||
<li v-for="t in tags" :class="{'active':t==tag}"><a href v-on:click.prevent="tag = t">{{t}}</a></li>
|
<li v-for="g in Object.keys(groups)" :class="{'active':g==group}"><a href v-on:click.prevent="group = g">{{g}}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<table class="table table-striped" id="joblist">
|
<table class="table table-striped" id="joblist">
|
||||||
<tr v-for="job in filteredJobs()">
|
<tr v-for="job in filteredJobs()">
|
||||||
@@ -228,6 +228,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-5 col-md-6 col-lg-7">
|
<div class="col-sm-5 col-md-6 col-lg-7">
|
||||||
<h3>{{$route.params.name}}</h3>
|
<h3>{{$route.params.name}}</h3>
|
||||||
|
<div v-html="description"></div>
|
||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
<dt>Last Successful Run</dt>
|
<dt>Last Successful Run</dt>
|
||||||
<dd><router-link v-if="lastSuccess" :to="'/jobs/'+$route.params.name+'/'+lastSuccess.number">#{{lastSuccess.number}}</router-link> {{lastSuccess?' - at '+formatDate(lastSuccess.started):'never'}}</dd>
|
<dd><router-link v-if="lastSuccess" :to="'/jobs/'+$route.params.name+'/'+lastSuccess.number">#{{lastSuccess.number}}</router-link> {{lastSuccess?' - at '+formatDate(lastSuccess.started):'never'}}</dd>
|
||||||
|
|||||||
@@ -415,8 +415,9 @@ const Jobs = function() {
|
|||||||
var state = {
|
var state = {
|
||||||
jobs: [],
|
jobs: [],
|
||||||
search: '',
|
search: '',
|
||||||
tags: [],
|
groups: {},
|
||||||
tag: null
|
group: null,
|
||||||
|
ungrouped: []
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
template: '#jobs',
|
template: '#jobs',
|
||||||
@@ -437,13 +438,10 @@ const Jobs = function() {
|
|||||||
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var tags = {};
|
state.groups = {};
|
||||||
for (var i in state.jobs) {
|
Object.keys(msg.groups).map(k => state.groups[k] = new RegExp(msg.groups[k]));
|
||||||
for (var j in state.jobs[i].tags) {
|
state.ungrouped = state.jobs.filter(j => !Object.values(state.groups).some(r => r.test(j.name))).map(j => j.name);
|
||||||
tags[state.jobs[i].tags[j]] = true;
|
state.group = state.ungrouped.length ? null : Object.keys(state.groups)[0];
|
||||||
}
|
|
||||||
}
|
|
||||||
state.tags = Object.keys(tags);
|
|
||||||
},
|
},
|
||||||
job_started: function(data) {
|
job_started: function(data) {
|
||||||
var updAt = null;
|
var updAt = null;
|
||||||
@@ -470,6 +468,8 @@ const Jobs = function() {
|
|||||||
// first execution of new job. TODO insert without resort
|
// first execution of new job. TODO insert without resort
|
||||||
state.jobs.unshift(data);
|
state.jobs.unshift(data);
|
||||||
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
||||||
|
if(!Object.values(state.groups).some(r => r.test(data.name)))
|
||||||
|
state.ungrouped.push(data.name);
|
||||||
} else {
|
} else {
|
||||||
state.jobs[updAt] = data;
|
state.jobs[updAt] = data;
|
||||||
}
|
}
|
||||||
@@ -492,19 +492,13 @@ const Jobs = function() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
filteredJobs: function() {
|
filteredJobs: function() {
|
||||||
var ret = state.jobs;
|
let ret = [];
|
||||||
var tag = state.tag;
|
if (state.group)
|
||||||
if (tag) {
|
ret = state.jobs.filter(job => state.groups[state.group].test(job.name));
|
||||||
ret = ret.filter(function(job) {
|
else
|
||||||
return job.tags.indexOf(tag) >= 0;
|
ret = state.jobs.filter(job => state.ungrouped.includes(job.name));
|
||||||
});
|
if (this.search)
|
||||||
}
|
ret = ret.filter(job => job.name.indexOf(this.search) > -1);
|
||||||
var search = this.search;
|
|
||||||
if (search) {
|
|
||||||
ret = ret.filter(function(job) {
|
|
||||||
return job.name.indexOf(search) > -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -513,6 +507,7 @@ const Jobs = function() {
|
|||||||
|
|
||||||
var Job = function() {
|
var Job = function() {
|
||||||
var state = {
|
var state = {
|
||||||
|
description: '',
|
||||||
jobsRunning: [],
|
jobsRunning: [],
|
||||||
jobsRecent: [],
|
jobsRecent: [],
|
||||||
lastSuccess: null,
|
lastSuccess: null,
|
||||||
@@ -530,6 +525,7 @@ var Job = function() {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
status: function(msg) {
|
status: function(msg) {
|
||||||
|
state.description = msg.description;
|
||||||
state.jobsRunning = msg.running;
|
state.jobsRunning = msg.running;
|
||||||
state.jobsRecent = msg.recent;
|
state.jobsRecent = msg.recent;
|
||||||
state.lastSuccess = msg.lastSuccess;
|
state.lastSuccess = msg.lastSuccess;
|
||||||
|
|||||||
20
src/rpc.cpp
20
src/rpc.cpp
@@ -81,10 +81,10 @@ public:
|
|||||||
std::string jobName = context.getParams().getJobName();
|
std::string jobName = context.getParams().getJobName();
|
||||||
LLOG(INFO, "RPC run", jobName);
|
LLOG(INFO, "RPC run", jobName);
|
||||||
std::shared_ptr<Run> run = laminar.queueJob(jobName, params(context.getParams().getParams()));
|
std::shared_ptr<Run> run = laminar.queueJob(jobName, params(context.getParams().getParams()));
|
||||||
if(Run* r = run.get()) {
|
if(run) {
|
||||||
return r->whenFinished().then([context,r](RunState state) mutable {
|
return run->whenFinished().then([context,run](RunState state) mutable {
|
||||||
context.getResults().setResult(fromRunState(state));
|
context.getResults().setResult(fromRunState(state));
|
||||||
context.getResults().setBuildNum(r->build);
|
context.getResults().setBuildNum(run->build);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
context.getResults().setResult(LaminarCi::JobResult::UNKNOWN);
|
context.getResults().setResult(LaminarCi::JobResult::UNKNOWN);
|
||||||
@@ -92,20 +92,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a parameter on a running build
|
|
||||||
kj::Promise<void> set(SetContext context) override {
|
|
||||||
std::string jobName = context.getParams().getRun().getJob();
|
|
||||||
uint buildNum = context.getParams().getRun().getBuildNum();
|
|
||||||
LLOG(INFO, "RPC set", jobName, buildNum);
|
|
||||||
|
|
||||||
LaminarCi::MethodResult result = laminar.setParam(jobName, buildNum,
|
|
||||||
context.getParams().getParam().getName(), context.getParams().getParam().getValue())
|
|
||||||
? LaminarCi::MethodResult::SUCCESS
|
|
||||||
: LaminarCi::MethodResult::FAILED;
|
|
||||||
context.getResults().setResult(result);
|
|
||||||
return kj::READY_NOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
// List jobs in queue
|
// List jobs in queue
|
||||||
kj::Promise<void> listQueued(ListQueuedContext context) override {
|
kj::Promise<void> listQueued(ListQueuedContext context) override {
|
||||||
const std::list<std::shared_ptr<Run>>& queue = laminar.listQueuedJobs();
|
const std::list<std::shared_ptr<Run>>& queue = laminar.listQueuedJobs();
|
||||||
|
|||||||
247
src/run.cpp
247
src/run.cpp
@@ -17,7 +17,7 @@
|
|||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
///
|
///
|
||||||
#include "run.h"
|
#include "run.h"
|
||||||
#include "node.h"
|
#include "context.h"
|
||||||
#include "conf.h"
|
#include "conf.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
|
||||||
@@ -52,7 +52,9 @@ Run::Run(std::string name, ParamMap pm, kj::Path&& rootPath) :
|
|||||||
queuedAt(time(nullptr)),
|
queuedAt(time(nullptr)),
|
||||||
rootPath(kj::mv(rootPath)),
|
rootPath(kj::mv(rootPath)),
|
||||||
started(kj::newPromiseAndFulfiller<void>()),
|
started(kj::newPromiseAndFulfiller<void>()),
|
||||||
finished(kj::newPromiseAndFulfiller<RunState>())
|
startedFork(started.promise.fork()),
|
||||||
|
finished(kj::newPromiseAndFulfiller<RunState>()),
|
||||||
|
finishedFork(finished.promise.fork())
|
||||||
{
|
{
|
||||||
for(auto it = params.begin(); it != params.end();) {
|
for(auto it = params.begin(); it != params.end();) {
|
||||||
if(it->first[0] == '=') {
|
if(it->first[0] == '=') {
|
||||||
@@ -75,119 +77,53 @@ Run::~Run() {
|
|||||||
LLOG(INFO, "Run destroyed");
|
LLOG(INFO, "Run destroyed");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Run::configure(uint buildNum, std::shared_ptr<Node> nd, const kj::Directory& fsHome)
|
static void setEnvFromFile(const kj::Path& rootPath, kj::Path file) {
|
||||||
|
StringMap vars = parseConfFile((rootPath/file).toString(true).cStr());
|
||||||
|
for(auto& it : vars) {
|
||||||
|
setenv(it.first.c_str(), it.second.c_str(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<RunState> Run::start(uint buildNum, std::shared_ptr<Context> ctx, const kj::Directory &fsHome, std::function<kj::Promise<int>(kj::Maybe<pid_t>&)> getPromise)
|
||||||
{
|
{
|
||||||
kj::Path cfgDir{"cfg"};
|
kj::Path cfgDir{"cfg"};
|
||||||
|
|
||||||
// create the run directory
|
|
||||||
kj::Path rd{"run",name,std::to_string(buildNum)};
|
|
||||||
bool createWorkdir = true;
|
|
||||||
KJ_IF_MAYBE(ls, fsHome.tryLstat(rd)) {
|
|
||||||
LASSERT(ls->type == kj::FsNode::Type::DIRECTORY);
|
|
||||||
LLOG(WARNING, "Working directory already exists, removing", rd.toString());
|
|
||||||
if(fsHome.tryRemove(rd) == false) {
|
|
||||||
LLOG(WARNING, "Failed to remove working directory");
|
|
||||||
createWorkdir = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(createWorkdir && fsHome.tryOpenSubdir(rd, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT) == nullptr) {
|
|
||||||
LLOG(ERROR, "Could not create working directory", rd.toString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create an archive directory
|
|
||||||
kj::Path archive = kj::Path{"archive",name,std::to_string(buildNum)};
|
|
||||||
if(fsHome.exists(archive)) {
|
|
||||||
LLOG(WARNING, "Archive directory already exists", archive.toString());
|
|
||||||
} else if(fsHome.tryOpenSubdir(archive, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT) == nullptr) {
|
|
||||||
LLOG(ERROR, "Could not create archive directory", archive.toString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a workspace for this job if it doesn't exist
|
|
||||||
kj::Path ws{"run",name,"workspace"};
|
|
||||||
if(!fsHome.exists(ws)) {
|
|
||||||
fsHome.openSubdir(ws, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT);
|
|
||||||
// prepend the workspace init script
|
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".init")))
|
|
||||||
addScript(cfgDir/"jobs"/(name+".init"), kj::mv(ws));
|
|
||||||
}
|
|
||||||
|
|
||||||
// add scripts
|
|
||||||
// global before-run script
|
|
||||||
if(fsHome.exists(cfgDir/"before"))
|
|
||||||
addScript(cfgDir/"before", rd.clone());
|
|
||||||
// per-node before-run script
|
|
||||||
if(fsHome.exists(cfgDir/"nodes"/(nd->name+".before")))
|
|
||||||
addScript(cfgDir/"nodes"/(nd->name+".before"), rd.clone());
|
|
||||||
// job before-run script
|
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".before")))
|
|
||||||
addScript(cfgDir/"jobs"/(name+".before"), rd.clone());
|
|
||||||
// main run script. must exist.
|
|
||||||
addScript(cfgDir/"jobs"/(name+".run"), rd.clone());
|
|
||||||
// job after-run script
|
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".after")))
|
|
||||||
addScript(cfgDir/"jobs"/(name+".after"), rd.clone(), true);
|
|
||||||
// per-node after-run script
|
|
||||||
if(fsHome.exists(cfgDir/"nodes"/(nd->name+".after")))
|
|
||||||
addScript(cfgDir/"nodes"/(nd->name+".after"), rd.clone(), true);
|
|
||||||
// global after-run script
|
|
||||||
if(fsHome.exists(cfgDir/"after"))
|
|
||||||
addScript(cfgDir/"after", rd.clone(), true);
|
|
||||||
|
|
||||||
// add environment files
|
|
||||||
if(fsHome.exists(cfgDir/"env"))
|
|
||||||
addEnv(cfgDir/"env");
|
|
||||||
if(fsHome.exists(cfgDir/"nodes"/(nd->name+".env")))
|
|
||||||
addEnv(cfgDir/"nodes"/(nd->name+".env"));
|
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".env")))
|
|
||||||
addEnv(cfgDir/"jobs"/(name+".env"));
|
|
||||||
|
|
||||||
// add job timeout if specified
|
// add job timeout if specified
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".conf"))) {
|
if(fsHome.exists(cfgDir/"jobs"/(name+".conf"))) {
|
||||||
timeout = parseConfFile((rootPath/cfgDir/"jobs"/(name+".conf")).toString(true).cStr()).get<int>("TIMEOUT", 0);
|
timeout = parseConfFile((rootPath/cfgDir/"jobs"/(name+".conf")).toString(true).cStr()).get<int>("TIMEOUT", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// All good, we've "started"
|
int plog[2];
|
||||||
startedAt = time(nullptr);
|
LSYSCALL(pipe(plog));
|
||||||
build = buildNum;
|
|
||||||
node = nd;
|
|
||||||
|
|
||||||
// notifies the rpc client if the start command was used
|
// Fork a process leader to run all the steps of the job. This gives us a nice
|
||||||
started.fulfiller->fulfill();
|
// process tree output (job name and number as the process name) and helps
|
||||||
|
// contain any wayward descendent processes.
|
||||||
|
pid_t leader;
|
||||||
|
LSYSCALL(leader = fork());
|
||||||
|
|
||||||
return true;
|
if(leader == 0) {
|
||||||
}
|
// All output from this process will be captured in the plog pipe
|
||||||
|
close(plog[0]);
|
||||||
|
dup2(plog[1], STDOUT_FILENO);
|
||||||
|
dup2(plog[1], STDERR_FILENO);
|
||||||
|
close(plog[1]);
|
||||||
|
|
||||||
std::string Run::reason() const {
|
// All initial/fixed env vars can be set here. Dynamic ones, including
|
||||||
return reasonMsg;
|
// "RESULT" and any set by `laminarc set` have to be handled in the subprocess.
|
||||||
}
|
|
||||||
|
|
||||||
bool Run::step() {
|
// add environment files
|
||||||
if(!scripts.size())
|
if(fsHome.exists(cfgDir/"env"))
|
||||||
return true;
|
setEnvFromFile(rootPath, cfgDir/"env");
|
||||||
|
if(fsHome.exists(cfgDir/"contexts"/(ctx->name+".env")))
|
||||||
|
setEnvFromFile(rootPath, cfgDir/"contexts"/(ctx->name+".env"));
|
||||||
|
if(fsHome.exists(cfgDir/"jobs"/(name+".env")))
|
||||||
|
setEnvFromFile(rootPath, cfgDir/"jobs"/(name+".env"));
|
||||||
|
|
||||||
Script currentScript = kj::mv(scripts.front());
|
// parameterized vars
|
||||||
scripts.pop();
|
for(auto& pair : params) {
|
||||||
|
setenv(pair.first.c_str(), pair.second.c_str(), false);
|
||||||
int pfd[2];
|
}
|
||||||
pipe(pfd);
|
|
||||||
pid_t pid = fork();
|
|
||||||
if(pid == 0) { // child
|
|
||||||
// reset signal mask (SIGCHLD blocked in Laminar::start)
|
|
||||||
sigset_t mask;
|
|
||||||
sigemptyset(&mask);
|
|
||||||
sigaddset(&mask, SIGCHLD);
|
|
||||||
sigprocmask(SIG_UNBLOCK, &mask, nullptr);
|
|
||||||
|
|
||||||
// set pgid == pid for easy killing on abort
|
|
||||||
setpgid(0, 0);
|
|
||||||
|
|
||||||
close(pfd[0]);
|
|
||||||
dup2(pfd[1], 1);
|
|
||||||
dup2(pfd[1], 2);
|
|
||||||
close(pfd[1]);
|
|
||||||
std::string buildNum = std::to_string(build);
|
|
||||||
|
|
||||||
std::string PATH = (rootPath/"cfg"/"scripts").toString(true).cStr();
|
std::string PATH = (rootPath/"cfg"/"scripts").toString(true).cStr();
|
||||||
if(const char* p = getenv("PATH")) {
|
if(const char* p = getenv("PATH")) {
|
||||||
@@ -195,73 +131,62 @@ bool Run::step() {
|
|||||||
PATH.append(p);
|
PATH.append(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
LSYSCALL(chdir((rootPath/currentScript.cwd).toString(true).cStr()));
|
std::string runNumStr = std::to_string(buildNum);
|
||||||
|
|
||||||
// conf file env vars
|
|
||||||
for(kj::Path& file : env) {
|
|
||||||
StringMap vars = parseConfFile((rootPath/file).toString(true).cStr());
|
|
||||||
for(auto& it : vars) {
|
|
||||||
setenv(it.first.c_str(), it.second.c_str(), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// parameterized vars
|
|
||||||
for(auto& pair : params) {
|
|
||||||
setenv(pair.first.c_str(), pair.second.c_str(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
setenv("PATH", PATH.c_str(), true);
|
setenv("PATH", PATH.c_str(), true);
|
||||||
setenv("RUN", buildNum.c_str(), true);
|
setenv("RUN", runNumStr.c_str(), true);
|
||||||
setenv("JOB", name.c_str(), true);
|
setenv("JOB", name.c_str(), true);
|
||||||
if(!node->name.empty())
|
setenv("CONTEXT", ctx->name.c_str(), true);
|
||||||
setenv("NODE", node->name.c_str(), true);
|
|
||||||
setenv("RESULT", to_string(result).c_str(), true);
|
|
||||||
setenv("LAST_RESULT", to_string(lastResult).c_str(), true);
|
setenv("LAST_RESULT", to_string(lastResult).c_str(), true);
|
||||||
setenv("WORKSPACE", (rootPath/"run"/name/"workspace").toString(true).cStr(), true);
|
setenv("WORKSPACE", (rootPath/"run"/name/"workspace").toString(true).cStr(), true);
|
||||||
setenv("ARCHIVE", (rootPath/"archive"/name/buildNum).toString(true).cStr(), true);
|
setenv("ARCHIVE", (rootPath/"archive"/name/runNumStr).toString(true).cStr(), true);
|
||||||
|
// RESULT set in leader process
|
||||||
|
|
||||||
fprintf(stderr, "[laminar] Executing %s\n", currentScript.path.toString().cStr());
|
// leader process assumes $LAMINAR_HOME as CWD
|
||||||
kj::String execPath = (rootPath/currentScript.path).toString(true);
|
LSYSCALL(chdir(rootPath.toString(true).cStr()));
|
||||||
execl(execPath.cStr(), execPath.cStr(), NULL);
|
setenv("PWD", rootPath.toString(true).cStr(), 1);
|
||||||
// cannot use LLOG because stdout/stderr are captured
|
|
||||||
fprintf(stderr, "[laminar] Failed to execute %s\n", currentScript.path.toString().cStr());
|
// We could just fork/wait over all the steps here directly, but then we
|
||||||
_exit(1);
|
// can't set a nice name for the process tree. There is pthread_setname_np,
|
||||||
|
// but it's limited to 16 characters, which most of the time probably isn't
|
||||||
|
// enough. Instead, we'll just exec ourselves and handle that in laminard's
|
||||||
|
// main() by calling leader_main()
|
||||||
|
char* procName;
|
||||||
|
asprintf(&procName, "{laminar} %s:%d", name.data(), buildNum);
|
||||||
|
execl("/proc/self/exe", procName, NULL); // does not return
|
||||||
|
_exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
LLOG(INFO, "Forked", currentScript.path, currentScript.cwd, pid);
|
// All good, we've "started"
|
||||||
close(pfd[1]);
|
startedAt = time(nullptr);
|
||||||
|
build = buildNum;
|
||||||
|
context = ctx;
|
||||||
|
|
||||||
current_pid = pid;
|
output_fd = plog[0];
|
||||||
output_fd = pfd[0];
|
close(plog[1]);
|
||||||
|
pid = leader;
|
||||||
|
|
||||||
|
// notifies the rpc client if the start command was used
|
||||||
|
started.fulfiller->fulfill();
|
||||||
|
|
||||||
|
return getPromise(pid).then([this](int status){
|
||||||
|
// The leader process passes a RunState through the return value.
|
||||||
|
// Check it didn't die abnormally, then cast to get it back.
|
||||||
|
result = WIFEXITED(status) ? RunState(WEXITSTATUS(status)) : RunState::ABORTED;
|
||||||
|
finished.fulfiller->fulfill(RunState(result));
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Run::reason() const {
|
||||||
|
return reasonMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Run::abort() {
|
||||||
|
// if the Maybe is empty, wait() was already called on this process
|
||||||
|
KJ_IF_MAYBE(p, pid) {
|
||||||
|
kill(-*p, SIGTERM);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Run::addScript(kj::Path scriptPath, kj::Path scriptWorkingDir, bool runOnAbort) {
|
|
||||||
scripts.push({kj::mv(scriptPath), kj::mv(scriptWorkingDir), runOnAbort});
|
|
||||||
}
|
|
||||||
|
|
||||||
void Run::addEnv(kj::Path path) {
|
|
||||||
env.push_back(kj::mv(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Run::abort(bool respectRunOnAbort) {
|
|
||||||
while(scripts.size() && (!respectRunOnAbort || !scripts.front().runOnAbort))
|
|
||||||
scripts.pop();
|
|
||||||
// if the Maybe is empty, wait() was already called on this process
|
|
||||||
KJ_IF_MAYBE(p, current_pid) {
|
|
||||||
kill(-*p, SIGTERM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Run::reaped(int status) {
|
|
||||||
// once state is non-success it cannot change again
|
|
||||||
if(result != RunState::SUCCESS)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if(WIFSIGNALED(status) && (WTERMSIG(status) == SIGTERM || WTERMSIG(status) == SIGKILL))
|
|
||||||
result = RunState::ABORTED;
|
|
||||||
else if(status != 0)
|
|
||||||
result = RunState::FAILED;
|
|
||||||
// otherwise preserve earlier status
|
|
||||||
|
|
||||||
finished.fulfiller->fulfill(RunState(result));
|
|
||||||
}
|
|
||||||
|
|||||||
33
src/run.h
33
src/run.h
@@ -43,7 +43,7 @@ enum class RunState {
|
|||||||
|
|
||||||
std::string to_string(const RunState& rs);
|
std::string to_string(const RunState& rs);
|
||||||
|
|
||||||
class Node;
|
class Context;
|
||||||
|
|
||||||
typedef std::unordered_map<std::string, std::string> ParamMap;
|
typedef std::unordered_map<std::string, std::string> ParamMap;
|
||||||
|
|
||||||
@@ -57,26 +57,17 @@ public:
|
|||||||
Run(const Run&) = delete;
|
Run(const Run&) = delete;
|
||||||
Run& operator=(const Run&) = delete;
|
Run& operator=(const Run&) = delete;
|
||||||
|
|
||||||
// Call this to "start" the run with a specific number and node
|
kj::Promise<RunState> start(uint buildNum, std::shared_ptr<Context> ctx, const kj::Directory &fsHome, std::function<kj::Promise<int>(kj::Maybe<pid_t>&)> getPromise);
|
||||||
bool configure(uint buildNum, std::shared_ptr<Node> node, const kj::Directory &fsHome);
|
|
||||||
|
|
||||||
// executes the next script (if any), returning true if there is nothing
|
|
||||||
// more to be done.
|
|
||||||
bool step();
|
|
||||||
|
|
||||||
// aborts this run
|
// aborts this run
|
||||||
void abort(bool respectRunOnAbort);
|
bool abort();
|
||||||
|
|
||||||
// called when a process owned by this run has been reaped. The status
|
|
||||||
// may be used to set the run's job status
|
|
||||||
void reaped(int status);
|
|
||||||
|
|
||||||
std::string reason() const;
|
std::string reason() const;
|
||||||
|
|
||||||
kj::Promise<void>&& whenStarted() { return kj::mv(started.promise); }
|
kj::Promise<void> whenStarted() { return startedFork.addBranch(); }
|
||||||
kj::Promise<RunState>&& whenFinished() { return kj::mv(finished.promise); }
|
kj::Promise<RunState> whenFinished() { return finishedFork.addBranch(); }
|
||||||
|
|
||||||
std::shared_ptr<Node> node;
|
std::shared_ptr<Context> context;
|
||||||
RunState result;
|
RunState result;
|
||||||
RunState lastResult;
|
RunState lastResult;
|
||||||
std::string name;
|
std::string name;
|
||||||
@@ -84,7 +75,7 @@ public:
|
|||||||
int parentBuild = 0;
|
int parentBuild = 0;
|
||||||
uint build = 0;
|
uint build = 0;
|
||||||
std::string log;
|
std::string log;
|
||||||
kj::Maybe<pid_t> current_pid;
|
kj::Maybe<pid_t> pid;
|
||||||
int output_fd;
|
int output_fd;
|
||||||
std::unordered_map<std::string, std::string> params;
|
std::unordered_map<std::string, std::string> params;
|
||||||
int timeout = 0;
|
int timeout = 0;
|
||||||
@@ -105,13 +96,13 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
kj::Path rootPath;
|
kj::Path rootPath;
|
||||||
std::queue<Script> scripts;
|
|
||||||
std::list<kj::Path> env;
|
|
||||||
std::string reasonMsg;
|
std::string reasonMsg;
|
||||||
kj::PromiseFulfillerPair<void> started;
|
|
||||||
kj::PromiseFulfillerPair<RunState> finished;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
kj::PromiseFulfillerPair<void> started;
|
||||||
|
kj::ForkedPromise<void> startedFork;
|
||||||
|
kj::PromiseFulfillerPair<RunState> finished;
|
||||||
|
kj::ForkedPromise<RunState> finishedFork;
|
||||||
|
};
|
||||||
|
|
||||||
// All this below is a somewhat overengineered method of keeping track of
|
// All this below is a somewhat overengineered method of keeping track of
|
||||||
// currently executing builds (Run objects). This would probably scale
|
// currently executing builds (Run objects). This would probably scale
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "tempdir.h"
|
#include "tempdir.h"
|
||||||
#include "laminar.h"
|
#include "laminar.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
|
#include "conf.h"
|
||||||
|
|
||||||
class LaminarFixture : public ::testing::Test {
|
class LaminarFixture : public ::testing::Test {
|
||||||
public:
|
public:
|
||||||
@@ -38,7 +39,7 @@ public:
|
|||||||
settings.bind_rpc = bind_rpc.c_str();
|
settings.bind_rpc = bind_rpc.c_str();
|
||||||
settings.bind_http = bind_http.c_str();
|
settings.bind_http = bind_http.c_str();
|
||||||
settings.archive_url = "/test-archive/";
|
settings.archive_url = "/test-archive/";
|
||||||
server = new Server(ioContext);
|
server = new Server(*ioContext);
|
||||||
laminar = new Laminar(*server, settings);
|
laminar = new Laminar(*server, settings);
|
||||||
}
|
}
|
||||||
~LaminarFixture() noexcept(true) {
|
~LaminarFixture() noexcept(true) {
|
||||||
@@ -47,19 +48,76 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
kj::Own<EventSource> eventSource(const char* path) {
|
kj::Own<EventSource> eventSource(const char* path) {
|
||||||
return kj::heap<EventSource>(ioContext, bind_http.c_str(), path);
|
return kj::heap<EventSource>(*ioContext, bind_http.c_str(), path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void defineJob(const char* name, const char* scriptContent) {
|
void defineJob(const char* name, const char* scriptContent, const char* configContent = nullptr) {
|
||||||
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "jobs", std::string(name) + ".run"},
|
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "jobs", std::string(name) + ".run"},
|
||||||
kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT | kj::WriteMode::EXECUTABLE)) {
|
kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT | kj::WriteMode::EXECUTABLE)) {
|
||||||
(*f)->writeAll(std::string("#!/bin/sh\n") + scriptContent + "\n");
|
(*f)->writeAll(std::string("#!/bin/sh\n") + scriptContent + "\n");
|
||||||
}
|
}
|
||||||
|
if(configContent) {
|
||||||
|
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "jobs", std::string(name) + ".conf"}, kj::WriteMode::CREATE)) {
|
||||||
|
(*f)->writeAll(configContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RunExec {
|
||||||
|
LaminarCi::JobResult result;
|
||||||
|
kj::String log;
|
||||||
|
};
|
||||||
|
|
||||||
|
RunExec runJob(const char* name, kj::Maybe<StringMap> params = nullptr) {
|
||||||
|
auto req = client().runRequest();
|
||||||
|
req.setJobName(name);
|
||||||
|
KJ_IF_MAYBE(p, params) {
|
||||||
|
auto params = req.initParams(p->size());
|
||||||
|
int i = 0;
|
||||||
|
for(auto kv : *p) {
|
||||||
|
params[i].setName(kv.first);
|
||||||
|
params[i].setValue(kv.second);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto res = req.send().wait(ioContext->waitScope);
|
||||||
|
std::string path = std::string{"/log/"} + name + "/" + std::to_string(res.getBuildNum());
|
||||||
|
kj::HttpHeaderTable headerTable;
|
||||||
|
kj::String log = kj::newHttpClient(ioContext->lowLevelProvider->getTimer(), headerTable,
|
||||||
|
*ioContext->provider->getNetwork().parseAddress(bind_http.c_str()).wait(ioContext->waitScope))
|
||||||
|
->request(kj::HttpMethod::GET, path, kj::HttpHeaders(headerTable)).response.wait(ioContext->waitScope).body
|
||||||
|
->readAllText().wait(ioContext->waitScope);
|
||||||
|
return { res.getResult(), kj::mv(log) };
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::String stripLaminarLogLines(const kj::String& str) {
|
||||||
|
auto out = kj::heapString(str.size());
|
||||||
|
char *o = out.begin();
|
||||||
|
for(const char *p = str.cStr(), *e = p + str.size(); p < e;) {
|
||||||
|
const char *nl = strchrnul(p, '\n');
|
||||||
|
if(!kj::StringPtr{p}.startsWith("[laminar]")) {
|
||||||
|
memcpy(o, p, nl - p + 1);
|
||||||
|
o += nl - p + 1;
|
||||||
|
}
|
||||||
|
p = nl + 1;
|
||||||
|
}
|
||||||
|
*o = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringMap parseFromString(kj::StringPtr content) {
|
||||||
|
char tmp[16] = "/tmp/lt.XXXXXX";
|
||||||
|
int fd = mkstemp(tmp);
|
||||||
|
write(fd, content.begin(), content.size());
|
||||||
|
close(fd);
|
||||||
|
StringMap map = parseConfFile(tmp);
|
||||||
|
unlink(tmp);
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
LaminarCi::Client client() {
|
LaminarCi::Client client() {
|
||||||
if(!rpc) {
|
if(!rpc) {
|
||||||
auto stream = ioContext.provider->getNetwork().parseAddress(bind_rpc).wait(ioContext.waitScope)->connect().wait(ioContext.waitScope);
|
auto stream = ioContext->provider->getNetwork().parseAddress(bind_rpc).wait(ioContext->waitScope)->connect().wait(ioContext->waitScope);
|
||||||
auto net = kj::heap<capnp::TwoPartyVatNetwork>(*stream, capnp::rpc::twoparty::Side::CLIENT);
|
auto net = kj::heap<capnp::TwoPartyVatNetwork>(*stream, capnp::rpc::twoparty::Side::CLIENT);
|
||||||
rpc = kj::heap<capnp::RpcSystem<capnp::rpc::twoparty::VatId>>(*net, nullptr).attach(kj::mv(net), kj::mv(stream));
|
rpc = kj::heap<capnp::RpcSystem<capnp::rpc::twoparty::VatId>>(*net, nullptr).attach(kj::mv(net), kj::mv(stream));
|
||||||
}
|
}
|
||||||
@@ -76,7 +134,7 @@ public:
|
|||||||
Settings settings;
|
Settings settings;
|
||||||
Server* server;
|
Server* server;
|
||||||
Laminar* laminar;
|
Laminar* laminar;
|
||||||
static kj::AsyncIoContext ioContext;
|
static kj::AsyncIoContext* ioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // LAMINAR_FIXTURE_H_
|
#endif // LAMINAR_FIXTURE_H_
|
||||||
|
|||||||
@@ -18,13 +18,14 @@
|
|||||||
///
|
///
|
||||||
#include <kj/async-unix.h>
|
#include <kj/async-unix.h>
|
||||||
#include "laminar-fixture.h"
|
#include "laminar-fixture.h"
|
||||||
|
#include "conf.h"
|
||||||
|
|
||||||
// TODO: consider handling this differently
|
// TODO: consider handling this differently
|
||||||
kj::AsyncIoContext LaminarFixture::ioContext = kj::setupAsyncIo();
|
kj::AsyncIoContext* LaminarFixture::ioContext;
|
||||||
|
|
||||||
TEST_F(LaminarFixture, EmptyStatusMessageStructure) {
|
TEST_F(LaminarFixture, EmptyStatusMessageStructure) {
|
||||||
auto es = eventSource("/");
|
auto es = eventSource("/");
|
||||||
ioContext.waitScope.poll();
|
ioContext->waitScope.poll();
|
||||||
ASSERT_EQ(1, es->messages().size());
|
ASSERT_EQ(1, es->messages().size());
|
||||||
|
|
||||||
auto json = es->messages().front().GetObject();
|
auto json = es->messages().front().GetObject();
|
||||||
@@ -51,12 +52,7 @@ TEST_F(LaminarFixture, JobNotifyHomePage) {
|
|||||||
defineJob("foo", "true");
|
defineJob("foo", "true");
|
||||||
auto es = eventSource("/");
|
auto es = eventSource("/");
|
||||||
|
|
||||||
auto req = client().runRequest();
|
runJob("foo");
|
||||||
req.setJobName("foo");
|
|
||||||
ASSERT_EQ(LaminarCi::JobResult::SUCCESS, req.send().wait(ioContext.waitScope).getResult());
|
|
||||||
|
|
||||||
// wait for job completed
|
|
||||||
ioContext.waitScope.poll();
|
|
||||||
|
|
||||||
ASSERT_EQ(4, es->messages().size());
|
ASSERT_EQ(4, es->messages().size());
|
||||||
|
|
||||||
@@ -84,13 +80,8 @@ TEST_F(LaminarFixture, OnlyRelevantNotifications) {
|
|||||||
auto es1Run = eventSource("/jobs/job1/1");
|
auto es1Run = eventSource("/jobs/job1/1");
|
||||||
auto es2Run = eventSource("/jobs/job2/1");
|
auto es2Run = eventSource("/jobs/job2/1");
|
||||||
|
|
||||||
auto req1 = client().runRequest();
|
runJob("job1");
|
||||||
req1.setJobName("job1");
|
runJob("job2");
|
||||||
ASSERT_EQ(LaminarCi::JobResult::SUCCESS, req1.send().wait(ioContext.waitScope).getResult());
|
|
||||||
auto req2 = client().runRequest();
|
|
||||||
req2.setJobName("job2");
|
|
||||||
ASSERT_EQ(LaminarCi::JobResult::SUCCESS, req2.send().wait(ioContext.waitScope).getResult());
|
|
||||||
ioContext.waitScope.poll();
|
|
||||||
|
|
||||||
EXPECT_EQ(7, esHome->messages().size());
|
EXPECT_EQ(7, esHome->messages().size());
|
||||||
EXPECT_EQ(7, esJobs->messages().size());
|
EXPECT_EQ(7, esJobs->messages().size());
|
||||||
@@ -101,3 +92,74 @@ TEST_F(LaminarFixture, OnlyRelevantNotifications) {
|
|||||||
EXPECT_EQ(4, es1Run->messages().size());
|
EXPECT_EQ(4, es1Run->messages().size());
|
||||||
EXPECT_EQ(4, es2Run->messages().size());
|
EXPECT_EQ(4, es2Run->messages().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, FailedStatus) {
|
||||||
|
defineJob("job1", "false");
|
||||||
|
auto run = runJob("job1");
|
||||||
|
ASSERT_EQ(LaminarCi::JobResult::FAILED, run.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, WorkingDirectory) {
|
||||||
|
defineJob("job1", "pwd");
|
||||||
|
auto run = runJob("job1");
|
||||||
|
ASSERT_EQ(LaminarCi::JobResult::SUCCESS, run.result);
|
||||||
|
std::string cwd{tmp.path.append(kj::Path{"run","job1","1"}).toString(true).cStr()};
|
||||||
|
EXPECT_EQ(cwd + "\n", stripLaminarLogLines(run.log).cStr());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, Environment) {
|
||||||
|
defineJob("foo", "env");
|
||||||
|
auto run = runJob("foo");
|
||||||
|
|
||||||
|
std::string ws{tmp.path.append(kj::Path{"run","foo","workspace"}).toString(true).cStr()};
|
||||||
|
std::string archive{tmp.path.append(kj::Path{"archive","foo","1"}).toString(true).cStr()};
|
||||||
|
|
||||||
|
StringMap map = parseFromString(run.log);
|
||||||
|
EXPECT_EQ("1", map["RUN"]);
|
||||||
|
EXPECT_EQ("foo", map["JOB"]);
|
||||||
|
EXPECT_EQ("success", map["RESULT"]);
|
||||||
|
EXPECT_EQ("unknown", map["LAST_RESULT"]);
|
||||||
|
EXPECT_EQ(ws, map["WORKSPACE"]);
|
||||||
|
EXPECT_EQ(archive, map["ARCHIVE"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, ParamsToEnv) {
|
||||||
|
defineJob("foo", "env");
|
||||||
|
StringMap params;
|
||||||
|
params["foo"] = "bar";
|
||||||
|
auto run = runJob("foo", params);
|
||||||
|
StringMap map = parseFromString(run.log);
|
||||||
|
EXPECT_EQ("bar", map["foo"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, Abort) {
|
||||||
|
defineJob("job1", "yes");
|
||||||
|
auto req = client().runRequest();
|
||||||
|
req.setJobName("job1");
|
||||||
|
auto res = req.send();
|
||||||
|
// There isn't a nice way of knowing when the leader process is ready to
|
||||||
|
// handle SIGTERM. Just wait until it prints something to the log
|
||||||
|
ioContext->waitScope.poll();
|
||||||
|
kj::HttpHeaderTable headerTable;
|
||||||
|
char _;
|
||||||
|
kj::newHttpClient(ioContext->lowLevelProvider->getTimer(), headerTable,
|
||||||
|
*ioContext->provider->getNetwork().parseAddress(bind_http.c_str()).wait(ioContext->waitScope))
|
||||||
|
->request(kj::HttpMethod::GET, "/log/job1/1", kj::HttpHeaders(headerTable)).response.wait(ioContext->waitScope).body
|
||||||
|
->tryRead(&_, 1, 1).wait(ioContext->waitScope);
|
||||||
|
// now it should be ready to abort
|
||||||
|
ASSERT_TRUE(laminar->abort("job1", 1));
|
||||||
|
EXPECT_EQ(LaminarCi::JobResult::ABORTED, res.wait(ioContext->waitScope).getResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, JobDescription) {
|
||||||
|
defineJob("foo", "true", "DESCRIPTION=bar");
|
||||||
|
auto es = eventSource("/jobs/foo");
|
||||||
|
ioContext->waitScope.poll();
|
||||||
|
ASSERT_EQ(1, es->messages().size());
|
||||||
|
auto json = es->messages().front().GetObject();
|
||||||
|
ASSERT_TRUE(json.HasMember("data"));
|
||||||
|
auto data = json["data"].GetObject();
|
||||||
|
ASSERT_TRUE(data.HasMember("description"));
|
||||||
|
EXPECT_STREQ("bar", data["description"].GetString());
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,10 +18,22 @@
|
|||||||
///
|
///
|
||||||
#include <kj/async-unix.h>
|
#include <kj/async-unix.h>
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include <kj/debug.h>
|
||||||
|
|
||||||
// gtest main supplied in order to call captureChildExit
|
#include "laminar-fixture.h"
|
||||||
|
#include "leader.h"
|
||||||
|
|
||||||
|
// gtest main supplied in order to call captureChildExit and handle process leader
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
|
if(argv[0][0] == '{')
|
||||||
|
return leader_main();
|
||||||
|
|
||||||
|
// TODO: consider handling this differently
|
||||||
|
auto ioContext = kj::setupAsyncIo();
|
||||||
|
LaminarFixture::ioContext = &ioContext;
|
||||||
|
|
||||||
kj::UnixEventPort::captureChildExit();
|
kj::UnixEventPort::captureChildExit();
|
||||||
|
//kj::_::Debug::setLogLevel(kj::_::Debug::Severity::INFO);
|
||||||
|
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
return RUN_ALL_TESTS();
|
return RUN_ALL_TESTS();
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
///
|
|
||||||
/// Copyright 2018 Oliver Giles
|
|
||||||
///
|
|
||||||
/// This file is part of Laminar
|
|
||||||
///
|
|
||||||
/// Laminar is free software: you can redistribute it and/or modify
|
|
||||||
/// it under the terms of the GNU General Public License as published by
|
|
||||||
/// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
/// (at your option) any later version.
|
|
||||||
///
|
|
||||||
/// Laminar is distributed in the hope that it will be useful,
|
|
||||||
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
/// GNU General Public License for more details.
|
|
||||||
///
|
|
||||||
/// You should have received a copy of the GNU General Public License
|
|
||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
///
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
#include "run.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "node.h"
|
|
||||||
#include "conf.h"
|
|
||||||
#include "tempdir.h"
|
|
||||||
|
|
||||||
class RunTest : public testing::Test {
|
|
||||||
protected:
|
|
||||||
RunTest() :
|
|
||||||
testing::Test(),
|
|
||||||
node(std::make_shared<Node>()),
|
|
||||||
tmp(),
|
|
||||||
run("foo", ParamMap{}, tmp.path.clone())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
~RunTest() noexcept {}
|
|
||||||
|
|
||||||
void wait() {
|
|
||||||
int state = -1;
|
|
||||||
waitpid(run.current_pid.orDefault(0), &state, 0);
|
|
||||||
run.reaped(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
void runAll() {
|
|
||||||
while(!run.step())
|
|
||||||
wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string readAllOutput() {
|
|
||||||
std::string res;
|
|
||||||
char tmp[64];
|
|
||||||
for(ssize_t n = read(run.output_fd, tmp, 64); n > 0; n = read(run.output_fd, tmp, 64))
|
|
||||||
res += std::string(tmp, n);
|
|
||||||
// strip the first "[laminar] executing.. line
|
|
||||||
return strchr(res.c_str(), '\n') + 1;
|
|
||||||
}
|
|
||||||
StringMap parseFromString(std::string content) {
|
|
||||||
char tmp[16] = "/tmp/lt.XXXXXX";
|
|
||||||
int fd = mkstemp(tmp);
|
|
||||||
write(fd, content.data(), content.size());
|
|
||||||
close(fd);
|
|
||||||
StringMap map = parseConfFile(tmp);
|
|
||||||
unlink(tmp);
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<Node> node;
|
|
||||||
TempDir tmp;
|
|
||||||
class Run run;
|
|
||||||
|
|
||||||
void setRunLink(const char * path) {
|
|
||||||
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "jobs", run.name + ".run"},
|
|
||||||
kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT | kj::WriteMode::EXECUTABLE)) {
|
|
||||||
(f->get())->writeAll(std::string("#!/bin/sh\nexec ") + path + "\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TEST_F(RunTest, WorkingDirectory) {
|
|
||||||
setRunLink("pwd");
|
|
||||||
run.configure(1, node, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
std::string cwd{tmp.path.append(kj::Path{"run","foo","1"}).toString(true).cStr()};
|
|
||||||
EXPECT_EQ(cwd + "\n", readAllOutput());
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, SuccessStatus) {
|
|
||||||
setRunLink("true");
|
|
||||||
run.configure(1, node, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
EXPECT_EQ(RunState::SUCCESS, run.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, FailedStatus) {
|
|
||||||
setRunLink("false");
|
|
||||||
run.configure(1, node, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
EXPECT_EQ(RunState::FAILED, run.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, Environment) {
|
|
||||||
setRunLink("env");
|
|
||||||
run.configure(1234, node, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
|
|
||||||
std::string ws{tmp.path.append(kj::Path{"run","foo","workspace"}).toString(true).cStr()};
|
|
||||||
std::string archive{tmp.path.append(kj::Path{"archive","foo","1234"}).toString(true).cStr()};
|
|
||||||
|
|
||||||
StringMap map = parseFromString(readAllOutput());
|
|
||||||
EXPECT_EQ("1234", map["RUN"]);
|
|
||||||
EXPECT_EQ("foo", map["JOB"]);
|
|
||||||
EXPECT_EQ("success", map["RESULT"]);
|
|
||||||
EXPECT_EQ("unknown", map["LAST_RESULT"]);
|
|
||||||
EXPECT_EQ(ws, map["WORKSPACE"]);
|
|
||||||
EXPECT_EQ(archive, map["ARCHIVE"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, ParamsToEnv) {
|
|
||||||
setRunLink("env");
|
|
||||||
run.params["foo"] = "bar";
|
|
||||||
run.configure(1, node, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
StringMap map = parseFromString(readAllOutput());
|
|
||||||
EXPECT_EQ("bar", map["foo"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, Abort) {
|
|
||||||
setRunLink("yes");
|
|
||||||
run.configure(1, node, *tmp.fs);
|
|
||||||
run.step();
|
|
||||||
usleep(200); // TODO fix
|
|
||||||
run.abort(false);
|
|
||||||
wait();
|
|
||||||
EXPECT_EQ(RunState::ABORTED, run.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user