Skip to content

fix: repair the generated converge scripts so services actually load - #97

Merged
tas50 merged 1 commit into
mainfrom
fix/linux-converge-script
Aug 30, 2026
Merged

tas50 merged 1 commit into
mainfrom
fix/linux-converge-script

Conversation

@tas50

@tas50 tas50 commented Aug 30, 2026 •

Copy link
Copy Markdown
Member

I ran the shell this provisioner generates through bash -n, then actually converged it against a real machine (see #99), and found four things broken in ways our string-equality specs could never catch — the expectations were written from the same typos as the code.

A package with a run file instead of a hooks/run never loaded

This is the bad one. run_command decided whether to hab svc load like this:

if [ -f $(sudo hab pkg path core/redis)/hooks/run ]

The supervisor accepts a run hook in either of two places: hooks/run, written from a hook template, or a run file in the package root, which is what pkg_svc_run in a plan produces. We only ever checked the first.

core/redis — the example in our own README — is built the second way:

$ tar -tf core-redis-4.0.14-20240106065001-x86_64-linux.hart | grep -E '/(run|hooks/)'
hab/pkgs/core/redis/4.0.14/20240106065001/run

So the converge installed it, skipped the entire load branch, and exited 0. Test Kitchen reported success, hab svc status said No services loaded, and nothing in the output suggested anything had gone wrong. Both platform paths now resolve the package path once and check both locations; the Windows path also checks the .ps1 form of each.

The service load loop never timed out

timer=0
until sudo -E hab svc status | grep core/redis
  do
    if [$timer -gt 300]; then exit 1; fi
    sleep 1
    $timer++
  done

[$timer -gt 300] has no spaces, so bash parses it as a command named [0. $timer++ expands to 0++, another command name. Both are "command not found" every iteration:

$ bash -c 'timer=0; if [$timer -gt 300]; then echo yes; fi; $timer++'
bash: line 1: [0: command not found
bash: line 1: 0++: command not found

service_load_timeout did nothing, the counter never moved, and a package that failed to start hung the converge until someone killed Test Kitchen. It is now a real test expression and real shell arithmetic, and it says what it gave up waiting for before exiting non-zero.

The hab user and group were created in the wrong order

id -u hab tested for the user and then ran groupadd; id -g hab tested for a group in a way that only works once the user exists, then ran useradd -g hab hab. And a stray id -u hab >/dev/null 2>&1 || sudo -E useradd hab at the top of the script created the user with its own private group before either block ran, so in practice both were no-ops and the hab group was never created. The group is now created first with getent group hab, and the user is added to it.

An unset depot_url wrote a blank Builder URL into the unit file

The systemd unit always got both environment lines, so leaving depot_url alone produced Environment="HAB_BLDR_URL=". Empty is not the same as unset — it overrides the hab CLI's own default from cli.toml with a URL it cannot parse. HAB_BLDR_URL and HAB_LICENSE are now written only when configured.

And install_latest_artifact failed with a useless message

With no matching .hart, Dir.glob(...).max_by returns nil and we called File.basename(nil), so the user got no implicit conversion of nil into String. There was a # TODO: throw error and bail if there's no artifacts sitting right above it. It now raises a UserError naming the glob and the directory it searched.

Tests

Each fix has a spec. I also added a small bash -n helper and pointed it at install_command, init_command, prepare_command and run_command, so the next typo in one of those heredocs fails the build instead of being copied into an expectation.

$ bundle exec cookstyle --chefstyle
5 files inspected, no offenses detected

$ bundle exec rake test
110 examples, 0 failures
Line Coverage: 92.04% (208 / 226)
Branch Coverage: 79.84% (99 / 124)

(81.82% / 68.85% before.)

Three things in the scripts this provisioner generates were wrong in ways
that a string-equality spec written from the same typo could never catch.

The `hab svc load` wait loop never timed out. The guard was spelled
`[$timer -gt 300]` with no spaces, which bash parses as a command named
`[0`, and the counter was incremented with `$timer++`, which bash parses
as a command named `0++`. Both were "command not found" on every
iteration, so `service_load_timeout` did nothing and a service that
failed to start hung the converge until Test Kitchen was killed. It is
now `[ "$timer" -ge N ]` and `timer=$((timer + 1))`, and it prints what
it gave up waiting for before exiting non-zero.

The hab user and group were created in the wrong order with the wrong
checks. `id -u hab` tested for the user and then ran `groupadd`, while
`id -g hab` tested for a group in a way that only works once the user
already exists and then ran `useradd -g hab`. A stray
`id -u hab || useradd hab` at the top of the script created the user
without the hab group first, which made both blocks no-ops. The group is
now created first with `getent group hab`, then the user is added to it.

An unset `depot_url` was written into the systemd unit as
`Environment="HAB_BLDR_URL="`. An empty value is not the same as an
unset one -- it overrides the hab CLI's own default with a URL it cannot
parse. The `HAB_BLDR_URL` and `HAB_LICENSE` lines are now emitted only
when those options are actually configured.

Separately, `install_latest_artifact` with no matching .hart in the
results directory fell through to `File.basename(nil)` and raised "no
implicit conversion of nil into String", which says nothing about what
went wrong. It now raises a UserError naming the glob it looked for and
the directory it looked in.

Specs cover each of these, and the generated Linux scripts are now
handed to `bash -n` so a future typo in a heredoc fails the build rather
than being copied into an expectation.

Signed-off-by: Tim Smith <tim@mondoo.com>
@tas50
tas50 merged commit 31cf166 into main Aug 30, 2026
8 checks passed
@tas50
tas50 deleted the fix/linux-converge-script branch August 30, 2026 02:24
@tas50 tas50 changed the title fix: repair the broken shell in the generated Linux converge scripts fix: repair the generated converge scripts so services actually load Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant