Frontend Plugin Development
Overview of Frontend plugins
Frontend plugins provide a way to change the behavior of Deck, Spinnaker’s UI
service. You can add configuration and validation for new stages provided by
Orca plugins, override existing components with your own implementation, or add
new Kubernetes kind
definitions for custom resources in your environment. Spinnaker loads plugins at runtime through Gate.
You can write plugins in any JavaScript-compatible language, but the development tooling is designed for JavaScript and TypeScript.
The following are examples of Deck features that you can override:
- ApplicationIcon , replace the icon used to represent applications in Deck.
- ServerGroupHeader , replace how pod status is reported in Deck.
- SpinnakerHeader , replace the top navigation header.
The following projects demonstrate adding new stages to Spinnaker:
- nomadPlugin , adding a Nomad provider to Spinnaker.
- pf4jStagePlugin , adding a sample random wait stage to Spinnaker.
Before you begin
Make sure you have the following tools installed:
- Gradle .
- NPM and NPX .
- Yarn .
You also need access to a Spinnaker instance >= 1.20.6
running in a Kubernetes cluster.
This guide focuses on a frontend plugin, but if you need help configuring a backend plugin, see the Plugin Project Configuration and Backend Extension Points guides.
Plugin scaffolding
You can write a Frontend plugin as standalone or as part of a broader set of functionality. In both cases, you need the Gradle toolchain to build and release your plugin. The
Deck
project has the @spinnaker/pluginsdk
NPM package
that you can use to generate your new frontend plugin’s project structure and configuration files.
Create the plugin project structure
For a new plugin, create a top-level directory:
mkdir my-plugin
Then, from the root of the my-plugin
directory, run the @spinnaker/pluginsdk
scaffold
script to create your frontend plugin:
npx -p @spinnaker/pluginsdk scaffold
The script asks a few questions about your plugin. Enter the requested information and wait for the command to complete:
npx: installed 117 in 6.229s
Enter the short name for your plugin (default: myplugin): my-plugin
Directory to scaffold into (default: my-plugin-deck):
Deck plugin scaffolded into my-plugin-deck
Installing dependencies using 'yarn' and 'npx check-peer-dependencies --install' ...
The script creates the following project structure:
my-plugin
├── my-plugin-deck
├── .eslintrc.js
├── .prettierrc.js
├── package.json
├── my-plugin-deck.gradle
├── tsconfig.json
├── rollup.config.js
├── yarn.lock
├── node_modules
└── src
├── index.ts
├── WidgetizeStage.less
└── WidgetizeStage.tsx
You may see this scaffold
command fail to resolve dependencies initially. If
this is the case, you can run the check-peerdependencies
and check-plugin
commands from the my-plugin-deck
directory until these errors are resolved.
cd my-plugin-deck
npx check-peer-dependencies --install
npx check-plugin --fix
You should now be able to successfully build the plugin:
yarn && yarn build
Gradle Configuration
In order to build and release your plugin, you need to ensure that you have
your Gradle environment configured correctly. Follow the advice in the
Plugin Project Configuration
document up to the UI-extension build.gradle
section.
The development tooling uses this top-level Gradle file to create a simple plugin metadata file. See the Build and release section to learn how to configure the rest of this file to create release distributions and proper plugin manifest metadata.
Development workflow
The plugin resources created by the scaffold
command include functionality to
run your plugin locally, provided that you can connect to an external Spinnaker
instance. The most common way to do this is to port-forward
your running Deck
instance to your local machine using kubectl
.
kubectl port-forward service/spin-deck 90001:9000
This forwards Deck to your local machine on port 9001. Then you can run your plugin locally on port 9000 using yarn develop
.
DEV_PROXY_HOST=http://localhost:9001 yarn develop
You should now be able to navigate to http://localhost:9000
and access
the Deck UI. Create a new pipeline and search for the Widgetize
stage to verify that Spinnaker loaded the frontend plugin correctly.
You can also verify your plugin loaded correctly by navigating to
http://localhost:9000/plugin-manifest.json
. You should see at least two
plugins listed, the one with your pluginId
as well as a plugindev.livereload
plugin. The development tooling uses plugindev.livereload
to reload Deck on
each code change in your plugin directory.
Fix CORS errors when using a remote deck instance
During development, you may run into
CORS
-related
errors if your Deck service runs on an address other than
localhost
. If you can, consider
modifying your Gate
instance
to allow for CORS requests during development.
Add new stages
The plugin SDK enables the addition of new stages and kinds
within
Spinnaker. These additions are often accompanied by changes to Orca and
related services. If you haven’t started work on these backend components,
see the
Backend Extension Points
guide.
The scaffold
command creates the most up-to-date schema example, which you
should use as your template when you develop your plugin. If you are writing
a plugin that targets an older version of Spinnaker, you may need to refer to
existing stages for the Spinnaker release to ensure you’re following the correct schema.
In general, a Deck stage has these elements:
- A
StageConfig
React component.- This component wraps
FormikStageComponent
and refers to the ReactStageForm
component.
- This component wraps
- A
StageForm
React component.- This component provides a set of
FormikFormField
components that encapsulate the configuration options for your stage.
- This component provides a set of
- A (optional)
validate
function.- The
validate
function validates a stage configuration. When added to a stage object,validate
is called whenever input changes in the config form.
- The
- A Stage object that encapsulates the previous components.
- This object is passed to the plugin config object and registered when Deck starts.
You can define more than one stage per plugin. When you’re ready to register
your plugin and test in Deck, add all your stages to the stage
field in your
plugin index.ts
file like so:
import { IDeckPlugin } from '@spinnaker/core';
import { widgetizeStage } from './WidgetizeStage';
export const plugin: IDeckPlugin = {
stages: [widgetizeStage],
};
Override existing components
You can also override existing components within Deck, so long as they have
an Overridable
annotation or are registered as an overridable component. In
this use case, you define your replacement component and then leverage the
initialize
method of your plugin object to override the component when it is
loaded.
For example, if you want to remove the ability to modify Application configuration in Deck, you define a component like this:
import React from 'react';
export const InvisibleConfig = () => {
return <h1>No config here!</h1>;
}
In order to override the component, you need to know
the Application configuration registration key. You
can find its definition in the GitHub Deck project,
ApplicationConfig.tsx
.
There, you find the Overridable
annotation is applicationConfigView
. Then,
in your index.ts
file where you define your plugin object, you override that
component in the initialize
method. For example:
import { IDeckPlugin, overrideRegistrationQueue } from '@spinnaker/core';
import { InvisibleConfig } from './InvisibleConfig';
export const plugin: IDeckPlugin = {
initialize: () => {
overrideRegistrationQueue.register(InvisibleConfig, 'applicationConfigView')
}
};
Once installed, you see the Application configuration page now displays the h1
header.
Build and release
Building and releasing requires adding a distribution repository, building and committing changes for the plugin, and hosting the plugin.
Create plugin packages
Creating a plugin distribution starts from the root of your plugin project.
The releaseBundle
task is responsible for creating zip distributions of all
plugin code and storing it in the build/distributions
directory. Run the releaseBundle
task to get started:
./gradlew releaseBundle
If you find the zip archive doesn’t contain all your plugin code, make sure
that you’ve included each plugin sub-directory in the parent build. You can
do that by editing the settings.gradle
file and ensuring that each project
is included
. For example:
// file: my-plugin/settings.gradle
// other configuration ...
include "my-plugin-deck"
// other configuration ...
Create distribution files
Spinnaker needs a repositories.json
file and a plugins.json
file to install a plugin. repositories.json
represents a set of pointers to plugin files, and plugins.json
lists all versions of a particular plugin. See the
Plugin Users Guide
for more information.
The format of the repositories.json
file looks like this:
[
{
"id": "myPluginRepo",
"url": "https://raw.githubusercontent.com/my-org/my-plugin-repo/master/plugins.json"
}
]
The id
key uniquely identifies your plugin repository. The url
points to
where your keep your plugins.json
file. This URL can be any URI so long as Spinnaker can reach it.
The releaseBundle
command that generates the plugin packages also generates required metadata. You can find this metadata in the build/distributions/plugin-info.json
file. You must provide the value for the url
key. The value is the location of your plugin zip file. You can store the file in any location that Spinnaker can access.
Here is an example plugins.json
file’s contents:
[
{
"id": "My.Plugin.Id",
"description": "This a sample plugin.",
"provider": "https://github.com/my-organization",
"releases": [
{
"version": "0.1.0",
"date": "2021-03-09T17:30:00.948341Z",
"requires": "deck>=0.0.0",
"sha512sum": "a91cb7d412a25ca5e1b2d72e14ab499986d5773ae8016721fbefd0adf430e33b75e8e61bac92244bbbe4811f118724ec6e2bb568fdd8181a9e11327a96b45da9",
"url": "https://github.com/my-organization/my-plugin/blob/master/my-plugin-v0.1.0.zip?raw=true"
}
]
}
]
You can store the repositories.json
and plugins.json
in any location that Spinnaker can reach. Most developers store these files in a repository separate from the plugin code.
Create an installation README for your users
In addition to explaining what your plugin does, you should include a YAML snippet showing your plugin’s configuration. For example:
profiles:
spinnaker:
spinnaker:
extensibility:
plugins:
# The plugin id you defined in your build.gradle
My.Plugin.Id:
enabled: true
# This must be a SemVer-compatible string
# (i.e. do not include a `v` in front of the version string)
version: "0.1.0"
gate:
spinnaker:
extensibility:
# This snippet is necessary so that Gate can serve your plugin code to Deck
deck-proxy:
enabled: true
plugins:
My.Plugin.Id:
enabled: true
repositories:
myPluginRepo:
enabled: true
url: https://raw.githubusercontent.com/my-organization/my-plugin-repo/master/repositories.json
Next steps
Note: If you use the open source Spinnaker Operator to install and manage Spinnaker, see that product’s documentation for how to deploy a plugin.