NAME

API::Docker::API::Configs - Docker Engine Configs API

VERSION

version 0.004

SYNOPSIS

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

# List configs
my $configs = $docker->configs->list;

# Create a config -- Data is RAW BYTES, this class base64-encodes it
my $created = $docker->configs->create(
    Name   => 'my-config',
    Data   => "listen 8080;\n",
    Labels => { app => 'web' },
);

# Inspect a config -- an API::Docker::Type::Config; spec->data stays base64
my $config = $docker->configs->inspect($created->{ID});
my $text   = $config->decoded_data;

# Update: the version comes from the inspect above, and is mandatory
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});

DESCRIPTION

This module provides methods for managing Docker configs (/configs): listing, creation, inspection, update and removal.

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

"list" and "inspect" return API::Docker::Type::Config objects carrying the convenience methods of API::Docker::Role::Entity::Config. It is one class for both, where containers and images have two: the swagger answers GET /configs with an array of the Config definition and GET /configs/{id} with that same definition. Field names are the swagger's own spelling in snake_case -- ID is ->id, CreatedAt is ->created_at -- and the nested ones are generated classes rather than the raw HashRefs the old entity kept: $config->spec is an API::Docker::Type::ConfigSpec and $config->version an API::Docker::Type::ObjectVersion, whose ->index is what version_index reaches.

Configs are API::Docker::API::Secrets without the secrecy: the same five endpoints, the same spec shape, the same mandatory version on update. The one behavioural difference is that a config's value can be read back -- "inspect" returns it in $config->spec->data, and "decoded_data" in API::Docker::Role::Entity::Config 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 docker config inspect in a secret.

Data is raw bytes on the way out, base64 on the way back

The wire field Data carries base64. "create" and "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 /secrets, which takes the identical field, a Data of the plain text "hello there!" was accepted with HTTP 200 and stored three bytes of garbage -- Go's decoder took the leading "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 standard base64 with padding (+ and /), 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 -v_--w== were rejected 500, the same four as +v/++w== stored correctly.

Data must be a byte string. Characters above U+00FF croak here rather than reaching MIME::Base64 and dying with a bare Wide character in subroutine entry; encode first, e.g. with Encode::encode_utf8.

The reverse trip is not symmetric, deliberately. "inspect" and "list" hand back what the daemon sent with nothing rewritten, so $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 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 -- "decoded_data" in API::Docker::Role::Entity::Config 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 });

update takes the current version, and it is mandatory

POST /configs/{id}/update carries a version query parameter and the daemon rejects the request without it. The value is the Version.Index of the config as it stands right now, which is what "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 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. "update" in API::Docker::Role::Entity::Config supplies it from the entity's own ->version->index instead, which is the same value read at the same moment.

The Engine API reference states that only Labels may actually change: the rest of the spec must go back unchanged from what inspect returned. Hence %spec = %{ $config->spec->TO_JSON } in the SYNOPSIS -- 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 inspect carries Data already base64-encoded, so passing it straight to "update" would encode it a second time; delete the key, or pass "decoded_data" in API::Docker::Role::Entity::Config in its place, before sending it back.

Swarm, and Podman

The Engine API groups /configs with Swarm. A Docker daemon that is not a swarm manager answers 503 "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 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.

Podman does not serve /configs, though what it answers for "not served" differs by path. Measured against the rootless socket on Podman 5.8.4 (API 1.44): GET /configs -- the collection listing -- still answers 404 with the plain-text body Not Found, not a JSON error, so the croak from this client reads Docker API error (404): Not Found. Every other path under it -- GET /configs/{id}, POST /configs/create, DELETE /configs/{id} and POST /configs/{id}/update -- answers 503 instead, with a JSON body naming the route it refuses, e.g. {"cause":"Podman does not support service: /v1.44/configs/xyz","message":"...","response":503}.

An earlier pass measured every path here as a flat 404 against Podman 5.4.2 (API 1.41). That measurement is not reproducible on this machine any more -- 5.4.2 is gone from it -- so whether 5.8.4 actually changed this or the original pass only ever exercised the collection endpoint is not something this distribution can decide from here; it is recorded as what 5.8.4 answers, not as a change from 5.4.2. Either way, the split this section used to draw between the two engines -- Docker's 503 "not a swarm manager" against a flat Podman 404 -- no longer holds cleanly: most /configs paths on Podman answer 503 too now, just with a different body and for a different reason.

There is no Podman-side equivalent to fall back on and no socket setting that enables it. This remains the one class in the distribution that cannot be exercised for real against the engine the rest of it is tested on -- API::Docker::API::Secrets, the same five endpoints, is served by Podman from its own local secret store.

client

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

list

my $configs = $configs->list;
my $configs = $configs->list(filters => { label => ['app=web'] });

List configs. Returns an ArrayRef of API::Docker::Type::Config objects, each carrying the methods of API::Docker::Role::Entity::Config. Each carries a ->spec->data that is still base64; "decoded_data" in API::Docker::Role::Entity::Config is the decode.

Options:

  • filters - HashRef of filters, JSON-encoded by the transport. The Engine API accepts id, label, name and names; values are always ArrayRefs of strings, shape-checked and normalised by API::Docker::Role::Filters.

create

my $created = $configs->create(
    Name   => 'my-config',
    Data   => "listen 8080;\n",
    Labels => { app => 'web' },
);

Create a config. Returns the daemon's response, a HashRef carrying ID -- not an API::Docker::Type::Config, because ID is all the daemon answers with and an entity built from it would carry no Spec and no Version. Call "inspect" on that ID for the object.

Options:

  • Name - Required. The config's name.

  • Data - Required. The config's content as raw bytes; this method base64-encodes it. See "Data is raw bytes on the way out, base64 on the way back".

  • Labels - HashRef of labels.

  • Templating - HashRef naming a templating driver, { Name => ..., Options => {...} }.

inspect

my $config = $configs->inspect($id);
my $index  = $config->version_index;      # what update needs
my $text   = $config->decoded_data;

Get a config by ID or name. Returns an API::Docker::Type::Config -- the same class "list" returns -- with ->id, ->spec, ->created_at, ->updated_at and ->version as the daemon sent them. ->spec->data is base64 and is not rewritten; "decoded_data" in API::Docker::Role::Entity::Config derives the bytes from it on demand.

update

my $config = $configs->inspect($id);
my %spec   = %{ $config->Spec };
delete $spec{Data};                       # already base64 -- see below
$spec{Labels} = { app => 'web', tier => 'edge' };

$configs->update($id, $config->version_index, %spec);
$config->update(%spec);                   # the same call, via the entity

Update a config. Returns nothing on success -- the daemon answers 200 with an empty body.

$version is mandatory and is the Version.Index from "inspect"; see "update takes the current version, and it is mandatory" for why it cannot be guessed and why the whole spec goes back. "update" in API::Docker::Role::Entity::Config fills it in from the entity it was called on.

A Data passed here is treated like "create"'s: raw bytes, encoded on the way out. The Data in a spec from "inspect" is already base64, so drop it, or replace it with "decoded_data" in API::Docker::Role::Entity::Config, before handing that spec back -- otherwise it gets encoded twice.

remove

$configs->remove($id);

Remove a config by ID or name. The daemon answers 204 with no body, so this returns nothing; a config that is not there is a 404 and croaks.

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.