How to Automatically Issue Badges for Instruqt Labs
Raphaël Pinson
Posted on October 17, 2024
In the first blog post, we talked about making labs fun and enjoyable by adding elements of gamification. Issuing badges is a fantastic way to motivate learners, giving them a sense of accomplishment for the skills they've gained, and Isovalent issues hundreds of them every month for the Cilium labs!
Issuing Credentials
Obviously, issuing badges can be done manually, but this is not scalable or ideal for creating a seamless experience. So, let's automate it!
Credly is a widely recognized provider of digital badges, so we will be using this solution to issue badges whenever a user finishes an Instruqt lab.
We'll be using Instruqt webhooks, coupled with the Credly API, to automatically issue badges when labs are completed.
And thanks to Isovalent's open-sourced Go libraries for both Instruqt and Credly APIs, you will find this automation process smooth and straightforward.
instruqt-go is a Go client library for interacting with the Instruqt platform. It provides a simple and convenient way to programmatically access Instruqt's APIs, manage content, retrieve user data and track information.
Features
Manage Instruqt Teams and Challenges: Retrieve team information, challenges, and user progress.
Installation
To install the instruqt-go library, run:
go get github.com/isovalent/instruqt-go
Example Usage
package main
import (
"github.com/isovalent/instruqt-go/instruqt""cloud.google.com/go/logging"
)
funcmain() {
// Initialize the Instruqt clientclient:=instruqt.NewClient("your-api-token", "your-team-slug")
// Get all trackstracks, err:=client.GetTracks()
// Add context to callsctx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
clientWithTimeout:=client.WithContext(ctx)
userInfo, err:=clientWithTimeout.GetUserInfo("user-id")
// Attach a loggerlogClient, err:=logging
credly-go is a Go client library for interacting with the Credly platform. It provides a simple and convenient way to programmatically access Credly's APIs and handle badges and templates.
Features
Badge Management: Issue, retrieve, and manage badges using the Credly API.
Installation
To install the credly-go library, run:
go get github.com/isovalent/credly-go
Example Usage
package main
import (
"github.com/isovalent/credly-go/credly"
)
funcmain() {
// Initialize the Credly clientclient:=credly.NewClient("your-api-token", "your-credly-org")
// Get all badges for user joe@example.combadges, err:=client.GetBadges("joe@example.com")
}
Contributing
We welcome contributions! Please follow these steps to contribute:
Fork the repository.
Create a new branch with your feature or bug fix.
Make your changes and add tests.
Submit a pull request with a detailed description of your changes.
In this post, we'll take you step by step through the process:
Setting up the environment and harnessing Google Cloud Functions.
Initializing imports, constants, and setting up secret environment variables.
Implementing the webhook and explaining each step.
Setting up the webhook in Instruqt and adding signature verification to secure it.
Testing locally using Docker and Docker Compose.
Deploying the webhook and required secrets to Google Cloud Platform.
Wrapping up with some final considerations.
Let's dive in!
Pre-requisites
As for the first blog post, you will need an Instruqt account (with an API key) and a Google Cloud project.
In addition, you will also need a Credly account with an API key this time.
Setting Up the Environment
First, create a directory for your function and initialize the Go environment.
mkdir instruqt-webhook
cd instruqt-webhook
go mod init example.com/labs
Just as in the first post, we create a cmd directory so we can build and test the function locally:
mkdir cmd
Create a main.go file in that directory, with the following content:
packagemainimport("log""os"// Blank-import the function package so the init() runs// Adapt if you replaced example.com earlier_"example.com/labs""github.com/GoogleCloudPlatform/functions-framework-go/funcframework")funcmain(){// Use PORT environment variable, or default to 8080.port:="8080"ifenvPort:=os.Getenv("PORT");envPort!=""{port=envPort}iferr:=funcframework.Start(port);err!=nil{log.Fatalf("funcframework.Start: %v\n",err)}}
Back to the instruqt-webhook directory, create a file named webhook.go to contain the function logic. This file will serve as the webhook handler for incoming events from Instruqt.
Setting Up the Basics
In webhook.go, begin by adding the necessary imports, constants, and initializing the function:
packagelabsimport("fmt""net/http""os""strings""github.com/GoogleCloudPlatform/functions-framework-go/functions""github.com/isovalent/instruqt-go/instruqt""github.com/isovalent/credly-go/credly")funcinit(){functions.HTTP("InstruqtWebhookCatch",instruqtWebhookCatch)}const(instruqtTeam="yourInstruqtTeam"// Replace with your own team namecredlyOrg="yourCredlyOrg"// Replace with your own credly organization ID)
Implementing the Webhook Receiver
Now, let's write the instruqtWebhookCatch function to receive the event.
We will take advantage of the methods provided by the Isovalent instruqt-go library to manage the Instruqt webhook:
This function works as a proxy between the HTTP connection handler provided by the Google Cloud Functions framework and the instruqt.HandleWebhook method provided by Isovalent's library to manage the Svix webhook.
It allows us to set up a webhook manager by passing the webhook's secret. We will see later where to find the value for the webhook secret.
The instruqt.HandleWebhook method will automatically:
Verify the webhook signature using svix.
Parse the incoming event payload.
Check if the event is valid.
Retrieve the information into an instruqt.WebhookEvent structure.
Step 4: The processWebhook() Function
Next, we need to implement the processWebhook function, where our logic will be placed.
This function will receive 3 parameters:
the HTTP connection handlers (http.ResponseWriter and *http.Request) inherited from the GCP Function handler;
the instruqt.Webhook structure parsed by instruqt.HandleWebhook and passed down to us.
Here's the complete implementation:
funcprocessWebhook(whttp.ResponseWriter,r*http.Request,webhookinstruqt.WebhookEvent)(errerror){// Return early if the event type is not track.completedifwebhook.Type!="track.completed"{w.WriteHeader(http.StatusNoContent)return}// Setup the Instruqt clientinstruqtToken:=os.Getenv("INSTRUQT_TOKEN")ifinstruqtToken==""{w.WriteHeader(http.StatusInternalServerError)return}instruqtClient:=instruqt.NewClient(instruqtToken,instruqtTeam)// Setup the Credly clientcredlyToken:=os.Getenv("CREDLY_TOKEN")ifcredlyToken==""{w.WriteHeader(http.StatusInternalServerError)return}credlyClient:=credly.NewClient(credlyToken,credlyOrg)// Get user info from Instruqtuser,err:=instruqtClient.GetUserInfo(webhook.UserId)iferr!=nil{fmt.Printf("Failed to get user info: %v",err)w.WriteHeader(http.StatusInternalServerError)return}// Get track details to extract badge template ID from tagstrack,err:=instruqtClient.GetTrackById(webhook.TrackId)iferr!=nil{fmt.Printf("Failed to get track info: %v",err)w.WriteHeader(http.StatusInternalServerError)return}// Extract badge template ID from track tagsvartemplateIdstringfor_,tag:=rangetrack.TrackTags{// Use strings.Split to parse the tag and extract the badge template IDparts:=strings.Split(tag.Value,":")iflen(parts)==2&&parts[0]=="badge"{templateId=parts[1]break}}iftemplateId==""{fmt.Printf("No badge template ID found for track %s",webhook.TrackId)w.WriteHeader(http.StatusBadRequest)return}// Issue badge through Credly_,badgeErr:=credlyClient.IssueBadge(templateId,user.Email,user.FirstName,user.LastName)// Check if the badge has already been issuedifbadgeErr!=nil{ifstrings.Contains(badgeErr.Error(),credly.ErrBadgeAlreadyIssued){fmt.Printf("Badge already issued for %s",user.Email)w.WriteHeader(http.StatusConflict)return}fmt.Printf("Failed to issue badge: %v",badgeErr)w.WriteHeader(http.StatusInternalServerError)return}w.WriteHeader(http.StatusOK)return}
This function does the following:
Check if the event is of type track.completed, exit otherwise.
Instantiate Instruqt and Credly clients using environment variables for the tokens.
Retrieve user information from the Instruqt API. This requires to ensure that Instruqt has that information. See the first blog post to find how to do that with a proxy.
Get track information from Instruqt. We will use set a badge: special tag on the track to store the Credly badge ID to issue.
Parse track tags to find the badge template ID.
Issue the badge using the Credly library.
Setting Up the Webhook on Instruqt
To enable Instruqt to call your webhook, navigate to the Instruqt UI, go to Settings -> Webhooks, and click "Add Endpoint" to set up a new webhook that points to your Google Cloud Function URL.
Select track.completed in the list of events to fire up this endpoint.
Since we'll be hosting the function on Google Cloud Functions, the URL will be in the form https://<zone>-<project>.cloudfunctions.net/<name>. For example, if your function is called instruqt-webhook and is deployed in the labs GCP project in the europe-west1 zone, then the URL will be https://europe-west1-labs.cloudfunctions.net/instruqt-webhook. If in doubt, put a fake URL and you can modify it later.
Create "Create", then locate the "Signing secret" field to the right side of the panel and copy its value.
Export it in your terminal as the INSTRUQT_WEBHOOK_SECRET value:
This will upload and build your project, and return the URL to access the function.
If necessary, update the URL in your Instruqt webhook configuration.
Testing
Now for the moment of truth: testing!
Create a badge on Credly. Publish it and copy its template ID.
Add a tag to the Instruqt track you want to associate the badge with. Name the tag badge:<template_ID>, replacing template_ID with the ID you just copied.
Publish the track.
Take the track and complete it!
You should get the badge in your email!
Further Considerations
User Information: Make sure you read the first blog post to understand how to send user information to Instruqt.
Make it worth it!: Getting badges is fun, but it's better if users deserve them. Consider adding exam steps to your tracks to make earning the badges a challenge.
Rate Limiting and Retries: Consider rate limiting incoming webhook requests to prevent abuse and adding retry logic to handle temporary failures when interacting with Credly.
Manage more Events: This webhook manager only manages track.completed events. You can extend it to do a lot more things with all the events provided by Instruqt! I typically like to capture lots of events to send them to Slack for better visibility.
Logs: Consider adding more logging (for example using the GCP logging library) to the code.