Instruqt Labs (beta)
  • Instruqt
  • Getting started
    • Setting up Version Control
    • Install Instruqt CLI
    • Creating your first lab
    • Configuration basics
    • Exploring the lab configuration
    • Adding your first chapter
    • Configuring sandboxes
    • Adding quizzes
    • Adding tasks and gating content
    • Finishing up
  • Documentation
    • Writing Lab Content
      • Project Structure
      • Markdown and Components
    • Integrations
      • Version Control
    • Lab reference
      • Content
        • Lab
        • Page
        • Activities
          • Task
          • Quiz
            • Multiple Choice
            • Single Choice
            • Text Answer
            • Numeric Answer
        • Layout
        • Tabs
          • Terminal
          • Service
          • Editor
          • External Website
          • Note
      • Sandbox
        • Containers
          • Container
          • Sidecar Container
        • Kubernetes
          • Cluster
          • Config
          • Helm
        • Nomad
          • Cluster
          • Job
        • Networking
          • Network
          • Ingress
        • Cloud Accounts
          • AWS
          • Azure
          • Google Cloud
        • Terraform
        • Template
        • Exec
        • Copy
        • Certificates
          • Root
          • Leaf
        • Random
          • Number
          • ID
          • UUID
          • Password
          • Creature
      • Functions
    • Tools
      • Instruqt CLI
    • Glossary
Powered by GitBook
On this page
  • Lifecycle hooks
  • Scripts
  • Embed the task in a page
  • Adding a terminal
  • Add the new page and terminal to the Lab UI
  • Preview your changes
Edit on GitHub
Export as PDF
  1. Getting started

Adding tasks and gating content

In the previous section, you learned about activities and added a quiz to the lab. In this section, we will explore another type of activity called tasks.

Tasks are a way to verify the hands-on skills and knowledge of a user by having them perform actions within the sandbox environment, and then validating that the desired outcome of the actions is reached.

Tasks are made up of one or more conditions that need to be met before the task is considered completed. A condition tells the user what needs to be completed, and specifies lifecycle scripts that are executed for the condition.

For example, lets say we have a task where we want the user to write a certain value e.g. world to a file in a certain location e.g. /tmp/hello.

This task can be split up into a few steps or conditions:

  • the user needs to create a file at the given location

  • the user needs to write the given value to the created file

When writing the description of a condition, try to describe the end result you want rather than the steps to get there. This is because there are often multiple valid ways to get the same result. For the previous two conditions, you could write them as:

  • The file /tmp/hello should exist

  • The file should contain the text world

Lets start by creating a file tasks.hcl to hold the task resource and name it "helloworld".

Add two conditions to it and set their description field.

resource "task" "helloworld" {
  condition "file_exists" {
    description = "The file `/tmp/hello` should exist"
  }

  condition "file_contains" {
    description = "The file should contain the text `world`"
  }
}

Lifecycle hooks

Now that you have defined the task and its conditions, the next step is to specify what scripts need to be run in order to validate each of the conditions. This is done by specifying lifecycle hooks on the conditions.

The lifecycle of a task and its conditions is split up into setup, check, solve, cleanup.

The setup lifecycle scripts are run when the task and its conditions enter the unlocked state. This lifecycle hook can be used to do any setup that might be needed in order to prepare the sandbox for the user actions of this specific task and uts conditions.

The check lifecycle scripts are run when the user clicks the "Check" button of an embedded task component in the instructions. This lifecycle hook is used to validate if the desired outcome has been reached for each condition.

The solve lifecycle scripts are run when the user clocks the "Solve for me" button of an embedded task component in the instructions. This lifecycle hook is used to solve the task for the user and mimic the steps the user would have to do to reach the desired outcome. If the actions of the user would not impact any following tasks, then the solve lifecycle can be omitted to just skip the task altogether.

The cleanup lifecycle scripts are run when the task has successfully been completed or skipped. This lifecycle hook can be used to do any cleanup that might be needed in order to prepare the sandbox for upcoming user actions.

Each condition can have as many lifecycle hooks as needed, depending on the situation. For instance, you could split the scripts that you want to execute for a condition logically in order to keep them simple and reusable, or if you wanted to execute multiple scripts, but each on a different target or with different settings.

Lets add a check block to each of the conditions of the task, to execute scripts when the user clicks the "Check" button.

resource "task" "helloworld" {
  condition "file_exists" {
    description = "The file `/tmp/hello` should exist"

    check {

    }
  }

  condition "file_contains" {
    description = "The file should contain the text `world`"

    check {

    }
  }
}

Scripts

When specifying a lifecycle hook, it needs to know which script to run for that hook. You do this by providing a path to the script file in the script field.

Lets create a script file scripts/file_exists.sh and add the following contents:

#!/bin/sh
if [ ! -f /usr/share/nginx/html/index.html ]; then
  exit 1
fi

Then tell the check of the first condition "file_exists" to use the created file by setting the script field to scripts/file_exists.sh.

resource "task" "helloworld" {
  condition "file_exists" {
    description = "The file `/tmp/hello` should exist"

    check {
      script = "scripts/file_exists.sh"
    }
  }

  ...
}

In order to give clear feedback to the user when the check script fails, you can specify a failure_message. This message will de displayed to the user on the task component for the given condition.

Add a failure message to the check of the "file_exists" condition and set the message to "The file /usr/share/nginx/html/index.html does not exist".

resource "task" "helloworld" {
  condition "file_exists" {
    description = "The file `/tmp/hello` should exist"

    check {
      script = "scripts/file_exists.sh"
      failure_message = "The file `/tmp/hello` does not exist"
    }
  }

  ...
}

Create another script file scripts/contents_match.sh with the following contents:

#!/bin/sh
CONTENTS=$(cat /usr/share/nginx/html/index.html)
if [ ! "$CONTENTS" = "hello world" ]; then
  exit 1
fi

Add it to the check of the second condition file_contains and set the failure_message to "The file /usr/share/nginx/html/index.html does not contain the text hello world".

resource "task" "helloworld" {
  ...

  condition "file_contains" {
    description = "The file should contain the text `world`"

    check {
      script = "scripts/contents_match.sh"
      failure_message = "The file `/tmp/hello` does not contain the text `world`"
    }
  }
}

In order for the scripts to be executed in the correct place and with the correct settings, they need to be configured.

This configuration can be specified on the task with a config block, and any of the fields can be overridden in the condition and lifecycle hook config blocks. This can be particularly useful when only a subset of lifecycle scripts requires non-default settings e.g. to set a longer timeout duration for a script that runs longer than all the others.

In the case of the task you created, all scripts can be executed the same way. This means that the config can be specified once on the task resource, and then used by each of the conditions and each of their lifecycle hooks.

The only configuration setting that needs to be specified is where to run the script, by setting the target field of the config block to a reference of the "webserver" container (resource.container.webserver) you created earlier.

resource "task" "helloworld" {
  config {
    target = resource.container.webserver
  }

  condition "file_exists" {
    ...
  }

  condition "file_contains" {
    ...
  }
}

Embed the task in a page

Now that you have defined the task and configured all its conditions and lifecycle hooks, lets use it in the instructions.

In pages.hcl add a new page resource and name it "task".

Add the resource.task.helloworld task to the activities map, like before when adding a quiz in the "Adding quizzes" section.

resource "page" "task" {
  activities = {
    "edit_html" = resource.task.helloworld
  }
}

Then create a markdown file instructions/task.md, with the following contents.

# Tasks

In the left pane there is a terminal session on the ubuntu container.
We want the user to perform actions in the terminal and then validate that the actions had the desired outcome.

Each task can have multiple conditions that need to be met before the task is considered completed. 

To embed the task component in the page, add the instruqt-task component and pass it the id that you mapped the resource.task.helloworld task to e.g. "edit_html".

Inside of the instruqt-task html tags, you can specify instructions for the user that they need to follow to complete the task.

Together with the descriptions of the conditions, this is what instructs the user to perform the desired actions.

Change the contents of instructions/task.md so it contains an instruqt-task with the basic actions to complete.

# Tasks

In the left pane there is a terminal session on the ubuntu container.
We want the user to perform actions in the terminal and then validate that the actions had the desired outcome.

Each task can have multiple conditions that need to be met before the task is considered completed. 

<instruqt-task id="edit_html">
  Change the contents of `/usr/share/nginx/html/index.html` to `hello world`.
</instruqt-task>

Then add the created file to the page resource.

resource "page" "task" {
  file = "instructions/task.md"

  activities = {
    "edit_html" = resource.task.helloworld
  }
}

Adding a terminal

You now have actions for the user to perform, but no way for that user to actually perform them.

To fix this, lets add a terminal resource named "shell" to the lab, so the user can interact with the "webserver" container using a terminal tab. On tabs.hcl, add:

resource "terminal" "shell" {

}

Just like the service resource you added in the "Configuring Sandboxes" section, a terminal resource also has a target field that specifies which resource to provide terminal access to.

In this case, we want to provide access to the "webserver" container. So, set the target field to a reference e.g. resource.container.webserver.

resource "terminal" "shell" {
  target = resource.container.webserver
}

This is enough to have a working terminal on a container resource, but to make it nicer to interact with for the user you can add additional configuration.

Set the default shell that the user will be presented to /bin/bash, to allow for things like tab completion.

And to already put the user in the correct location to edit the html files of the webserver, lets set the working_directory to /usr/share/nginx/html.

resource "terminal" "shell" {
  target = resource.container.webserver

  shell = "/bin/bash"
  working_directory = "/usr/share/nginx/html"
}

Add the new page and terminal to the Lab UI

In order for the user to interact with the "shell" terminal you just created, we need to connect a tab to it.

In the main.hcl file, add a tab named "terminal" to the "two_columns" layout.

Assign the tab to the "right" panel, and set the target to a reference to the terminal resource e.g. resource.terminal.shell.

resource "lab" "main" {
  ...

  layout "two_columns" {
    reference = resource.layout.two_panels
    default = true

    tab "webserver" {
      title = "Webserver"
      panel = "left"
      target = resource.service.webserver
    }

    tab "terminal" {
      title = "Terminal"
      panel = "left"
      target = resource.terminal.shell
    }

    instructions {
      panel = "right"
    }
  }
}

Finally, to show the page containing the task, add a page block named "task" to the "introduction" chapter block.

Set the reference to the page resource you created earlier resource.page.task.

resource "lab" "main" {
  ...
  content {
    ...
    chapter "introduction" {
      ...
      page "task" {
        reference = resource.page.task
      }
    }
  }
}

Preview your changes

Like in the previous section, you can validate your changes using instruqt lab validate, and then git add, git commit, and git push the changes to GitHub. You can then go to the "Labs" section, verify the latest commit on GitHub matches the status of your lab, and start the lab.

Your lab should now have an additional page in the "introduction" chapter.

When you are going through the lab, you should notice that you are now not able to navigate past the page with the quiz as the next page is still locked. This is because the quiz needs to be successfully completed in order for the next page, and any of the activities embedded in that page, to be unlocked.

To get to the page with the task, solve the quiz and the "Next Page" button should become active.

The third page should display the task component and show the conditions you have defined. Try interacting with the task to see how it behaves when you submit the wrong answer, a partially wrong answer, and when you complete it.

Once you are done exploring your lab, click the "Stop" button at the top right of the Lab UI to shut down your lab environment and go back to the "Labs" list.

PreviousAdding quizzesNextFinishing up

Last updated 5 days ago

Tasks can be completed or skipped to proceed.
Adding a terminal to the lab.