API-Docker

 view release on metacpan or  search on metacpan

lib/API/Docker/API/Plugins.pm  view on Meta::CPAN

  return $self->client->delete_request("/plugins/$name",
    params => \%params,
    %{ $self->_request_options },
  );
}


sub enable {
  my ($self, $name, %opts) = @_;
  croak __PACKAGE__ . '->enable plugin name required' unless $name;
  # Always sent, and not conditional on the caller passing it: the daemon
  # parses this parameter with strconv.Atoi and has no default, so an absent
  # timeout is parsed as the empty string and answers 400. See the POD.
  my %params = ( timeout => $opts{timeout} // 0 );
  return $self->client->post("/plugins/$name/enable", undef,
    params => \%params,
    %{ $self->_request_options },
  );
}


sub disable {
  my ($self, $name, %opts) = @_;
  croak __PACKAGE__ . '->disable plugin name required' unless $name;
  my %params;
  $params{force} = $opts{force} ? 1 : 0 if defined $opts{force};
  return $self->client->post("/plugins/$name/disable", undef,
    params => \%params,
    %{ $self->_request_options },
  );
}


sub upgrade {
  my ($self, $name, %opts) = @_;
  croak __PACKAGE__ . '->upgrade plugin name required' unless $name;

  my $remote = $opts{remote} // $name;
  my $privileges = $self->_privileges_body('upgrade', $remote, %opts);

  return $self->client->post("/plugins/$name/upgrade", $privileges,
    params => { remote => $remote },
    $self->_auth_headers(\%opts),
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


sub push {
  my ($self, $name, %opts) = @_;
  croak __PACKAGE__ . '->push plugin name required' unless $name;
  return $self->client->post("/plugins/$name/push", undef,
    $self->_auth_headers(\%opts),
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


sub configure {
  my ($self, $name, @settings) = @_;
  croak __PACKAGE__ . '->configure plugin name required' unless $name;

  # One ArrayRef or a plain list, and nothing after either: this method reads
  # no options at all. The ArrayRef form used to be where the transport bounds
  # went, because a trailing `read_timeout => 2` in the plain list would be two
  # more settings as far as this method can tell -- they now go on the resource
  # class instead (karr k74), and what is left is a form, not a split.
  if (ref $settings[0] eq 'ARRAY') {
    my $list = shift @settings;
    croak __PACKAGE__ . '->configure takes nothing after the ArrayRef of '
      . 'settings; a transport bound goes on the resource class, as '
      . '$docker->plugins->using(read_timeout => 5)->configure(...)'
      if @settings;
    @settings = @$list;
  }

  croak __PACKAGE__ . '->configure requires at least one setting, as an '
    . 'ArrayRef or a list of "KEY=value" strings' unless @settings;

  croak __PACKAGE__ . '->configure settings must be plain strings'
    if grep { ref $_ } @settings;

  return $self->client->post("/plugins/$name/set", \@settings,
    %{ $self->_request_options },
  );
}



1;

__END__

=pod

=encoding UTF-8

=head1 NAME

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

=head1 VERSION

version 0.004

=head1 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');

=head1 DESCRIPTION

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

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

=head2 Installing is two calls, and the engine enforces it

C<< POST /plugins/pull >> takes the list of privileges the plugin demands
B<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
C<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.

L</privileges> is the first call, L</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);

C<install> B<requires> C<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
C<network: host> would fail with an error naming neither. Passing
C<< accept_privileges => 1 >> makes C<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 L</upgrade>, which takes the same body.

=head2 Not available on Podman

Measured against the rootless Podman socket (5.4.2, API 1.41): B<none> of the
C</plugins> endpoints exist there. C<< GET /v1.41/plugins >> answers
C<404 Not Found> with
C<< {"cause":"","message":"Path /v1.41/plugins is not supported","response":0} >>
(the C<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 -- C</plugins/privileges>,
C</plugins/pull>, C</plugins/{name}/json>, C</plugins/{name}/enable> and the
rest -- answers a bare C<404 Not Found> as C<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.

=head2 What this class returns

L</list> and L</inspect> return L<API::Docker::Type::Plugin> objects carrying
the convenience methods of L<API::Docker::Role::Entity::Plugin>, following
the C<list>/C<inspect> convention every other resource class here follows.
It is B<one> class for both, where containers and images have two: the
swagger answers C<GET /plugins> with an array of the C<Plugin> definition and
C<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:
C<< $plugin->settings >> is an L<API::Docker::Type::Plugin::Settings> whose
C<< ->env >> is a list of C<KEY=value> strings, and C<< $plugin->config >> an
L<API::Docker::Type::Plugin::Config> whose C<< ->env >> is a list of
L<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: L</privileges>
an ArrayRef of privilege HashRefs, L</install>, L</upgrade> and L</push> an
ArrayRef of progress events, and L</enable>, L</disable>, L</remove> and
L</configure> C<undef>.

=head2 client

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

=head2 list

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

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

Options:

=over

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

=back

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

=head2 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 L</"Installing is two calls, and
the engine enforces it">. Reading it is the point -- the result is what you
hand to L</install>, and the daemon accepts the install only if the two
lists agree.

A plugin that demands nothing answers with an empty ArrayRef.

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

Options:

=over

=item * C<auth> - Registry credentials for a plugin in a private registry;
HashRef of C<username> / C<password> / C<serveraddress> / C<identitytoken>,

lib/API/Docker/API/Plugins.pm  view on Meta::CPAN

=over

=item * C<privileges> - ArrayRef of privilege HashRefs. Required, unless
C<accept_privileges> is set

=item * C<accept_privileges> - Fetch the privileges for C<remote> and grant
them, in one call

=item * C<remote> - Remote reference to upgrade to. Defaults to C<$name>,
which is what you want unless the plugin was installed under a local name

=item * C<auth> - Registry credentials, as for L</privileges>

=item * C<on_event> - CodeRef called with each progress event as it arrives.
The return value is then the summary HashRef; see
L</"Progress as it arrives">

=back

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

=head2 push

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

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

Options:

=over

=item * C<auth> - Registry credentials; HashRef of C<username> / C<password> /
C<serveraddress> / C<identitytoken>, or a pre-encoded base64 string. Sent as
C<X-Registry-Auth>

=item * C<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 L</"Progress as it arrives">

=back

Unlike L<API::Docker::API::Images/push>, which sends C<X-Registry-Auth> on
every call because the engine rejects an image push without it, this sends
the header only when C<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, C<[]> when the engine sent no
progress. Failure is reported by the same two routes as L</install>.

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

=head2 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 (C<< POST /plugins/{name}/set >>).
The plugin must be disabled. Returns C<undef>.

Settings are C<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 C<< $plugin->settings >>
reports; L</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 --
C<< $docker->plugins->using(read_timeout => 5)->configure(...) >>, see
L<API::Docker::Role::Using>.

=head1 SEE ALSO

=over

=item * L<API::Docker::Role::Entity::Plugin> - the convenience methods the
returned objects carry

=item * L<API::Docker::Type::Plugin> - the fields L</list> and L</inspect>
return

=item * L<API::Docker> - Main Docker client

=item * L<API::Docker::Role::RegistryAuth> - the C<X-Registry-Auth>
encoding used here, shared with the other registry-facing endpoints

=item * L<API::Docker::API::Images> - Image endpoints, whose C<push>
sends that header on every call rather than only when credentials were
given

=item * L<API::Docker::Error::Stream> - Raised for a failure reported inside
a 200 event stream by L</install>, L</upgrade> and L</push>

=back

=head1 SUPPORT

=head2 Issues

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

=head1 CONTRIBUTING

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

=head1 AUTHOR

Torsten Raudssus <getty@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> L<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.

=cut



( run in 0.474 second using v1.01-cache-2.11-cpan-aadc1410aed )