NAME

API::Docker::API::Secrets - Docker Engine Secrets API

VERSION

version 0.004

SYNOPSIS

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

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

# Create a secret -- Data is RAW BYTES, this class base64-encodes it
my $created = $docker->secrets->create(
    Name   => 'my-secret',
    Data   => "hunter2\n",
    Labels => { env => 'prod' },
);

# Inspect a secret -- an API::Docker::Type::Secret
my $secret = $docker->secrets->inspect($created->{ID});
say $secret->spec->name;

# Update: the version comes from the inspect above, and is mandatory
my %spec = %{ $secret->spec->TO_JSON };
$spec{Labels} = { env => 'staging' };
$secret->update(%spec);

# Remove
$docker->secrets->remove($created->{ID});

DESCRIPTION

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

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

"list" and "inspect" return API::Docker::Type::Secret objects carrying the convenience methods of API::Docker::Role::Entity::Secret. It is one class for both, where containers and images have two: the swagger answers GET /secrets with an array of the Secret definition and GET /secrets/{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: $secret->spec is an API::Docker::Type::SecretSpec and $secret->version an API::Docker::Type::ObjectVersion, whose ->index is what version_index reaches.

The value of a secret is write-only. "list" and "inspect" return the metadata -- ->id, ->spec, ->created_at, ->version -- and never the payload; the engine hands that out to containers, not over this API. If you need to read the value back, this is the wrong storage: use API::Docker::API::Configs, whose entity offers a decoded_data because the daemon actually sends one.

Data is raw bytes; this class does the base64

The wire field Data carries base64. This class encodes it for you. Pass "create" raw bytes and they go out encoded; do not pre-encode, or the daemon faithfully stores your base64 text as the secret.

That division of labour is not a matter of taste, because the daemon does not validate what it decodes. Measured against Podman 5.4.2: a Data of the plain text "hello there!" was accepted with HTTP 200 and stored three bytes of garbage -- Go's decoder consumed the leading "hell", stopped at the space, and reported nothing. A caller left to encode their own payload can therefore corrupt a secret and be told it succeeded. Doing it here removes that failure mode from the caller entirely.

The alphabet is standard base64 with padding (+ and /), not the URL-safe one, and unwrapped. The Engine API reference calls the field "base64-url-safe-encoded"; that is measurably not what the engine accepts. The same four bytes sent as -v_--w== were rejected with 500 "secret data must be larger than 0 and less than 512000 bytes" -- the URL-safe alphabet decoded to nothing -- where +v/++w== was stored correctly.

Data must be a byte string. A string holding characters above U+00FF croaks here rather than reaching MIME::Base64, which would die with a bare Wide character in subroutine entry. Encode it first, for instance with Encode::encode_utf8.

To send an already-encoded value verbatim, bypass this class and use the transport directly:

$docker->post('/secrets/create', { Name => 'my-secret', Data => $b64 });

update takes the current version, and it is mandatory

POST /secrets/{id}/update carries a version query parameter, and the daemon rejects the request without it. The value is the Version.Index of the secret as it stands right now, which is what "inspect" returns:

my $secret = $docker->secrets->inspect($id);
$docker->secrets->update($id, $secret->version_index, %spec);

It is an optimistic-concurrency token, not a serial number to invent. If anything else changed the secret since that inspect, the index has moved on and the daemon refuses the write instead of silently overwriting that change. Read it immediately before the update, and read it again before a retry.

This class makes it the second positional argument and croaks when it is missing or not numeric, so the mistake is caught here rather than one round trip later. "update" in API::Docker::Role::Entity::Secret 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: every other field of the spec must be sent back unchanged from what inspect returned. Hence the %spec = %{ $secret->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, not just the key you edited.

Swarm, and what Podman serves instead

The Engine API groups /secrets with Swarm. A Docker daemon that is not a swarm manager answers 503 "This node is not a swarm manager." to every one of these endpoints, and this client turns that into a croak. That is the engine behaving as documented, not a fault at this end: it 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.

GET /info's Swarm.LocalNodeState does not tell you which of those two you are looking at. Measured fresh against both engines with no swarm initialized anywhere, it reports "inactive" on Docker and on Podman alike -- and Podman still serves /secrets with 200 and real data in that state, while Docker still answers 503. Whether /secrets works is a property of the engine, not of that field.

Podman is the useful exception. Measured against Podman 5.4.2 (API 1.41) with no swarm involved anywhere, /secrets is served from Podman's own local secret store: "list", "create", "inspect" and "remove" all work, and the objects carry a Version.Index just as Docker's do. Two differences worth knowing:

  • "create" answers 200 where Docker documents 201. The body is the same { ID => ... }, so only code inspecting the status code notices.

  • "update" is not implemented at all: 501 "update is not supported", with or without a version parameter.

API::Docker::API::Configs gets none of this -- Podman does not serve /configs from a real store the way it does /secrets; see "Swarm, and Podman" in API::Docker::API::Configs for what it answers instead, which is not simply "no route" on every path.

client

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

list

my $secrets = $secrets->list;
my $secrets = $secrets->list(filters => { label => ['env=prod'] });

List secrets. Returns an ArrayRef of API::Docker::Type::Secret objects, each carrying the methods of API::Docker::Role::Entity::Secret.

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 = $secrets->create(
    Name   => 'my-secret',
    Data   => "hunter2\n",
    Labels => { env => 'prod' },
);

Create a secret. Returns the daemon's response, a HashRef carrying ID -- not an API::Docker::Type::Secret, 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 secret's name.

  • Data - Required. The secret's value as raw bytes; this method base64-encodes it. See "Data is raw bytes; this class does the base64".

  • Labels - HashRef of labels.

  • Driver - HashRef naming an external secret driver, { Name => ..., Options => {...} }.

  • Templating - HashRef naming a templating driver, same shape.

inspect

my $secret = $secrets->inspect($id);
my $index  = $secret->version_index;      # what update needs

Get a secret's metadata by ID or name. Returns an API::Docker::Type::Secret -- the same class "list" returns -- with ->id, ->spec, ->created_at, ->updated_at and ->version. Never the value -- see "There is no accessor for the value" in API::Docker::Role::Entity::Secret.

update

my $secret = $secrets->inspect($id);
my %spec   = %{ $secret->spec->TO_JSON };
$spec{Labels} = { env => 'staging' };

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

Update a secret. 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::Secret fills it in from the entity it was called on. Podman does not implement this endpoint and answers 501.

remove

$secrets->remove($id);

Remove a secret by ID or name. The daemon answers 204 with no body, so this returns nothing; a secret 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.