No Preview

Sorry, but you either have no stories or none are selected somehow.

If the problem persists, check the browser console, or the terminal you've run Storybook from.

Go Plugins

Build an example plugin

Octant ships with an example plugin.

Install the plugin using:

go run build.go install-test-plugin

Alternatively, build the go binary using go build then move the binary to the install path described below.

Installation

'go run build.go install-test-plugin' installs the plugin by creating a $HOME/.config/octant/plugin/ directory then building the binary to that location.

Run plugins from additional paths by setting paths to the OCTANT_PLUGIN_PATH environment variable when running Octant.

Octant will also respect XDG_CONFIG_HOME on Unix and LocalAppData on Windows for default plugin paths.

Uninstall

Plugins can be removed by deleting the plugin binary from ~/.config/octant/plugins. An example of deleting a plugin is shown below where octant-sample-plugin is the plugin that will be uninstalled:

rm ~/.config/octant/plugins/octant-sample-plugin

After deleting the plugin binary and restarting Octant, you should no longer see the plugin available as part of Octant.

Logs

Octant use go-plugin library, this library allows "Any plugins that use the log standard library will have log data automatically sent to the host process." by default with granularity level INFO, to set the log's granularity Octant provides a LoggerHelper that can be use this way.

var logger = service.NewLoggerHelper()
logger.Info("octant-sample-plugin is starting, with logger helper")

The default granularity level is INFO and can be change to DEBUG by adding the option -v or by setting the OCTANT_VERBOSE environment variable to true

Log levels

  • Info: Will be display by default, information about steady state operations.
  • Warn: Will be display by default, information about rare but handled events.
  • Error: Will be display by default and and will show the stack trace, information about unrecoverable events.
  • Debug: Not shown by default, information for programmer low level analysis

References

Writing Plugins

When you want to extend Octant to do something that is not part of the core functionality you will need to write a plugin. Writing an Octant plugin consists of three main parts: defining the capabilities, creating handlers, and registering and serving the plugin.

Capabilities

Using plugin.Capabilities you can define your desired list of capabilities using GVKs. Octant provides a set of well defined capabilities for plugins. These capabilities directly map to Octant renderers and allow your plugin to inject its own components in to the view.

When plugin.Metadata.IsModule to true plugins can provide content and navigation entries.

capabilities := &plugin.Capabilities{
    SupportsTab:           []schema.GroupVersionKind{podGVK},
    IsModule:              False,
}

The above defines a non-module plugin that will generate a new tab for Pod objects.

Handlers

Using service.HandlerFuncs you will assign handler functions for each of the capabilities for your plugin.

func handleTab(dashboardClient service.Dashboard, object runtime.Object) (*component.Tab, error) {
    // ...
}

handlers := service.HandlerFuncs{
    PrintTab: handleTab,
}

Handling Actions

In Octant you can create custom action handlers that you can trigger from button actions in the UI. There are also built-in actions which are triggered from internal Octant events, those are defined in octant/pkg/action/actions.go.

Here is an example of setting up your plugin to know when the current namespace has changed.

capabilities := &plugin.Capabilities{
        ActionNames:           []string{action.RequestSetNamespace},
    }
    // Set up the action handler.
    options := []service.PluginOption{
        service.WithActionHandler(handleAction),
    }

    func handleAction(request *service.ActionRequest) error {
        switch request.ActionName {
            case action.RequestSetNamespace:
                namespace, err := action.Payload.String("namespace")
                // err check, do work
        }
        return nil
    }

Besides having custom actions, there are pre-existing octant actions which are registered by octant internal modules and they can be leveraged to perform an action. For example: action.octant.dev/apply (https://github.com/vmware-tanzu/octant/blob/master/pkg/action/actions.go) can be used to apply a yaml configuration for a resource.

Example Payload:

{
    "namespace": "default",
    "update": "---\napiVersion: apps/v1 ..."
}

Register and Serve

Registering and serving your plugin is the final step to get your plugin communicating with Octant. This is also where you will pass in the name and description for the plugin.

p, err := service.Register("plugin-name", "a description", capabilities, handlers)
if err != nil {
    log.Fatal(err)
}

log.Printf("octant-sample-plugin is starting")
p.Serve()

Plugins with partial octant state

All plugin implementations are passed a request parameter which makes a partial octant state available to the plugins. request.ClientState contains accessors for the current namespace, filters and context name. The plugins can now respond to state updates by either responding to specific actions (eg. action.octant.dev/setNamespace) or they can reflect the current state using the request.ClientState.

More About Capabilities

Octant provides a well defined set of capabilities for plugins to implement. These include:

  • Print support: printing config, status, and items to the overview summary for an object.
  • Tab support: creating a new tab in the overview for an object.
  • Object status: adding object status to a given object.
  • Actions: defining custom actions that route to the plugin.

For plugins that as configured as modules the capabilities also include:

  • Navigation support; adding entries to the navigation section.
  • Content support; creating content to display on a given path.

Print

A PrintResponse consists of a Config, Status, and Items. The Content can be any of the various components found in reference.

func handlePrint(dashboardClient service.Dashboard, object runtime.Object) (*plugin.PrintResponse, error) {
    ...
    return plugin.PrintResponse{
        Config: []component.SummarySection{
            {Header: "from-plugin", Content: component.NewText("")},
        },
        Status: []component.SummarySection{
            {Header: "from-plugin", Content: component.NewText("")},
        },
        Items: []component.FlexLayoutItem{
            {
                Width: component.WidthFull,
                View:  component.NewText(""),
            },
        },
    }, nil

Tab

Adding a new tab via a plugin requires a new flexlayout then Tab component. The Name is used in the URL query param, and Contents defines the tab name within Octant.

func handleTab(dashboardClient service.Dashboard, object runtime.Object) (*component.Tab, error) {
    if object == nil {
        return nil, errors.New("object is nil")
    }

    layout := flexlayout.New()

    tab := component.Tab{
        Name:     "Plugin",
        Contents: *layout.ToComponent("Plugin Tab Name"),
    }

    return &tab, nil
}

Object Status

An ObjectStatusResponse has an ObjectStatus which contains a list of Properties, Details, and a Status (ok, warning, error). Details can be any of the various components found in reference.

A resource viewer shows the property labels and their respective components inside a table. Details can be seen in datagrid tables by clicking the icon.

func handleStatus(request *service.PrintRequest) (plugin.ObjectStatusResponse, error) {
    if request.Object == nil {
        return plugin.ObjectStatusResponse{}, errors.Errorf("object is nil")
    }

    key, err := store.KeyFromObject(request.Object)
    if err != nil {
        return plugin.ObjectStatusResponse{}, err
    }
    u, err := request.DashboardClient.Get(request.Context(), key)
    if err != nil {
        return plugin.ObjectStatusResponse{}, err
    }

    if u == nil {
        return plugin.ObjectStatusResponse{}, errors.New("object doesn't exist")
    }

    return plugin.ObjectStatusResponse{
        ObjectStatus: component.PodSummary{
            Status: component.NodeStatusError,
            Details: []component.Component{
                component.NewText("from plugin: " + string(u.GetUID())),
            },
            Properties: []component.Property{{
                Label: "ID (from plugin)",
                Value: component.NewText(string(u.GetUID())),
            }},
        },
    }, nil
}

Actions

Plugins configured as modules can supply navigation entries. These navigation entries will be displayed with the application's navigation.

var pluginName = "plugin-name"
var pluginPath = path.Join("content", pluginName)

func handleNavigation(dashboardClient service.Dashboard) (navigation.Navigation, error) {
    return navigation.Navigation{
        Title: "Module Plugin",
        Path:  path.Join(pluginPath, "/"),
        Children: []navigation.Navigation{
            {
                Title:    "Nested Once",
                Path:     path.Join(pluginPath, "nested-once"),
                IconName: "folder",
                Children: []navigation.Navigation{
                    {
                        Title:    "Nested Twice",
                        Path:     path.Join(pluginPath, "nested-once", "nested-twice"),
                        IconName: "folder",
                    },
                },
            },
        },
        IconName: "cloud",
    }, nil

}

Content

Plugins configured as modules can serve content. The content consists of Octant components wrapped in a ContentResponse. The function will receive the currently requested content path and can display content based on that path.

func handleContent(dashboardClient service.Dashboard, contentPath string) (component.ContentResponse, error) {
    return component.ContentResponse{
        Components: []component.Component{
            component.NewText(fmt.Sprintf("hello from plugin: path %s", contentPath)),
        },
    }, nil
}

Module Path

Currently Octant creates a non-configurable base path for your plugin that is derived from the name of the plugin.

/content/plugin-name

You can create nested paths that route to your module using that base path. Plugins should handle nested paths in the Content function and dispatch the responses accordingly.

Define Capability

Each plugin must have a defined name, description, and capability.

Plugins can provide a PrintResponse containing capabilities enabled by a provided GVK.

Config

A plugin with support for PrinterConfig appends a view component to the Configuration table of the supported GVK(s).

The header is added to the column on the left. Content is a component that is added to the right.

PrinterConfig

Certain GVK such as Deployments have a Configuration but not Status.

Status

A plugin with support for PrinterStatus appends a view component to the Status table of the supported GVK(s).

PrinterStatus

This pod has both a Configuration and Status.

Items

A plugin with support for PrinterItems allow adding a FlexLayoutItem consisting of a width and a view component.

Custom Runtime

Custom object store

For runtimes with custom object store implementations, Go plugins can pass arbitrary key/values when issuing calls to the object store. A plugin can set a grpc header with prefix x-octant- which will be consumed by the grpc server to set up random properties for the object store context. For example: To set a property named foo on the object store context, a plugin can set a grpc header named x-octant-foo.

func handlePrint(request *service.PrintRequest) (plugin.PrintResponse, error) {
  ...
  key, err := store.KeyFromObject(request.Object)
    if err != nil {
        return plugin.PrintResponse{}, err
    }
  ctx = metadata.AppendToOutgoingContext(request.Context(), "x-octant-foo", "bar")
  u, err := request.DashboardClient.Get(ctx, key)

Custom Icon

Custom Icon for plugins

It's possible to add a custom SVG for plugins.

First to do is define your SVG:

svg := "<svg width=\"10px\" height=\"10px\">" +
  "<g>" +
    "<ellipse fill=\"#000000\" stroke=\"#000000\" cx=\"5\" cy=\"5.5\" rx=\"3\" ry=\"3\"/>" +
  "</g>" +
"</svg>"

Then, where you are defining your navigation, add the SVG and set a custom name for it:

navigation.Navigation{
        Title: "Sample Plugin",
        Path:  request.GeneratePath(),
        Children: []navigation.Navigation{
            {
                Title:    "Nested Once",
                Path:     request.GeneratePath("nested-once"),
                IconName: "folder",
                Children: []navigation.Navigation{
                    {
                        Title:    "Nested Twice",
                        Path:     request.GeneratePath("nested-once", "nested-twice"),
                        IconName: "folder",
                    },
                },
            },
        },
        IconName:  "test-custom-svg",
        CustomSvg: svg,
    }, nil

Note: It's possible to have a naming collision with Clarity icons, it's recommended to use non-common names for the svg