File Coverage

blib/lib/Testcontainers/DockerClient.pm
Criterion Covered Total %
statement 18 78 23.0
branch 0 34 0.0
condition 0 21 0.0
subroutine 6 16 37.5
pod 0 8 0.0
total 24 157 15.2


line stmt bran cond sub pod time code
1             package Testcontainers::DockerClient;
2             # ABSTRACT: Docker client wrapper using WWW::Docker
3              
4 3     3   17 use strict;
  3         4  
  3         94  
5 3     3   11 use warnings;
  3         5  
  3         108  
6 3     3   1945 use Moo;
  3         30634  
  3         28  
7 3     3   10413 use Carp qw( croak );
  3         8  
  3         210  
8 3     3   18 use Log::Any qw( $log );
  3         8  
  3         28  
9 3     3   2594 use WWW::Docker;
  3         12  
  3         2813  
10              
11             our $VERSION = '0.001';
12              
13             =head1 DESCRIPTION
14              
15             Internal wrapper around L that provides a simplified interface
16             for Testcontainers operations. This module handles Docker daemon communication
17             for container lifecycle management, image operations, and container inspection.
18              
19             =cut
20              
21             has docker_host => (
22             is => 'ro',
23             default => sub { $ENV{DOCKER_HOST} // 'unix:///var/run/docker.sock' },
24             );
25              
26             has _client => (
27             is => 'lazy',
28             builder => sub {
29 0     0     my ($self) = @_;
30 0           WWW::Docker->new(
31             host => $self->docker_host,
32             );
33             },
34             );
35              
36             sub pull_image {
37 0     0 0   my ($self, $image) = @_;
38 0 0         croak "Image name required" unless $image;
39              
40 0           my ($name, $tag) = _parse_image($image);
41 0           $log->debugf("Pulling image: %s:%s", $name, $tag);
42              
43 0           eval {
44 0           $self->_client->images->pull(fromImage => $name, tag => $tag);
45             };
46 0 0         if ($@) {
47 0           $log->warnf("Failed to pull image %s:%s: %s", $name, $tag, $@);
48             # Don't die - image may already exist locally
49             }
50 0           return;
51             }
52              
53             =method pull_image($image)
54              
55             Pull a Docker image. Parses image name and tag from the full image string.
56             Does not die on failure (image may already exist locally).
57              
58             =cut
59              
60             sub create_container {
61 0     0 0   my ($self, $config, $name) = @_;
62 0 0         croak "Container config required" unless $config;
63              
64 0 0         $config->{name} = $name if defined $name;
65              
66 0           $log->debugf("Creating container with image: %s", $config->{Image});
67 0           my $result = $self->_client->containers->create(%$config);
68 0 0 0       croak "Failed to create container" unless $result && $result->{Id};
69              
70 0           return $result;
71             }
72              
73             =method create_container($config, $name)
74              
75             Create a Docker container from configuration hash. Returns hashref with C.
76              
77             =cut
78              
79             sub start_container {
80 0     0 0   my ($self, $id) = @_;
81 0 0         croak "Container ID required" unless $id;
82              
83 0           $log->debugf("Starting container: %s", $id);
84 0           $self->_client->containers->start($id);
85 0           return;
86             }
87              
88             =method start_container($id)
89              
90             Start a container by ID.
91              
92             =cut
93              
94             sub stop_container {
95 0     0 0   my ($self, $id, %opts) = @_;
96 0 0         croak "Container ID required" unless $id;
97              
98 0   0       my $timeout = $opts{timeout} // 10;
99 0           $log->debugf("Stopping container: %s (timeout: %d)", $id, $timeout);
100 0           eval { $self->_client->containers->stop($id, timeout => $timeout) };
  0            
101 0 0         if ($@) {
102 0           $log->warnf("Error stopping container %s: %s", $id, $@);
103             }
104 0           return;
105             }
106              
107             =method stop_container($id, %opts)
108              
109             Stop a container. Options: C (default 10 seconds).
110              
111             =cut
112              
113             sub remove_container {
114 0     0 0   my ($self, $id, %opts) = @_;
115 0 0         croak "Container ID required" unless $id;
116              
117 0           $log->debugf("Removing container: %s", $id);
118 0           eval {
119             $self->_client->containers->remove($id,
120             force => $opts{force} // 1,
121 0   0       volumes => $opts{volumes} // 1,
      0        
122             );
123             };
124 0 0         if ($@) {
125 0           $log->warnf("Error removing container %s: %s", $id, $@);
126             }
127 0           return;
128             }
129              
130             =method remove_container($id, %opts)
131              
132             Remove a container. Options: C (default true), C (default true).
133              
134             =cut
135              
136             sub inspect_container {
137 0     0 0   my ($self, $id) = @_;
138 0 0         croak "Container ID required" unless $id;
139              
140 0           my $info = $self->_client->containers->inspect($id);
141 0           return $info;
142             }
143              
144             =method inspect_container($id)
145              
146             Inspect container details. Returns L object.
147              
148             =cut
149              
150             sub container_logs {
151 0     0 0   my ($self, $id, %opts) = @_;
152 0 0         croak "Container ID required" unless $id;
153              
154             return $self->_client->containers->logs($id,
155             stdout => $opts{stdout} // 1,
156             stderr => $opts{stderr} // 1,
157             tail => $opts{tail} // 'all',
158 0   0       timestamps => $opts{timestamps} // 0,
      0        
      0        
      0        
159             );
160             }
161              
162             =method container_logs($id, %opts)
163              
164             Get container logs. Options: C, C, C, C.
165              
166             =cut
167              
168             sub exec_in_container {
169 0     0 0   my ($self, $id, $cmd, %opts) = @_;
170 0 0         croak "Container ID required" unless $id;
171 0 0         croak "Command required" unless $cmd;
172              
173 0 0         my @cmd = ref $cmd eq 'ARRAY' ? @$cmd : ($cmd);
174              
175             my $exec = $self->_client->exec->create($id,
176             Cmd => \@cmd,
177             AttachStdout => \1,
178             AttachStderr => \1,
179 0 0         Tty => $opts{tty} ? \1 : \0,
180             );
181              
182 0           my $output = $self->_client->exec->start($exec->{Id});
183 0           my $info = $self->_client->exec->inspect($exec->{Id});
184              
185             return {
186 0   0       exit_code => $info->{ExitCode} // -1,
      0        
187             output => $output // '',
188             };
189             }
190              
191             =method exec_in_container($id, $cmd, %opts)
192              
193             Execute a command inside a running container. Returns hashref with
194             C and C.
195              
196             =cut
197              
198             sub _parse_image {
199 0     0     my ($image) = @_;
200              
201             # Handle images with registry prefix (e.g., docker.io/library/nginx:latest)
202 0           my $tag = 'latest';
203 0 0         if ($image =~ m{^(.+):([^:/]+)$}) {
204 0           return ($1, $2);
205             }
206 0           return ($image, $tag);
207             }
208              
209             1;