Setup scripts can be written in any language that is supported by the VM. Here are the rules about setup scripts:
- It must return a nonzero exit code
- In must be named
setup-<user>
where<user>
is the user the lab will run as. For example if the user islabuser
it will besetup-labuser
- Use a shebang at the top like this:
#!/bin/env python3
or#!/usr/bin/node
or#!/bin/bash
- Make sure it's executable (use
chmod +x setup-labserver
) - Should be in the same folder as your
assignment.md
These scripts can be used at the Challenge level or at the track level
Put them in a folder called track_scripts
at the root of your track folder (same level as the config.yml
). The file goes in that folder and follows all the rules above.
Keep in mind that the track-level scripts represent a fixed cost at the beginning of your track and will make iteration harder. If at all possible, prefer adding certain actions to the VM image itself.
- The script will run as root so you may not have access to things that are in the user's environment, and changes you make will apply to
root
unless you specifically run them as your user. - To run a command as your use, use
su -c '<COMMAND>' <YOUR_USER_NAME>
- Example:
su -c 'git checkout -f mm23-metrics-challenge-2-start' labuser
- Example:
set-workdir <SOME DIR>
will set the starting directory for all tabs. It works by modifying the$HOME
variable so it could have other effects. You can change this in every setup script.- It's helpful to add
echo
statements and tag them. That way when your script is running an you are tailing the logs usinginstruqt track logs
you can easily pick out the stage of the setup script. Example:echo "[AUTHOR] start docker compose"
- It's a good idea to use git branches or commits to set up any code state. You want a clean, knowable state on start of every challenge, so using a branch in combination with the
-f
flag forgit checkout
is recommended.- Example:
su -c 'git checkout -f mm23-metrics-challenge-2-start' labuser
- Example:
- Kill any processes from a possible previous challenge at the beginning to ensure a clean slate, or restart them if the command is idempotent (like
docker compose up -d
)kill -9 $(lsof -ti tcp:4000) || true
for example kills anything that might be up and listening on tcp port 4000 but always returns successful.- Avoid overgeneral commands like
killall node
since there may be other things you need that are using node to run (like visual studio code) docker compose up -d
is always safe and makes sure some container arrangement is up and running
- It must return a nonzero exit code
- echoing
FAIL: <YOUR MESSAGE>
and exiting with a nonzero exit code will cause instruqt to show the user a failure with your message - In must be named
check-<user>
where<user>
is the user the lab will run as. For example if the user islabuser
it will besetup-labuser
- Use a shebang at the top like this:
#!/bin/env python3
or#!/usr/bin/node
or#!/bin/bash
- Make sure it's executable (use
chmod +x setup-labserver
) - Should be in the same folder as your
assignment.md
The biggest learning on check scripts was to not make them too complex or specific. Since the instruqt environment is free, users can get themselves into all sorts of correct arrangements that your script could deem as incorrect. Instead:
- Check broad things like:
- Did they create this file?
- Does a request give a response with the correct substring match?
if [ -f "/home/labuser/microservices-march/messenger/load-balancer/Dockerfile" ]
then
echo "load-balancer/Dockerfile exists"
else
fail-message "/home/labuser/microservices-march/messenger/load-balancer/Dockerfile should exist"
fi
# Check that the load balancer is up
if [[ "$(curl -X GET http://localhost:8085/health)" == "OK" ]]; then
echo "messenger through lb is healthy"
else
fail-message "curl -X GET http://localhost:8085/health should return a 200 response with text OK"
fi
If you feel like the check script isn't able to test what you want, consider adding a quiz after the challenge.
Write your failure message so that it provides the user with a clue towards solving the problem. See the above failure messages
I found that by
- Stopping everything and restarting everything in the
setup
scripts - Forcefully checking out a certain state from a git branch or commit
- Running any other necessary commands to get the system in a certain state
I the setup
script, solve
scripts were not really necessary. This is highly dependent on the nature of your track though.
Instruqt is not well suited for large amounts of text. The eye easily gets lost. Recommend keeping text to a minimum.
Here are some strategies to break up text if you have a lot of it:
Adding ===
after a line turns it into a collapsible section. I was afraid to have "too many" of these initially, but later found that being very liberal with them increased readability.
Diagrams can help you eliminate text but they also help break things up
In instruqt, lower-level headings are not visually distinct enough. Recommend using only H1 headings #
You can write html with your Markdown and use inline css to make sections of your text visually distinct.
Here is an example of a piece of text that is called out with custom styling:
<div style="border: 1px solid black; border-radius: 5px; padding-left: 1em; padding-bottom: 1em; background-color: lightgray; color: black; border-radius: 5px;">
<h3 style="color: black; border-radius: 5px;">🎉 One of the microservices can produce OpenTelemetry traces</h3>
The traces are just going to the console for now, but the basic set up is complete and you did not have to touch the main application code.
</div>
Note that Markdown won't work inside html.
You can also use a "collapse" to hide nonessential information:
<details style="cursor: pointer;">
<summary>Why wait?</summary>
<div style="border: 1px solid black; padding: 0.5em;">
Node.js when auto-instrumented will produce a lot of filesystem access spans when it first starts up.
Waiting a short time after starting the application helps us see our main traces without distractions.
</div>
</details>
I think (asdf)[https://asdf-vm.com/] is the best way to set up and manage language runtimes and tooling. For example, if you need exact versions of python, nodejs, the github cli tool, and the azure CLI took, you can define a file called .tool-versions
in your $HOME
directory or in the folder you are working in that looks like this:
nodejs 19.3.0
python 3.11.1
azure-cli 2.46.0
github-cli 2.24.3
Then running asdf install
will just make sure all those are installed at the correct versions. However, asdf
is a very user specific tool. Here's what I had to do to use asdf
-managed things in a setup script:
export ASDF_DIR=/home/labuser/.asdf
export PATH=/home/labuser/.asdf/shims:/home/labuser/.asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:$PATH
export NPM_PATH=/home/labuser/.asdf/shims/npm
export NODE_PATH=/home/labuser/.asdf/shims/node
Then when using node or npm, do it like this:
su -c "$NPM_PATH install" labuser
The PATH
variable was set by running echo $PATH
while logged in to the instruqt track as the user.
Make REALLY sure the user knows what tab they should be working in. I used the following HTML snippet to force a tab change:
<div style="border: 1px solid black; border-radius: 5px; padding-left: 1em; padding-bottom: 1em; background-color: lightgray; color: black; border-radius: 5px;">
<h3 style="color: black; border-radius: 5px;">✅ Use Tab: <strong>messenger</strong></h3>
</div>
It looks like this:
You can define a tab for something that will come up later. It'll just load once it's up.
tabs:
- title: Jaeger
type: service
hostname: labserver
path: /
port: 16686
Try to order the tabs in the order they are initially referenced in the instructions. If there are multiple challenges, try to keep the order the same.
I didn't have time to build out a whole git-based workflow. But ideally it would work like this:
- Merge to
main
does aninstruqt track push --force
to the main track - Changes are done in PRs and autopush to a track that has the same name as the branch
- No one uses the UI under pain of death
In the absence of this process, or if you have a collaborator who can only use the UI:
- Communicate and have only one person working on the track at a time
- Once the UI person has done their work do the following:
- Commit and push what you have to git
- Create a new branch
git checkout -b track-remote
- From that branch, pull with force:
instruct track pull --force
- Diff the two:
git diff my-branch..track-remote
- If all looks well, you can pull with force from
my-branch
and commit it - If there are changes, apply them manually or commit them then merge
track-remote
intomy-branch
using your preferrence merge strategy
If using code-server
, throw this in your track-level setup script. This assumes your user is labuser
so you'll need to change that:
mkdir -p /home/labuser/code-server/User
cat > /home/labuser/code-server/User/settings.json <<-EOF
{
"workbench.colorTheme": "Default Dark+",
"workbench.startupEditor": "none",
"security.workspace.trust.enabled": false,
"security.workspace.trust.banner": "never",
"security.workspace.trust.startupPrompt": "never",
"security.workspace.trust.untrustedFiles": "open",
}
EOF
cat > /home/labuser/code-server/coder.json <<-EOF
{
"query": {
"folder": "/home/labuser/microservices-march"
},
"update": {
"checked": 1665349162360,
"version": "4.7.1"
}
}
EOF
- Do as much busywork up front. For example, if you are having the user build a docker image in the challenge, pull the base image in the track setup. Make sure you use specific versions instead of
latest
.- Example: (in setup script)
docker pull postgrest:15.2-alpine
- Example: (in setup script)
- As you create the track, for each challenge maintain a bare set of commands that will solve the challenge if you're not allowing skips. Have that ready in case frustrated users ran out of time at the last challenge and need to jump forward.
- Write up a set of troubleshooting questions for each challenge that you can anticipate and circulate it to others who might be on the hook to support your track