NAME

API::Docker::API::Plugins - Docker Engine Plugins API

VERSION

version 0.004

SYNOPSIS

my $docker = API::Docker->new;

# List installed plugins
my $plugins = $docker->plugins->list;

# Install: look at what the plugin demands, then grant exactly that
my $privileges = $docker->plugins->privileges('vieux/sshfs:latest');
$docker->plugins->install('vieux/sshfs:latest',
    privileges => $privileges,
);
$docker->plugins->enable('vieux/sshfs:latest');

# Inspect
my $plugin = $docker->plugins->inspect('vieux/sshfs:latest');
say $plugin->name, $plugin->enabled ? ' (enabled)' : ' (disabled)';

# Configure, upgrade, disable, remove
$docker->plugins->configure('vieux/sshfs:latest', ['DEBUG=1']);
$docker->plugins->upgrade('vieux/sshfs:latest', privileges => $privileges);
$docker->plugins->disable('vieux/sshfs:latest');
$docker->plugins->remove('vieux/sshfs:latest');

DESCRIPTION

This module provides access to the Docker managed-plugin endpoints (/plugins).

Accessed via $docker->plugins, or through "using" in API::Docker::Role::Using for a run of calls that needs its own transport bound: $docker->plugins->using(read_timeout => 5).

Installing is two calls, and the engine enforces it

POST /plugins/pull takes the list of privileges the plugin demands in its request body, and the daemon compares that list against the one it computes from the plugin's own config. They must match exactly -- same length, same names, same values -- or the install fails with incorrect privileges. A plugin runs with the host access it asked for, so the round trip exists to make somebody look at that access before granting it.

"privileges" is the first call, "install" the second:

my $privileges = $docker->plugins->privileges('vieux/sshfs:latest');
# inspect $privileges here -- it is an ArrayRef of
#   { Name => 'network', Description => '...', Value => ['host'] }
$docker->plugins->install('vieux/sshfs:latest', privileges => $privileges);

install requires privileges and croaks without it, which is stricter than the engine: the daemon's own body parser treats a missing body as an empty privilege list rather than an error, so a blind install of a plugin that happens to demand nothing would quietly succeed and one that demands network: host would fail with an error naming neither. Passing accept_privileges => 1 makes install perform the first call itself and hand the answer straight back -- a blanket grant, spelled out at the call site so it is greppable.

The same applies to "upgrade", which takes the same body.

Not available on Podman

Measured against the rootless Podman socket (5.4.2, API 1.41): none of the /plugins endpoints exist there. GET /v1.41/plugins answers 404 Not Found with {"cause":"","message":"Path /v1.41/plugins is not supported","response":0} (the 1.41 there is this client's negotiated API version, echoed back from the request path -- it moves with negotiation, not a fixed string in the daemon's error text), and every other path in this family -- /plugins/privileges, /plugins/pull, /plugins/{name}/json, /plugins/{name}/enable and the rest -- answers a bare 404 Not Found as text/plain, meaning the compat layer has no route registered for them at all. Managed plugins are a Docker feature; Podman's own plugin model is not served here. Everything in this class therefore needs a real Docker daemon.

What this class returns

"list" and "inspect" return API::Docker::Type::Plugin objects carrying the convenience methods of API::Docker::Role::Entity::Plugin, following the list/inspect convention every other resource class here follows. It is one class for both, where containers and images have two: the swagger answers GET /plugins with an array of the Plugin definition and GET /plugins/{name}/json with that same definition.

Field names are the swagger's own spelling in snake_case, and the nested ones are generated classes rather than the raw HashRefs the old entity kept: $plugin->settings is an API::Docker::Type::Plugin::Settings whose ->env is a list of KEY=value strings, and $plugin->config an API::Docker::Type::Plugin::Config whose ->env is a list of API::Docker::Type::PluginEnv objects describing those same variables. The entity's methods thread the plugin's name back through this class.

Everything else returns the decoded engine response as it came: "privileges" an ArrayRef of privilege HashRefs, "install", "upgrade" and "push" an ArrayRef of progress events, and "enable", "disable", "remove" and "configure" undef.

client

Reference to API::Docker client. Weak reference to avoid circular dependencies.

list

my $plugins = $plugins->list;
my $enabled = $plugins->list(filters => { enabled => ['true'] });

List installed plugins. Returns an ArrayRef of API::Docker::Type::Plugin objects, each carrying the methods of API::Docker::Role::Entity::Plugin. An engine with no plugins installed answers [], never null, so this is an empty ArrayRef rather than undef.

Options:

  • filters - HashRef of filters, JSON-encoded by the transport. Values are ArrayRefs of strings even for booleans -- API::Docker::Role::Filters shape-checks and normalises that, but not the names, which the daemon validates itself

The accepted filter names are enabled and capability. It is enabled, not enable -- the published Engine API reference says enable, and the daemon validates plugin filter names against its own list, so the documented spelling is refused outright rather than silently matching nothing. enabled takes ['true'] or ['false']; capability takes a capability name such as ['volumedriver'].

privileges

my $privileges = $plugins->privileges('vieux/sshfs:latest');

Get the privileges a plugin demands, without installing it. Returns an ArrayRef of HashRefs:

[ { Name => 'network', Description => '', Value => ['host'] },
  { Name => 'mount',   Description => '', Value => ['/var/lib/docker/plugins/'] } ]

This is the first half of the install; see "Installing is two calls, and the engine enforces it". Reading it is the point -- the result is what you hand to "install", and the daemon accepts the install only if the two lists agree.

A plugin that demands nothing answers with an empty ArrayRef.

The remote reference is normalised by the daemon, so vieux/sshfs and docker.io/vieux/sshfs:latest name the same plugin; :latest is the default when no tag is given.

Options:

  • auth - Registry credentials for a plugin in a private registry; HashRef of username / password / serveraddress / identitytoken, or a pre-encoded base64 string. Sent as X-Registry-Auth. The Engine API reference does not document this header on this endpoint, but the daemon reads it here exactly as it does on the pull

install

my $privileges = $plugins->privileges('vieux/sshfs:latest');
$plugins->install('vieux/sshfs:latest', privileges => $privileges);

# blanket grant, in one call
$plugins->install('vieux/sshfs:latest', accept_privileges => 1);

Pull and install a plugin (POST /plugins/pull). The plugin is installed disabled -- call "enable" afterwards.

privileges is required. Without it this croaks and names both ways forward; see "Installing is two calls, and the engine enforces it" for why it is not defaulted.

Options:

  • privileges - ArrayRef of privilege HashRefs from "privileges". Required, unless accept_privileges is set

  • accept_privileges - Fetch the privileges and grant them, in one call. A blanket grant: use it where the call site is allowed to trust the plugin, and know that it reads as consent to whatever the plugin demands

  • name - Local name for the installed plugin, if it should differ from remote. A digest is not allowed here

  • auth - Registry credentials, as for "privileges"

  • on_event - CodeRef called with each progress event as it arrives, instead of the ArrayRef being collected and returned; see below

Returns an ArrayRef of progress events, one per object in the engine's newline-delimited JSON stream, [] when the engine sent no progress at all.

Progress as it arrives

Without a callback the whole stream is read before anything is parsed, so pulling a plugin is silence until it is done. Pass on_event and the events are handed over as the daemon sends them:

my $summary = $plugins->install('vieux/sshfs:latest',
    privileges => $privileges,
    on_event   => sub {
        my ($event, $stop) = @_;
        print $event->{status}, "\n" if defined $event->{status};
    },
);

$summary;   # { delivered => 18, stopped => 0 }

With a callback the return value is that summary HashRef, not the events: delivered is how many went to the callback, stopped is 1 when the callback ended the stream and 0 when the daemon did. Nothing is accumulated. See "Streaming a response as it arrives" in API::Docker::Role::HTTP.

The errorDetail check runs on this path too, per event rather than over the finished list, so a failure inside the 200 stream still croaks with an API::Docker::Error::Stream -- at the event that reports it, and carrying that one event alone rather than the whole stream. It is the difference "A failed build still croaks, one event earlier" in API::Docker::API::Images describes, and it applies here identically. A caller that wants the progress that preceded a failure must collect it in the callback.

"upgrade" and "push" take on_event on the same terms.

A failed install croaks by one of two routes, exactly as "pull" in API::Docker::API::Images does, because the daemon commits to HTTP 200 the moment it flushes the first progress object. A failure before that point arrives as a real error status -- incorrect privileges is reported this way, since it is decided before anything is pulled -- and one after it arrives as an errorDetail object inside the 200 stream, which croaks with an API::Docker::Error::Stream. eval and inspect $@ as a string rather than testing for the exception class.

inspect

my $plugin = $plugins->inspect('vieux/sshfs:latest');
say $plugin->enabled;
say join ', ', @{ $plugin->settings->env };

Get detailed information about an installed plugin. Returns an API::Docker::Type::Plugin -- the same class "list" returns; see "What this class returns".

The name may carry a registry host, a repository path and a tag (docker.io/vieux/sshfs:latest) and is interpolated into the request path as given: the daemon routes this endpoint as /plugins/{name:.*}/json, so the slashes and the colon must survive unescaped, and they do.

remove

$plugins->remove('vieux/sshfs:latest');
$plugins->remove('vieux/sshfs:latest', force => 1);

Remove an installed plugin. A plugin that is still enabled is refused unless force is set.

Options:

  • force - Disable the plugin before removing it. Removing a plugin that containers are still using will break them

Returns undef. The Engine API reference documents a Plugin object as the 200 response body here; the daemon writes no body at all.

enable

$plugins->enable('vieux/sshfs:latest');
$plugins->enable('vieux/sshfs:latest', timeout => 30);

Enable an installed plugin. Returns undef.

Options:

  • timeout - Seconds to wait for the plugin to come up, 0 for no timeout (the default)

timeout is always sent, whether or not the caller passes it. The Engine API reference gives it a default of 0, but the daemon has none: it reads the raw query value and parses it with Go's strconv.Atoi, so an absent parameter is parsed as the empty string and the request fails with strconv.Atoi: parsing "": invalid syntax as an invalid-parameter error. This is the one endpoint in the family where omitting an optional parameter is fatal.

disable

$plugins->disable('vieux/sshfs:latest');
$plugins->disable('vieux/sshfs:latest', force => 1);

Disable an enabled plugin. Returns undef.

Options:

  • force - Disable even while the plugin is in use. Mounts held by the plugin stay behind, which is what makes a later "remove" fail

upgrade

my $privileges = $plugins->privileges('vieux/sshfs:latest');
$plugins->upgrade('vieux/sshfs:latest', privileges => $privileges);

# upgrade a locally renamed plugin from its upstream reference
$plugins->upgrade('sshfs', remote => 'vieux/sshfs:v2',
    accept_privileges => 1);

Upgrade an installed plugin in place. The plugin must be disabled first.

Like "install" this carries the privilege list in its body and the daemon checks it against what the new version demands, so privileges is required here too -- an upgrade is where a plugin's demands can change, which is the case worth looking at.

Options:

  • privileges - ArrayRef of privilege HashRefs. Required, unless accept_privileges is set

  • accept_privileges - Fetch the privileges for remote and grant them, in one call

  • remote - Remote reference to upgrade to. Defaults to $name, which is what you want unless the plugin was installed under a local name

  • auth - Registry credentials, as for "privileges"

  • on_event - CodeRef called with each progress event as it arrives. The return value is then the summary HashRef; see "Progress as it arrives"

Returns an ArrayRef of progress events, [] when the engine sent no progress. Failure is reported by the same two routes as "install".

push

$plugins->push('myrepo/sshfs:v1', auth => {
    username      => 'me',
    password      => 'secret',
    serveraddress => 'https://index.docker.io/v1/',
});

Push an installed plugin to a registry. This writes to a real registry under the credentials given.

Options:

  • auth - Registry credentials; HashRef of username / password / serveraddress / identitytoken, or a pre-encoded base64 string. Sent as X-Registry-Auth

  • on_event - CodeRef called with each progress event as it arrives -- layer by layer, rather than the whole upload in one silence. The return value is then the summary HashRef; see "Progress as it arrives"

Unlike "push" in API::Docker::API::Images, which sends X-Registry-Auth on every call because the engine rejects an image push without it, this sends the header only when auth is given: the plugin router decodes the header and discards a decoding failure, so an anonymous push needs no header. The Engine API reference documents no header on this endpoint at all; the daemon reads it.

Returns an ArrayRef of progress events, [] when the engine sent no progress. Failure is reported by the same two routes as "install".

push shadows the Perl builtin inside this package, which is why namespace::clean is loaded. Always call it as a method.

configure

$plugins->configure('vieux/sshfs:latest', ['DEBUG=1']);
$plugins->configure('vieux/sshfs:latest', 'DEBUG=1', 'sshkey.source=/tmp');

Set a plugin's user-configurable settings (POST /plugins/{name}/set). The plugin must be disabled. Returns undef.

Settings are KEY=value strings, given either as one ArrayRef or as a plain list. They name the mutable fields of the plugin's config -- the environment variables, mount sources, devices and args that $plugin->settings reports; "inspect" is how you find out which ones a given plugin has.

The engine replaces nothing it is not told about, and rejects a key the plugin's config does not declare as mutable.

$plugins->configure('vieux/sshfs:latest', ['DEBUG=1']);
$plugins->configure('vieux/sshfs:latest', 'DEBUG=1');

Both forms mean the same call, and this method takes no options in either: anything after the ArrayRef croaks rather than being read as a setting or quietly dropped. To bound the request, clone the resource class -- $docker->plugins->using(read_timeout => 5)->configure(...), see API::Docker::Role::Using.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-api-docker/issues.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.