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

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:

Architectural blueprint of the check-for-update plugin

Architectural blueprint 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 the spin 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 our check-for-update plugin by creating a corresponding PR.
  • The check-for-update is the individual spin plugin. It calls the Spin application (get-latest-spin-cli-version), which gives the user instructions on upgrading the local spin 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.

  1. Every Spin plugin must have a name ($name)
  2. Spin plugins created and maintained by Fermyon are prefixed with spin-
  3. Names of 3rd party plugins (such as the check-for-update plugin) may not start with spin-.
  4. A Spin plugin is a binary which is called $name (and respectively $name.exe for Windows)
  5. Spin plugins must be packaged and distributed as .tar.gz file
  6. Besides the executable, every plugin package (the .tar.gz file) should also contain a license file called $name.license
  7. 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)
  8. 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:

Plugin invocation in Spin

Plugin invocation in Spin

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 the check-for-update plugin. Having access to the tar 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.

Submitting the plugin manifest to fermyon/spin-plugins

Submitting the plugin manifest to fermyon/spin-plugins

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

The check-for-update plugin listed on the Spin Up Hub

The check-for-update plugin listed 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.