1.0.0 • Published 3 months ago

@openshift-console/dynamic-plugin-sdk-internal v1.0.0

Weekly downloads
-
License
Apache-2.0
Repository
-
Last release
3 months ago

OpenShift Console Dynamic Plugins

Based on the concept of webpack module federation, dynamic plugins are loaded and interpreted from remote sources at runtime. The standard way to deliver and expose dynamic plugins to Console is through OLM operators.

Dynamic plugins are decoupled from the Console application, which means both plugins and Console can be released, installed and upgraded independently from each other. To ensure compatibility with Console and other plugins, each plugin must declare its dependencies using semantic version ranges.

Example project structure:

dynamic-demo-plugin/
├── src/
├── console-extensions.json
├── package.json
├── tsconfig.json
└── webpack.config.ts

Related Documentation

Extension Documentation - Detailed documentation of all available Console extension points.

API Documentation - Detailed documentation of React components, hooks and other APIs provided by Console to its dynamic plugins.

OpenShift Console Dynamic Plugins feature page - A high-level overview of dynamic plugins in relation to OLM operators and cluster administration.

Distributable SDK package overview

Package NameDescription
@openshift-console/dynamic-plugin-sdkProvides core APIs, types and utilities used by dynamic plugins at runtime.
@openshift-console/dynamic-plugin-sdk-webpackProvides webpack ConsoleRemotePlugin used to build all dynamic plugin assets.
@openshift-console/dynamic-plugin-sdk-internalInternal package exposing additional code.
@openshift-console/plugin-sharedProvides reusable components and utility functions to build OCP dynamic plugins. Compatible with multiple versions of OpenShift Console.

OpenShift Console Versions vs SDK Versions

Not all NPM packages are fully compatible with all versions of the Console. This table will help align compatible versions of distributable SDK packages to versions of the OpenShift Console.

Console VersionSDK PackageLast Package Version
4.15.x@openshift-console/dynamic-plugin-sdkLatest
@openshift-console/dynamic-plugin-sdk-webpackLatest
4.14.x@openshift-console/dynamic-plugin-sdk0.0.21
@openshift-console/dynamic-plugin-sdk-webpack0.0.11
4.13.x@openshift-console/dynamic-plugin-sdk0.0.19
@openshift-console/dynamic-plugin-sdk-webpack0.0.9
4.12.x@openshift-console/dynamic-plugin-sdk0.0.18
@openshift-console/dynamic-plugin-sdk-webpack0.0.9
4.11.x@openshift-console/dynamic-plugin-sdk0.0.12
@openshift-console/dynamic-plugin-sdk-webpack0.0.7
4.10.x Tech@openshift-console/dynamic-plugin-sdk0.0.3
@openshift-console/dynamic-plugin-sdk-webpack0.0.6
4.9.x Dev@openshift-console/dynamic-plugin-sdk0.0.0-alpha18

Notes:

  • Tech - Release 4.10 was Tech Preview for the SDK packages
  • Dev - Release 4.9 was Dev Preview for the SDK packages

Shared modules

Console is configured to share specific modules with its dynamic plugins.

The following shared modules are provided by Console, without plugins providing their own fallback:

  • @openshift-console/dynamic-plugin-sdk
  • @openshift-console/dynamic-plugin-sdk-internal
  • react
  • react-i18next
  • react-redux
  • react-router
  • react-router-dom
  • react-router-dom-v5-compat
  • redux
  • redux-thunk

For backwards compatibility, Console also provides the following PatternFly 4.x shared modules:

  • @patternfly/react-core
  • @patternfly/react-table
  • @patternfly/quickstarts

Newer versions of @openshift-console/dynamic-plugin-sdk-webpack package (1.0.0 and higher) include support for automatic detection and sharing of individual PatternFly 5.x dynamic modules.

Plugins using PatternFly 5.x dependencies should generally avoid non-index imports for any PatternFly packages, for example:

// Do _not_ do this:
import { MonitoringIcon } from '@patternfly/react-icons/dist/esm/icons/monitoring-icon';
// Instead, do this:
import { MonitoringIcon } from '@patternfly/react-icons';

Plugin metadata

Older versions of webpack ConsoleRemotePlugin assumed that the plugin metadata is specified via consolePlugin object within the package.json file, for example:

{
  "name": "dynamic-demo-plugin",
  "version": "0.0.0",
  // scripts, dependencies, devDependencies, ...
  "consolePlugin": {
    "name": "console-demo-plugin",
    "version": "0.0.0",
    "displayName": "Console Demo Plugin",
    "description": "Plasma reactors online. Initiating hyper drive.",
    "exposedModules": {
      "barUtils": "./utils/bar"
    },
    "dependencies": {
      "@console/pluginAPI": "~4.11.0"
    }
  }
}

Newer versions of webpack ConsoleRemotePlugin allow passing the plugin metadata directly as an object, for example:

new ConsoleRemotePlugin({
  pluginMetadata: { /* same metadata like above */ },
})

name serves as the plugin's unique identifier. Its value should be the same as metadata.name of the corresponding ConsolePlugin resource on the cluster. Therefore, it must be a valid DNS subdomain name.

version must be semver compliant version string.

Dynamic plugins can expose modules representing plugin code that can be referenced, loaded and executed at runtime. A separate webpack chunk is generated for each entry in the exposedModules object. Exposed modules are resolved relative to the plugin's webpack context option.

The @console/pluginAPI dependency is optional and refers to Console versions this dynamic plugin is compatible with. The dependencies object may also refer to other dynamic plugins that are required for this plugin to work correctly. For dependencies where the version string may include a semver pre-release identifier, adapt your semver range constraint (dependency value) to include the relevant pre-release prefix, e.g. use ~4.11.0-0.ci when targeting pre-release versions like 4.11.0-0.ci-1234.

Extensions contributed by the plugin

Older versions of webpack ConsoleRemotePlugin assumed that the list of extensions contributed by the plugin is specified via the console-extensions.json file, for example:

// This file is parsed as JSONC (JSON with Comments)
[
  {
    "type": "console.flag",
    "properties": {
      "handler": { "$codeRef": "barUtils.testHandler" }
    }
  },
  {
    "type": "console.flag/model",
    "properties": {
      "flag": "EXAMPLE",
      "model": {
        "group": "kubevirt.io",
        "version": "v1alpha3",
        "kind": "ExampleModel"
      }
    }
  }
]

Newer versions of webpack ConsoleRemotePlugin allow passing the extension list directly as an array of objects, for example:

new ConsoleRemotePlugin({
  extensions: [ /* same extensions like above */ ],
})

Each extension a single instance of extending the Console application's functionality. Extensions are declarative and expressed as plain static objects.

Extension type determines the kind of extension to perform, while any data and/or code necessary to interpret such extensions are declared through their properties.

Extensions may contain code references pointing to specific modules exposed by the plugin. For example:

  • { $codeRef: 'barUtils' } - refers to default export of barUtils module
  • { $codeRef: 'barUtils.testHandler' } - refers to testHandler export of barUtils module

When loading dynamic plugins, all encoded code references { $codeRef: string } are transformed into functions () => Promise<T> used to load the referenced objects on demand. Only the plugin's exposed modules (i.e. the keys of exposedModules object) may be used in code references.

Webpack config

Dynamic plugins must be built with webpack in order for their modules to seamlessly integrate with Console application at runtime. Use webpack version 5+ which includes native support for module federation.

All dynamic plugin assets are generated via webpack ConsoleRemotePlugin.

import { ConsoleRemotePlugin } from '@openshift-console/dynamic-plugin-sdk-webpack';
import { Configuration } from 'webpack';

const config: Configuration = {
  entry: {}, // Plugin container entry is generated by DynamicRemotePlugin
  plugins: [new ConsoleRemotePlugin()],
  // ... rest of webpack configuration
};

export default config;

Refer to ConsoleRemotePluginOptions type for details on supported Console plugin build options.

Generated assets

Building the above example plugin produces the following assets:

dynamic-demo-plugin/dist/
├── exposed-barUtils-chunk.js
├── plugin-entry.js
└── plugin-manifest.json

plugin-manifest.json is the dynamic plugin manifest. It contains both plugin metadata and extension declarations to be loaded and interpreted by Console at runtime. This is the first plugin asset loaded by Console.

plugin-entry.js is the webpack container entry chunk. It provides access to specific modules exposed by the plugin. It's loaded right after the plugin manifest.

exposed-barUtils-chunk.js is the generated webpack chunk for barUtils exposed module. It's loaded via the plugin entry chunk (plugin-entry.js) when needed.

Plugin development

Run Bridge locally and instruct it to proxy e.g. /api/plugins/console-demo-plugin requests directly to your local plugin asset server (web server hosting the plugin's generated assets):

# Note that the plugin's base URL should have a trailing slash
./bin/bridge -plugins console-demo-plugin=http://localhost:9001/

Your plugin should start loading automatically upon Console application startup. Inspect the value of window.SERVER_FLAGS.consolePlugins to see the list of plugins which Console loads upon its startup.

Plugin detection and management

Console operator detects available plugins through ConsolePlugin resources on the cluster. It also maintains a cluster-wide list of currently enabled plugins via spec.plugins field in its config (Console resource instance named cluster).

When the spec.plugins value in Console operator config changes, Console operator computes the actual list of plugins to load in Console as an intersection between all available plugins vs. plugins marked as enabled. Updating Console operator config triggers a new rollout of the Console (Bridge) deployment. Bridge reads the computed list of plugins upon its startup and injects this list into Console web page via SERVER_FLAGS object.

Disabling plugins in the browser

Console users can disable specific or all dynamic plugins that would normally get loaded upon Console startup via disable-plugins query parameter. The value of this parameter is either a comma separated list of plugin names (disable specific plugins) or an empty string (disable all plugins).

Runtime constraints and specifics

  • Loading multiple plugins with the same name (but with a different version) is not allowed.
  • Console will override certain modules to ensure a single version of React etc. is loaded and used by the application.
  • Enabling a plugin makes all of its extensions available for consumption. Individual extensions cannot be enabled or disabled separately.

Publishing SDK packages

To see the latest published version of the given package:

yarn info <package-name> dist-tags --json | jq .data.latest

Before publishing, it's recommended to log into your npm user account:

npm login

Build all distributable SDK packages into dist directory:

yarn build

Finally, publish relevant packages to npm registry:

yarn publish dist/<pkg> --no-git-tag-version --new-version <version>

If the given package doesn't exist in npm registry, add --access public to yarn publish command.

Future Deprecations in Shared Plugin Dependencies

Console provides certain packages as shared modules to all of its dynamic plugins. Some of these shared modules may be removed in the future. Plugin authors will need to manually add these items to their webpack configs or choose other options.

The list of shared modules planned for deprecation:

  • react-helmet