Dist-Zilla-Plugin-Docker-API
view release on metacpan or search on metacpan
lib/Dist/Zilla/Plugin/Docker/API.pm view on Meta::CPAN
# multi-line message gets repeated in full.
sub _flatten_error {
my ($self, $error) = @_;
$error = '' unless defined $error;
$error =~ s/\s+at\s+\S+\s+line\s+\d+\.?//g;
$error =~ s/\s+/ /g;
$error =~ s/^\s+|\s+$//g;
return $error;
}
# after_build builds an image unconditionally, so every dzil command that
# builds needs a reachable engine. Ask for one up front instead of letting
# Dist::Zilla gather, munge and write out a whole distribution first and only
# then die on a socket that was never there.
sub before_build {
my ($self) = @_;
if ($ENV{DZIL_DOCKER_API_SKIP}) {
$self->log('DZIL_DOCKER_API_SKIP is set: skipping the image build '
.'for this run - no engine contact, no image');
return;
}
return if $ENV{DZIL_DOCKER_API_SKIP_PRECHECK};
my $info = eval { $self->client->engine_info };
my $error = $@;
if ($error) {
$error = $self->_flatten_error($error);
$self->log('Docker::API speaks the Docker Engine HTTP API over a '
.'socket and never shells out to the docker binary, so any engine '
.'serving that API will do.');
$self->log('Point DOCKER_HOST at one. For rootless Podman: '
.'systemctl --user enable --now podman.socket, then '
.'DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"');
$self->log_fatal('cannot reach a container engine: '.$error
.' (set DZIL_DOCKER_API_SKIP_PRECHECK=1 to skip this check)');
}
$self->log('Docker::API engine ready: '
.($info->{engine} // 'unknown').' '
.($info->{version} // '?')
.' (API '.($info->{api_version} // '?').')');
$self->_precheck_registry_auth
if $ENV{DZIL_RELEASING} && $self->release_enabled && $self->release_push;
}
# A rejected or expired registry credential otherwise surfaces only when the
# push fails -- after Dist::Zilla gathered and wrote the distribution and
# after_build built and tagged the image, i.e. after the damage. This runs
# before any of it.
#
# The phase hook that runs early enough is before_build, and it can tell a
# release from a plain build: Dist::Zilla::Dist::Builder::release sets
# DZIL_RELEASING before it calls build_archive, so the variable is already
# there when this hook runs (measured against Dist::Zilla 6.037). A plain
# dzil build never sets it and so never needs registry credentials.
#
# Only the registry host of `image` decides which credential applies, and
# that part carries no template variables, so no expansion is needed here.
sub _precheck_registry_auth {
my ($self) = @_;
my $image_ref = $self->image;
my $status = eval { $self->client->verify_auth_for_image_ref($image_ref) };
my $error = $@;
# Deliberately not worded as "rejected": the engine answers a bad
# credential and an unreachable registry with the same failure -- Podman
# returns 500 for both, so the status cannot tell them apart. The engine's
# own text, which does, is carried through verbatim.
if ($error) {
$self->log_fatal('the registry credential check for '.$image_ref
.' failed: '.$self->_flatten_error($error)
.' (set DZIL_DOCKER_API_SKIP_PRECHECK=1 to skip this check)');
}
# No credential found for that registry. An anonymous push is a legal
# thing to attempt, so this is a note, not a failure.
unless (defined $status) {
$self->log('Docker::API: no registry credentials found for '
.$image_ref.' - the release push will be anonymous');
return;
}
$self->log('Docker::API registry credentials accepted for '.$image_ref);
}
sub after_build {
my ($self, $arg) = @_;
return if $ENV{DZIL_DOCKER_API_SKIP};
$self->log("Docker::API building image");
my $build_root = $arg->{build_root};
my $zilla = $self->zilla;
my %tmpl_vars = $self->_template_vars($build_root, undef, $arg->{archive});
my @image_refs = $self->_resolve_tags($self->tag, %tmpl_vars);
my %labels = $self->_resolve_labels(%tmpl_vars);
my %build_args = $self->_resolve_build_args(%tmpl_vars);
my $context_path = Path::Tiny->new($build_root // $self->zilla->root);
unless ($context_path->child($self->dockerfile)->exists) {
$self->log_fatal("Dockerfile '" . $self->dockerfile
. "' not found in build context: $context_path");
}
my $context_tar = {
type => 'dir',
path => $context_path->stringify,
dockerfile => $self->dockerfile,
};
my @platforms = @{ $self->platform };
my $result = $self->client->build_image(
context_tar => $context_tar,
dockerfile => $self->dockerfile,
tags => \@image_refs,
labels => \%labels,
buildargs => \%build_args,
pull => $self->pull,
nocache => $self->no_cache,
rm => $self->rm,
forcerm => $self->force_rm,
target => $self->target,
network_mode => $self->network_mode,
platform => $platforms[0],
verbose => $self->build_verbose,
);
$self->_log_build_result($result);
}
sub release {
my ($self, $archive) = @_;
# A skipped build phase means there is no image to tag and push. Refusing
# here beats releasing a dist whose containers silently never shipped.
$self->log_fatal('DZIL_DOCKER_API_SKIP is set: refusing to release '
.'- the image build was skipped, there is nothing to push')
if $ENV{DZIL_DOCKER_API_SKIP};
# Skip if release is disabled
lib/Dist/Zilla/Plugin/Docker/API.pm view on Meta::CPAN
image = ghcr.io/example/my-app
tags = latest %V %v
=head1 DESCRIPTION
This plugin builds and publishes Docker images as release artifacts derived from
the Dist::Zilla-built distribution.
=head1 BEHAVIOR
| Dzil command | Docker behavior |
|---|---|
| C<dzil build> | Build image, apply every C<tag>, load into daemon (if C<build_load=1>), no push |
| C<dzil release> | Re-tag the built image with every C<tag>, push (if C<release_push=1>), load (if C<release_load=1>) |
The same C<tag> list is used in both phases â C<dzil build> produces local tags
for verification, C<dzil release> re-applies them (against the already-built
image) and pushes if configured.
=head1 CONTAINER ENGINE
Builds and pushes go through L<API::Docker>, which speaks the Docker Engine
HTTP API over a socket. No C<docker> binary is involved at any point, so any
engine serving that API will do, and Docker itself need not be installed.
Podman's rootless socket is a tested alternative:
systemctl --user enable --now podman.socket
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
C<target> reaches the engine unchanged, so the multi-stage builds this plugin
is usually pointed at behave the same either way.
The socket is located from C<DOCKER_HOST>, falling back to
C</var/run/docker.sock> and nothing else. Docker contexts are not consulted, so
a daemon selected with C<docker context use> will not be picked up here; set
C<DOCKER_HOST> in the environment C<dzil> runs in. See
L<API::Docker/CONTAINER ENGINES> for how that compares to other clients.
=head2 Startup precheck
Because C<after_build> builds an image unconditionally, every C<dzil> command
that builds needs a reachable engine. The plugin therefore asks the engine for
its version in C<before_build>, before Dist::Zilla gathers a single file, and
gives up there if nothing answers -- rather than letting a whole distribution
be assembled and only then dying on a socket that was never there.
On success the engine is named in the build log:
[Docker::API] Docker::API engine ready: Podman Engine 5.4.2 (API 1.41)
Set C<DZIL_DOCKER_API_SKIP_PRECHECK=1> to skip the check and get the previous
behaviour back, where an unreachable engine only surfaces once the build
reaches the image. With several C<Docker::API> plugins in one F<dist.ini>,
each runs its own precheck.
When C<before_build> runs as part of C<dzil release> (Dist::Zilla sets
C<DZIL_RELEASING> before it calls C<build_archive>, which is early enough to
tell a release from a plain build) and both C<release_enabled> and
C<release_push> are true, the same hook also pre-flights the registry
credential the eventual push would use -- resolved as described in
L</"Registry credentials"> below -- and hands it to the engine's
C<POST /auth> (C<< system->auth >>) before anything is built. A plain
C<dzil build> never triggers this and needs no registry credentials at all.
No credential resolved for the registry is not a failure -- an anonymous push
is a legal thing to attempt, so nothing is checked and nothing fails. A
failed check is fatal, before the build starts, and its message says only
that the check failed, not that the credential was rejected: Podman answers
a rejected credential and an unreachable registry with the same C<500>, so
the two cannot be told apart from the status alone, and the engine's own
text is included instead.
C<DZIL_DOCKER_API_SKIP_PRECHECK=1> skips this credential check along with the
engine version probe above.
Set C<DZIL_DOCKER_API_SKIP=1> to skip the image build entirely for one run --
no engine contact, no image, one loud log line per plugin. This is for local
C<dzil build> / C<dzil install> / C<dzil test> while the image cannot build
yet, for example while a dependency pinned in the F<Dockerfile>'s C<cpanm>
run is not released. C<dzil release> refuses to run with the variable set:
a skipped build phase means there is no image to tag and push.
=head2 Registry credentials
The release push, the C<fail_if_tag_exists> lookup and the registry
credential precheck above all resolve a credential for an image reference
the same way, through C<auth_for_image_ref>: the C<auths> block of
F<config.json> in the directory named by C<DOCKER_CONFIG>, or
F<~/.docker/config.json> when that is unset. Nothing else is read --
C<REGISTRY_AUTH_FILE> and Podman's own
F<$XDG_RUNTIME_DIR/containers/auth.json> are not consulted, regardless of
which engine is at the other end of C<DOCKER_HOST>.
Within the matching registry's entry, the first of these present wins: an
C<identitytoken>; a base64 C<auth> field decoded to C<username:password>;
plain C<username> / C<password> fields. Docker Hub is matched under any of
C<https://index.docker.io/v1/> and C<v2/>, C<index.docker.io> or
C<docker.io>. A C<credsStore> or C<credHelpers> entry that delegates the
secret to an external helper is not supported -- nothing in this plugin
reads either key, so such a registry resolves to no credential and any
request against it goes out anonymous rather than failing.
C<docker login> is the usual way to populate the file. C<podman login>
writes to its own auth file instead
(F<$XDG_RUNTIME_DIR/containers/auth.json> by default, overridable with
C<REGISTRY_AUTH_FILE>), which this plugin never reads; point it at the file
that is read instead:
podman login --authfile ~/.docker/config.json registry.example.com
Finding no credential for a registry is never an error by itself in this
plugin -- both C<fail_if_tag_exists> and the release push treat it as "go
anonymous," and only a credential that C<auth_for_image_ref> did find and
the engine then rejects (or an unreachable registry -- see above) is fatal.
=head1 CONFIGURATION
=over 4
=item C<image> - Full image repository (required). Example: C<ghcr.io/user/my-app>
=item C<tag> - Tags applied to the image (can be repeated, template-enabled).
Default: C<latest>, C<%V>, and C<%v> (e.g. C<latest>, C<0>, C<0.402>).
Applied identically in both build and release. Note: setting C<tag>
explicitly B<replaces> the default list, it does not append to it.
=item C<dockerfile> - Dockerfile name (default: C<Dockerfile>)
=item C<build_load> - Load built image into local Docker daemon (default: true)
=item C<release_push> - Push to registry during release (default: true)
=item C<release_load> - Load released image locally (default: false)
=item C<build_verbose> - When false (default), the build log only echoes
Dockerfile step headers â the legacy builder format C<Step N/M : ...>,
Podman's classic builder C<STEP N/M: ...> and BuildKit's C<#N [N/M] ...> â
instead of the full per-command output.
Set to true to see every line the daemon streams back. Errors are always
surfaced regardless of this flag.
=item C<fail_if_tag_exists> - Abort the release if any tag already exists on
the remote registry (default: false). The check runs before anything is
tagged or pushed, and only when C<release_push> is also true. It asks the
I<registry>, not the local daemon, through C<API::Docker>'s
C<< distribution->exists >> (C<GET /distribution/{name}/json>), using the
credential resolved for C<image> (see L</"Registry credentials">), or an
anonymous request when none applies.
An engine that has no C</distribution> route -- rootless Podman among them --
cannot answer the question at all, and that is treated as a release-stopping
failure rather than as "the tag is free": the release aborts with the
engine's own error and a reminder that C<fail_if_tag_exists = 0> releases
without the check.
=item C<skip_latest_on_trial> - Skip C<latest> tag for trial releases
=item C<build_arg> - Build arguments (can be repeated, template-enabled)
=item C<label> - OCI labels (can be repeated, template-enabled)
=item C<platform> - Target platform (can be repeated)
=back
=head1 DEPRECATED
The following names are still accepted but emit a warning and will be removed
in a future release. Each is funneled into its canonical attribute by
C<BUILDARGS>; where both spellings are given, the canonical one wins and the
collision is reported.
=over 4
=item C<file> - Use C<dockerfile> instead.
Until 0.104 this was the only spelling that worked: the attribute carried
C<< init_arg => 'file' >> while the documentation described C<dockerfile>, so
C<dockerfile = ...> in a F<dist.ini> was silently ignored and the default
F<Dockerfile> used instead. C<dockerfile> is now the canonical key.
=item C<build_tag>, C<release_tag>
Replaced by the single C<tag> attribute. When either is given, the values are
merged (build_tag first, release_tag second) into C<tag> and a deprecation
warning is emitted. If C<tag> is also set explicitly, it wins and the legacy
values are ignored.
=item C<repository> - Use C<image> instead.
=item C<push> - Use C<release_push> instead.
=item C<load> - Use C<build_load> instead.
=item C<phase> - No longer needed; build and release phases are implicit. It is
accepted, warned about and discarded; it has no canonical counterpart.
=back
Note that C<repository>, C<push> and C<load> did B<not> work as aliases before
0.104: they were declared as readers taking their value I<from> the canonical
attribute, so setting one in a F<dist.ini> had no effect whatsoever. They are
funneled properly now.
=head1 SEE ALSO
L<Dist::Zilla::Plugin::Docker::API::TagTemplate>,
( run in 1.157 second using v1.01-cache-2.11-cpan-007c89162af )