Did you know that you can extend the spin
CLI with custom plugins that integrate seamlessly and feel like functionality provided by Fermyon itself?
No? No problem!
In this article, we will explore how I build the check-for-update
plugin - a simple Spin plugin that allows you to check if your local installation of the spin
CLI is outdated. On top of building the actual Spin Plugin, we’ll also go through the process of testing and distributing the check-for-update
plugin. Finally we’ll wrap up this article by adding the custom Spin Plugin to the Spin Up Hub, to ensure users can discover it.
- The Architectural Blueprint
- The
get-latest-spin-cli-version
backend - Important conventions when building Spin Plugins
- The
check-for-update
Plugin - The
check-for-update
plugin manifest - Testing the
check-for-update
plugin - Publishing a Spin Plugin manifest to
fermyon/spin-plugins
- Adding a Spin Plugin to the Spin Up Hub
- Conclusion
The Architectural Blueprint
Before we investigate how the plugin itself has been built, we will take a look at the overall architectural blueprint to lay out the different components of the check-for-update
plugin:
- The get-latest-spin-cli-version is a Spin application (exposing an HTTP trigger) that is deployed to Fermyon Cloud. It examines the
fermyon/spin
repository over on GitHub (using a GitHub PAT) and looks for the latest stable version number of thespin
CLI - The Spin CLI represents the local installation of the
spin
CLI fermyon/spin-plugins
is a GitHub repository maintained and governed by Fermyon, where manifests for all available plugins are located. Here we will publish the manifest for ourcheck-for-update
plugin by creating a corresponding PR.- The
check-for-update
is the individualspin
plugin. It calls the Spin application (get-latest-spin-cli-version
), which gives the user instructions on upgrading the localspin
installation if a new version of the CLI is available.
The get-latest-spin-cli-version
backend
Although not every Spin plugin will require a backend component, I thought it would be a good practice to separate the authenticated communication with GitHub‘s API into a small serverless application running in the cloud. That said, what could be a better fit than building a Spin application and publishing it to Fermyon Cloud 😃.
The app itself is unimpressive. It exposes a simple HTTP GET
endpoint, returning the latest stable version of the spin
CLI as text/plain
.A GitHub Personal Access Token (PAT) is read from the corresponding environment variable at runtime. If the PAT is absent, the endpoint will simply return an HTTP status code 500
.
The entire code for the Spin application is published on GitHub at https://github.com/ThorstenHans/get-latest-spin-cli-version.
When browsing through the repository, also look at the GitHub Action. It shows how easy Continuous Deployment (CD) can be achieved with Fermyon Cloud.
Important conventions when building Spin Plugins
Before we dive into how the check-for-Update
plugin has been created, we should talk about some crucial conventions we, as a community, should respect when building plugins for Spin.
- Every Spin plugin must have a name (
$name
) - Spin plugins created and maintained by Fermyon are prefixed with
spin-
- Names of 3rd party plugins (such as the
check-for-update
plugin) may not start withspin-
. - A Spin plugin is a binary which is called
$name
(and respectively$name.exe
for Windows) - Spin plugins must be packaged and distributed as
.tar.gz
file - Besides the executable, every plugin package (the
.tar.gz
file) should also contain a license file called$name.license
- Every Spin plugin must provide a manifest called
$name.json
(which represents the latest version) to ensure older plugin versions remain accessible. Plugin authors must keep older manifests following the[email protected]
naming scheme) - As part of the plugin manifest, authors must specify which versions of Spin are compatible with the particular plugin. This is done by defining the
spinCompatibility
property in the manifest. You can use several logical operators to express the compatibility level of your Spin plugin. See this explanation for more details.
The check-for-update
Plugin
Let’s talk about how the check-for-update
plugin is written. Although I decided to write the plugin in Rust, it’s not mandatory to use Rust. You can use any programming language you want as long you can compile your code into a binary executable.
I picked Rust because it’s easy to cross-compile Rust code for various operating systems and processor architectures. Spin itself supports many platforms, and I wanted my check-for-update
plugin to be available to all users of Fermyon Spin.
Spin Plugin invocation
Understanding how spin
CLI invokes plugins and passes data and contextual information is important. Again, let’s have a quick look at a simple diagram to illustrate a plugin invocation:
Here, the user invoked spin check-for-update
from their terminal. The spin
CLI is instantiated and starts processing the command line arguments provided (here, check-for-update
). Because check-for-update
is not a built-in command, Spin will check if a plugin with a matching name is installed.
If the user has installed the check-for-update
plugin, spin
starts with gathering contextual information (more on contextual information in a minute) and launches the check-for-update
executable as a child process.
Contextual information passed to Spin Plugins
To share contextual information from the spin
CLI with a particular Spin plugin, Spin provides all necessary information as environment variables for the child process. For example, you can get the installed spin
CLI version by reading the SPIN_VERSION
environment variable from within your Spin Plugin.
A list of all variables provided by spin
can be found in the official “Spin Plugin authoring guide” at https://developer.fermyon.com/spin/plugin-authoring or consider looking at the actual code.
The check-for-update
implementation
The implementation of the check-for-update
plugin is straightforward. However, I could not imagine publishing this article without having at least some code in it 😁:
#![warn(rust_2018_idioms)]
use anyhow::Result;
use spinners::{Spinner, Spinners};
const SPIN_INSTALL_INSTRUCTIONS: &str = "https://developer.fermyon.com/spin/install";
const GET_LATEST_SPIN_CLI_URL: &str= "https://get-latest-spin-cli-version-wuvznxqk.fermyon.app/version";
const SPIN_CLI_VERSION_ENV: &str = "SPIN_VERSION";
fn main() {
let mut spinner = Spinner::new(Spinners::Dots12, "Checking for latest spin CLI version...".into());
let Ok(latest) = get_latest_spin_cli_version() else {
println!("Failed to get latest version of spin-cli");
return;
};
let Ok(installed) = get_installed_spin_cli_version() else {
println!("Failed to get installed version of spin-cli");
return;
};
spinner.stop_with_newline();
println!();
if latest == installed {
println!("Your spin CLI is up to date (version {}) ✅", installed);
} else {
println!("Installed spin CLI version: {}", installed);
println!("Latest spin CLI version: {}", latest);
println!();
println!("See instructions for updating your spin CLI installation at {}", SPIN_INSTALL_INSTRUCTIONS);
}
}
fn get_installed_spin_cli_version() -> Result<String> {
let current = std::env::var(SPIN_CLI_VERSION_ENV)?;
Ok(current)
}
fn get_latest_spin_cli_version() -> Result<String> {
let client = reqwest::blocking::Client::new();
let response = client.get(GET_LATEST_SPIN_CLI_URL).send()?;
let latest_version = response.text()?;
Ok(latest_version)
}
The code contains more plumbing for constructing the spinner and generating proper responses that users will see in their terminal than business logic. See the get_installed_spin_cli_version
function, which reads the installed version from the environment variable mentioned before (SPIN_VERSION
). The get_latest_spin_cli_version
calls into the Spin application using reqwest
to determine the latest stable release of spin
CLI.
Finally, in main
, the code checks whether both versions are equal and print the corresponding message to stdout
.
Spin plugin compilation and packaging
I use GitHub Actions to cross-compile the source code of the check-for-update
plugin for all major platforms and architectures. Those binaries are published as artifacts and together with the check-for-update.license
file, compressed into a platform- and architecture-specific tar.gz
archive.
Did you know that the
tar
executable is available on Windows? TBH, I did not know that up until taking care of packaging thecheck-for-update
plugin. Having access to thetar
executable on all platforms made creating the packages super trivial and prevented me from trying to tame the 7Zip CLI (7z
).
Finally, the GitHub Action creates a release on GitHub and publishes all tar.gz
archives along with the checksums.txt
file, which contains all the checksums.
The check-for-update
plugin manifest
The last but most important component of a Spin plugin is the manifest. It’s a simple JSON document that contains essential metadata about the plugin and tells the Spin CLI where it could find our archives for the desired platform/architecture.
Don’t worry! You don’t have to craft the entire manifest from scratch. Consider cloning the fermyon/spin-plugins
repository to your machine. It contains a simple shell script that you can use to generate the manifest for your plugin.
The following snippet shows the manifest for version 0.0.1
of the check-for-update
plugin (check-for-update.json
), so you can see what an actual plugin manifest looks like:
{
"name": "check-for-update",
"description": "Command to verify if you're using latest spin CLI or not.",
"homepage": "https://github.com/ThorstenHans/spin-plugin-check-for-update",
"version": "0.0.1",
"spinCompatibility": ">=0.4",
"license": "Apache-2.0",
"packages": [
{
"os": "linux",
"arch": "aarch64",
"url": "https://github.com/ThorstenHans/spin-plugin-check-for-update/releases/download/v0.0.1/check-for-update-v0.0.1-linux-aarch64.tar.gz",
"sha256": "28b283edfdf7534c7651fae05c880367d37eb1a1d27d35422bc25d108f86ef37"
},
{
"os": "linux",
"arch": "amd64",
"url": "https://github.com/ThorstenHans/spin-plugin-check-for-update/releases/download/v0.0.1/check-for-update-v0.0.1-linux-amd64.tar.gz",
"sha256": "1c9106547ee7290aff5289a7e3e7062e9c073492b92155c357c378f83714b196"
},
{
"os": "macos",
"arch": "aarch64",
"url": "https://github.com/ThorstenHans/spin-plugin-check-for-update/releases/download/v0.0.1/check-for-update-v0.0.1-macos-aarch64.tar.gz",
"sha256": "919fc5e33f193747317ef5c3f24a7e0e309b57e8f25f9df77ae214727679b778"
},
{
"os": "macos",
"arch": "amd64",
"url": "https://github.com/ThorstenHans/spin-plugin-check-for-update/releases/download/v0.0.1/check-for-update-v0.0.1-macos-amd64.tar.gz",
"sha256": "d44d7f84769b93bb2a3e1c0cae56cb37be30ea261ed6c8a2b98df2643f96f872"
},
{
"os": "windows",
"arch": "amd64",
"url": "https://github.com/ThorstenHans/spin-plugin-check-for-update/releases/download/v0.0.1/check-for-update-v0.0.1-windows-amd64.tar.gz",
"sha256": "d581eeaaa2dd00fa5ac6fc9ee517dd059a033471fd6a8ad3219085c246eab602"
}
]
}
Validating Spin Plugin manifests
To validate the manifest of a check-for-update
plugin, we can use the ajv
. If you haven’t installed ajv
on your machine yet, install it using npm
, yarn
, or any other Node.JS package manager currently trending.
# Install ajv globally
npm install ajv-cli -g
Having ajv
installed on your machine, you can validate the plugin manifest as shown in the following snippet:
# Validate the check-for-update manifest
# Get the latest schema version from fermyon/spin-plugins
export schema_version=$(curl https://raw.githubusercontent.com/fermyon/spin-plugins/main/json-schema/version.txt)
# download the latest schema
wget https://raw.githubusercontent.com/fermyon/spin-plugins/main/json-schema/spin-plugin-manifest-schema-$schema_version.json
# validate local manifest (check-for-update.json) against latest schema version
ajv -s spin-plugin-manifest-schema-$schema_version.json \
-d check-for-update.json \
--spec=draft2019
# check-for-update.json valid
Testing the check-for-update
plugin
Now that we’ve everything in place and validated the plugin’s manifest, we can do a test installation. To do so, we can use the spin plugin install -u
command and point it to the raw URL of our manifest (Event when the PR is not yet merged). Alternatively, you can use spin plugin install -f
and point to a local manifest file. That said, you can install the check-for-update
plugin for testing purposes using the following command:
# Install check-for-update for testing purposes
spin plugin install -u https://raw.githubusercontent.com/fermyon/spin-plugins/main/manifests/check-for-update/check-for-update.json
# Are you sure you want to install plugin 'check-for-update' with license Apache-2.0 from https://github.com/ThorstenHans/spin-plugin-check-for-update/releases/download/v0.0.1/check-for-update-v0.0.1-macos-aarch64.tar.gz?
# yes
# Plugin 'check-for-update' was installed successfully!
# Description:
# Command to verify if you're using the latest spin CLI or not.
# Homepage:
# https://github.com/ThorstenHans/spin-plugin-check-for-update
# List all installed plugins
spin plugin list
# check-for-update 0.0.1 [installed]
# cloud 0.1.0
# cloud 0.1.1
# cloud 0.1.2 [installed]
# js2wasm 0.1.0
# js2wasm 0.2.0
# js2wasm 0.3.0
# js2wasm 0.4.0
# js2wasm 0.5.0
# js2wasm 0.5.1 [installed]
# py2wasm 0.1.0
# py2wasm 0.1.1
# py2wasm 0.2.0
# py2wasm 0.3.0 [installed]
Finally, we can invoke spin check-for-update
and do an end-to-end test:
# Invoke spin check-for-update
spin check-for-update
# ⠍⠉ Checking for latest spin CLI version...
# Your spin CLI is up to date (version 1.4.1) ✅
Publishing a Spin Plugin manifest to fermyon/spin-plugins
Having all components created for the plugin, we can finally publish our plugin manifest. To do so, you must fork the fermyon/spin-plugins
repository on GitHub and add your manifest to the manifests
directory.
Finally, you create a PR to get your manifest merged into fermyon/spin-plugins
and wait for a maintainer to merge the PR into the main
branch.
Adding a Spin Plugin to the Spin Up Hub
Now that we’ve published the check-for-update
plugin, we should ensure users could find the plugin. The Spin Up Hub is a central place to discover everything related to Spin.
To get the check-for-update
plugin listed on the Spin Up Hub, we can follow the Spin Up Hub Contribution Guid.
Basically, we must submit essential metadata of our Spin plugin to the content/api/hub
folder in the fermyon/developer
repository. The actual metadata for the check-for-update
plugin looks like this:
title = "Check for Update"
template = "render_hub_content_body"
date = "2023-08-24T08:42:56Z"
content-type = "text/plain"
tags = ["cli", "tooling"]
[extra]
author = "ThorstenHans"
type = "hub_document"
category = "Plugin"
language = "Rust"
created_at = "2023-08-23T15:20:00Z"
last_updated = "2023-08-23T15:20:00Z"
spin_version = ">=0.4"
summary = "A plugin to check if your local spin CLI installation is up-to-date"
url = "https://github.com/ThorstenHans/spin-plugin-check-for-update"
keywords = "plugins, tooling, cli, updates"
Once the PR with our plugin metadata is merged into the main
branch of fermyon/developer
, we can finally discover the check-for-update
plugin on the Spin Up Hub
Conclusion
To encapsulate, the journey through creating the check-for-update
plugin for the spin
CLI underscores the potential of custom plugin development. By extending the capabilities of the spin
CLI through plugins, users can seamlessly integrate additional features, mirroring core functionalities. This article’s focus on architectural design, coding specifics, and integration steps provides a comprehensive guide for creating Spin plugins.
The ability to choose programming languages for plugin creation, exemplified here with Rust, highlights the adaptability of Spin and the WebAssembly ecosystem, catering to a diverse set of developers and use cases.
The check-for-update
plugin’s journey from conceptualization to deployment underscores the iterative process of development, encompassing testing, validation, and distribution. Its presence on the Spin Up Hub showcases the collaborative and accessible nature of plugin sharing.