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 )