API-Docker
view release on metacpan or search on metacpan
lib/API/Docker/API/Configs.pm view on Meta::CPAN
package API::Docker::API::Configs;
# ABSTRACT: Docker Engine Configs API
our $VERSION = '0.004';
use Moo;
with 'API::Docker::Role::Filters', 'API::Docker::Role::Using';
use API::Docker::Role::Entity::Config;
use API::Docker::Type::Config;
use Carp qw( croak );
use MIME::Base64 qw( encode_base64 );
use namespace::clean;
has client => (
is => 'ro',
required => 1,
weak_ref => 1,
);
# The class is the caller's argument, as it is on the resource classes whose
# list and inspect really are two definitions -- here both are the swagger's
# one `Config`, and passing it keeps the seam in the same place.
#
# from_data, not new: this is a daemon response, and the two entry points of
# API::Docker::Role::Type read it differently. from_data takes the swagger's
# wire names and nothing else, so a key it has not heard of keeps its own
# spelling instead of being read as the Perl name of one it has, and a value
# that disagrees with the swagger costs its own field rather than the whole
# response. `client` is ours rather than the engine's, so it goes beside the
# data instead of into it.
sub _wrap {
my ($self, $class, $data) = @_;
return $class->from_data($data, client => $self->client);
}
sub _wrap_list {
my ($self, $class, $list) = @_;
return [ map { $self->_wrap($class, $_) } @$list ];
}
# The wire field is base64; the public contract is raw bytes. Guarding the
# character range here keeps the failure a croak naming this class instead of
# MIME::Base64's "Wide character in subroutine entry" from two frames down.
sub _encode_data {
my ($self, $method, $data) = @_;
croak __PACKAGE__ . "->$method Data must be a byte string, not decoded "
. 'characters -- encode it first (Encode::encode_utf8)'
if $data =~ /[^\x00-\xff]/;
return encode_base64($data, '');
}
sub list {
my ($self, %opts) = @_;
my %params;
$params{filters} = $self->_normalise_filters($opts{filters})
if defined $opts{filters};
return $self->_wrap_list('API::Docker::Type::Config',
$self->client->get('/configs',
params => \%params,
%{ $self->_request_options },
) // []);
}
sub create {
my ($self, %spec) = @_;
croak __PACKAGE__ . '->create Name required'
unless defined $spec{Name} && length $spec{Name};
croak __PACKAGE__ . '->create Data required'
unless defined $spec{Data} && length $spec{Data};
$spec{Data} = $self->_encode_data('create', $spec{Data});
return $self->client->post('/configs/create', \%spec);
}
sub inspect {
my ($self, $id) = @_;
croak __PACKAGE__ . '->inspect config ID or name required'
unless defined $id && length $id;
return $self->_wrap('API::Docker::Type::Config',
$self->client->get("/configs/$id",
%{ $self->_request_options },
));
}
sub update {
my ($self, $id, $version, %spec) = @_;
croak __PACKAGE__ . '->update config ID or name required'
unless defined $id && length $id;
croak __PACKAGE__ . '->update requires the current version as its second '
. 'argument: the Version.Index from inspect($id), which the daemon uses '
. 'as an optimistic-concurrency token and will not accept the update '
. 'without'
unless defined $version;
croak __PACKAGE__ . '->update version must be the numeric Version.Index '
. "from inspect(\$id), got '$version'"
unless $version =~ /\A[0-9]+\z/;
$spec{Data} = $self->_encode_data('update', $spec{Data})
if defined $spec{Data};
return $self->client->post("/configs/$id/update", \%spec,
params => { version => $version });
}
sub remove {
my ($self, $id) = @_;
lib/API/Docker/API/Configs.pm view on Meta::CPAN
my %spec = %{ $config->spec->TO_JSON };
delete $spec{Data}; # already base64 -- see below
$spec{Labels} = { app => 'web', tier => 'edge' };
$config->update(%spec);
# Remove
$docker->configs->remove($created->{ID});
=head1 DESCRIPTION
This module provides methods for managing Docker configs (C</configs>):
listing, creation, inspection, update and removal.
Accessed via C<< $docker->configs >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->configs->using(read_timeout => 5) >>.
L</list> and L</inspect> return L<API::Docker::Type::Config> objects carrying
the convenience methods of L<API::Docker::Role::Entity::Config>. It is B<one>
class for both, where containers and images have two: the swagger answers
C<GET /configs> with an array of the C<Config> definition and
C<GET /configs/{id}> with that same definition. Field names are the
swagger's own spelling in snake_case -- C<ID> is C<< ->id >>, C<CreatedAt> is
C<< ->created_at >> -- and the nested ones are generated classes rather than
the raw HashRefs the old entity kept: C<< $config->spec >> is an
L<API::Docker::Type::ConfigSpec> and C<< $config->version >> an
L<API::Docker::Type::ObjectVersion>, whose C<< ->index >> is what
C<version_index> reaches.
Configs are L<API::Docker::API::Secrets> without the secrecy: the same five
endpoints, the same spec shape, the same mandatory C<version> on update. The
one behavioural difference is that a config's value can be read back --
L</inspect> returns it in C<< $config->spec->data >>, and
L<API::Docker::Role::Entity::Config/decoded_data> decodes it, where a secret
returns no payload at all. Which is the whole point of the split: put configuration in
a config, and anything you would mind seeing in a C<docker config inspect> in
a secret.
=head2 Data is raw bytes on the way out, base64 on the way back
The wire field C<Data> carries base64. B<L</create> and L</update> encode it
for you> -- pass them raw bytes, and do not pre-encode, or the daemon stores
your base64 text as the config's content.
Doing it here is not a convenience, it is a guard. The daemon does not
validate what it decodes: measured against Podman 5.4.2's C</secrets>, which
takes the identical field, a C<Data> of the plain text C<"hello there!"> was
accepted with B<HTTP 200> and stored three bytes of garbage -- Go's decoder
took the leading C<"hell">, stopped at the space, and said nothing. A caller
left to encode their own payload can corrupt the value and be told it worked.
The alphabet is B<standard> base64 with padding (C<+> and C</>), unwrapped,
and not the URL-safe one. The Engine API reference calls the field
"base64-url-safe-encoded" and that is measurably not what the engine takes:
four bytes sent as C<-v_--w==> were rejected B<500>, the same four as
C<+v/++w==> stored correctly.
C<Data> must be a byte string. Characters above C<U+00FF> croak here rather
than reaching L<MIME::Base64> and dying with a bare
C<Wide character in subroutine entry>; encode first, e.g. with
C<Encode::encode_utf8>.
B<The reverse trip is not symmetric, deliberately.> L</inspect> and L</list>
hand back what the daemon sent with nothing rewritten, so
C<< $config->spec->data >> is still base64. Decoding is a separate,
explicit call on the entity:
my $text = $config->decoded_data;
The asymmetry follows one rule: this class encodes where getting it wrong is
silent, and rewrites nothing where getting it wrong is visible. An unencoded
C<Data> going out is stored as garbage with a 200; a base64 string coming back
is obvious the moment you look at it. So the decode is offered where it costs
nothing -- L<API::Docker::Role::Entity::Config/decoded_data> derives the bytes
on demand and leaves the spec verbatim -- rather than by replacing a field of a daemon
response, which nothing in this distribution does.
To send an already-encoded value verbatim, bypass this class:
$docker->post('/configs/create', { Name => 'my-config', Data => $b64 });
=head2 update takes the current version, and it is mandatory
C<POST /configs/{id}/update> carries a C<version> query parameter and the
daemon rejects the request without it. The value is the C<Version.Index> of
the config as it stands right now, which is what L</inspect> returns:
my $config = $docker->configs->inspect($id);
$docker->configs->update($id, $config->version_index, %spec);
It is an optimistic-concurrency token, not a serial number to invent. If
anything else changed the config since that C<inspect>, the index has moved on
and the daemon refuses the write rather than silently overwriting that change.
Read it immediately before the update, and again before a retry.
This class makes it the second positional argument and croaks when it is
missing or not numeric, so the mistake surfaces here instead of one round trip
later. L<API::Docker::Role::Entity::Config/update> supplies it from the
entity's own C<< ->version->index >> instead, which is the same value read at
the same moment.
The Engine API reference states that only C<Labels> may actually change: the
rest of the spec must go back unchanged from what C<inspect> returned. Hence
C<< %spec = %{ $config->spec->TO_JSON } >> in the SYNOPSIS -- C<TO_JSON>
renders the spec object back into the daemon's own spelling, and the whole
spec goes back with the one key edited. Note that a spec from C<inspect>
carries C<Data> already base64-encoded, so passing it straight to L</update>
would encode it a second time; delete the key, or pass
L<API::Docker::Role::Entity::Config/decoded_data> in its place, before
sending it back.
=head2 Swarm, and Podman
The Engine API groups C</configs> with Swarm. A Docker daemon that is not a
swarm manager answers B<503> C<"This node is not a swarm manager."> to all of
these endpoints, which this client turns into a croak. That is documented
engine behaviour, not a fault at this end -- the daemon needs
C<docker swarm init>, or a manager to talk to, and a single-node install that
has never run it is the ordinary case, not an edge one.
B<Podman does not serve C</configs>,> though what it answers for "not served"
( run in 2.166 seconds using v1.01-cache-2.11-cpan-364913b4093 )