File Coverage

blib/lib/IO/K8s/Role/APIObject.pm
Criterion Covered Total %
statement 109 153 71.2
branch 50 82 60.9
condition 31 78 39.7
subroutine 25 30 83.3
pod 23 23 100.0
total 238 366 65.0


line stmt bran cond sub pod time code
1             package IO::K8s::Role::APIObject;
2             # ABSTRACT: Role for top-level Kubernetes API objects
3             our $VERSION = '1.008';
4 26     26   19492 use Moo::Role;
  26         66  
  26         231  
5 26     26   17155 use Types::Standard qw( InstanceOf Maybe );
  26         67  
  26         313  
6 26     26   79591 use Scalar::Util qw(blessed);
  26         56  
  26         57297  
7              
8             has metadata => (
9             is => 'rw',
10             isa => Maybe[InstanceOf['IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ObjectMeta']],
11             );
12              
13              
14             # Map IO::K8s short group names to full Kubernetes API group names
15             my %API_GROUP_MAP = (
16             rbac => 'rbac.authorization.k8s.io',
17             networking => 'networking.k8s.io',
18             storage => 'storage.k8s.io',
19             admissionregistration => 'admissionregistration.k8s.io',
20             certificates => 'certificates.k8s.io',
21             coordination => 'coordination.k8s.io',
22             events => 'events.k8s.io',
23             scheduling => 'scheduling.k8s.io',
24             authentication => 'authentication.k8s.io',
25             authorization => 'authorization.k8s.io',
26             node => 'node.k8s.io',
27             discovery => 'discovery.k8s.io',
28             flowcontrol => 'flowcontrol.apiserver.k8s.io',
29             );
30              
31             # Derive apiVersion from class name
32             # IO::K8s::Api::Core::V1::Pod -> v1
33             # IO::K8s::Api::Apps::V1::Deployment -> apps/v1
34             # IO::K8s::Api::Rbac::V1::Role -> rbac.authorization.k8s.io/v1
35             # IO::K8s::ApiextensionsApiserver::...::V1::CustomResourceDefinition -> apiextensions.k8s.io/v1
36             # IO::K8s::KubeAggregator::...::V1::APIService -> apiregistration.k8s.io/v1
37             sub api_version {
38 385     385 1 994 my ($self) = @_;
39 385   66     1201 my $class = ref($self) || $self;
40              
41             # Standard API: IO::K8s::Api::Group::Version::Kind
42 385 50       2311 if ($class =~ /^IO::K8s::Api::(\w+)::(\w+)::/) {
43 385         1509 my ($group, $version) = ($1, $2);
44 385         921 $version = lc($version);
45 385 100       1977 return $version if $group eq 'Core';
46 171         404 my $group_lc = lc($group);
47 171   66     1458 return ($API_GROUP_MAP{$group_lc} // $group_lc) . '/' . $version;
48             }
49              
50             # Apiextensions: IO::K8s::ApiextensionsApiserver::Pkg::Apis::Apiextensions::Version::Kind
51 0 0       0 if ($class =~ /^IO::K8s::ApiextensionsApiserver::Pkg::Apis::Apiextensions::(\w+)::/) {
52 0         0 return 'apiextensions.k8s.io/' . lc($1);
53             }
54              
55             # KubeAggregator: IO::K8s::KubeAggregator::Pkg::Apis::Apiregistration::Version::Kind
56 0 0       0 if ($class =~ /^IO::K8s::KubeAggregator::Pkg::Apis::Apiregistration::(\w+)::/) {
57 0         0 return 'apiregistration.k8s.io/' . lc($1);
58             }
59              
60 0         0 return undef;
61             }
62              
63              
64             sub kind {
65 524     524 1 148854 my ($self) = @_;
66 524   66     1832 my $class = ref($self) || $self;
67              
68 524 50       3880 if ($class =~ /::(\w+)$/) {
69 524         2799 return $1;
70             }
71 0         0 return undef;
72             }
73              
74              
75 0     0 1 0 sub resource_plural { undef }
76              
77              
78 203     203   1024 sub _is_resource { 1 }
79              
80             sub to_yaml {
81 0     0 1 0 my ($self) = @_;
82 0         0 require YAML::PP;
83 0         0 my $yp = YAML::PP->new(schema => [qw/JSON/], boolean => 'JSON::PP');
84 0         0 return $yp->dump_string($self->TO_JSON);
85             }
86              
87              
88             sub save {
89 1     1 1 959 my ($self, $file) = @_;
90 1 50       83 open my $fh, '>', $file or die "Cannot write to $file: $!";
91 1         8 print $fh $self->to_yaml;
92 1         6888 close $fh;
93 1         15 return $self;
94             }
95              
96              
97             # ============================================================
98             # Label & annotation convenience methods
99             # ============================================================
100              
101             sub _ensure_metadata {
102 7     7   17 my ($self) = @_;
103 7 100       193 unless ($self->metadata) {
104 1         114 require IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ObjectMeta;
105 1         34 $self->metadata(IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ObjectMeta->new);
106             }
107 7         369 return $self->metadata;
108             }
109              
110              
111             sub add_label {
112 4     4 1 492769 my ($self, $key, $value) = @_;
113 4         17 my $meta = $self->_ensure_metadata;
114 4   100     113 my $labels = $meta->labels // {};
115 4         47 $labels->{$key} = $value;
116 4         90 $meta->labels($labels);
117 4         134 return $self;
118             }
119              
120              
121             sub add_labels {
122 1     1 1 5174 my ($self, %pairs) = @_;
123 1         5 my $meta = $self->_ensure_metadata;
124 1   50     26 my $labels = $meta->labels // {};
125 1         17 @{$labels}{keys %pairs} = values %pairs;
  1         6  
126 1         52 $meta->labels($labels);
127 1         35 return $self;
128             }
129              
130              
131             sub label {
132 9     9 1 6253 my ($self, $key) = @_;
133 9 100       328 my $labels = $self->metadata ? $self->metadata->labels : undef;
134 9 100       525 return defined $labels ? $labels->{$key} : undef;
135             }
136              
137              
138             sub has_label {
139 5     5 1 5130 my ($self, $key) = @_;
140 5 100       158 my $labels = $self->metadata ? $self->metadata->labels : undef;
141 5 100 100     379 return defined $labels && exists $labels->{$key} ? 1 : 0;
142             }
143              
144              
145             sub remove_label {
146 1     1 1 5793 my ($self, $key) = @_;
147 1 50 33     27 if ($self->metadata && $self->metadata->labels) {
148 1         84 delete $self->metadata->labels->{$key};
149             }
150 1         30 return $self;
151             }
152              
153              
154             sub match_labels {
155 5     5 1 10911 my ($self, %expected) = @_;
156 5 100       177 my $labels = $self->metadata ? $self->metadata->labels : undef;
157 5 100       317 return 0 unless defined $labels;
158 4         13 for my $key (keys %expected) {
159 5 100 100     41 return 0 unless exists $labels->{$key} && $labels->{$key} eq $expected{$key};
160             }
161 2         15 return 1;
162             }
163              
164              
165             sub add_annotation {
166 2     2 1 10665 my ($self, $key, $value) = @_;
167 2         8 my $meta = $self->_ensure_metadata;
168 2   50     61 my $annotations = $meta->annotations // {};
169 2         25 $annotations->{$key} = $value;
170 2         47 $meta->annotations($annotations);
171 2         67 return $self;
172             }
173              
174              
175             sub annotation {
176 3     3 1 13 my ($self, $key) = @_;
177 3 100       126 my $annotations = $self->metadata ? $self->metadata->annotations : undef;
178 3 100       137 return defined $annotations ? $annotations->{$key} : undef;
179             }
180              
181              
182             sub has_annotation {
183 5     5 1 22 my ($self, $key) = @_;
184 5 100       161 my $annotations = $self->metadata ? $self->metadata->annotations : undef;
185 5 100 100     270 return defined $annotations && exists $annotations->{$key} ? 1 : 0;
186             }
187              
188              
189             sub remove_annotation {
190 1     1 1 4286 my ($self, $key) = @_;
191 1 50 33     32 if ($self->metadata && $self->metadata->annotations) {
192 1         83 delete $self->metadata->annotations->{$key};
193             }
194 1         31 return $self;
195             }
196              
197             # ============================================================
198             # Status condition convenience methods
199             # ============================================================
200              
201             sub _extract_conditions {
202 20     20   25 my ($self) = @_;
203 20 100 66     435 return [] unless $self->can('status') && defined $self->status;
204 16         311 my $status = $self->status;
205              
206             # Typed status object with conditions accessor
207 16 50 33     83 if (blessed($status) && $status->can('conditions')) {
208 0         0 my $conds = $status->conditions;
209 0 0       0 return $conds if ref $conds eq 'ARRAY';
210 0         0 return [];
211             }
212              
213             # Opaque hashref (CRDs)
214 16 100 66     61 if (ref $status eq 'HASH' && ref $status->{conditions} eq 'ARRAY') {
215 13         36 return $status->{conditions};
216             }
217              
218 3         61 return [];
219             }
220              
221             sub _condition_field {
222 28     28   36 my ($cond, $field) = @_;
223 28 50 33     68 if (blessed($cond) && $cond->can($field)) {
224 0         0 return $cond->$field;
225             }
226 28 50       41 if (ref $cond eq 'HASH') {
227 28         58 return $cond->{$field};
228             }
229 0         0 return undef;
230             }
231              
232              
233             sub conditions {
234 3     3 1 354400 my ($self) = @_;
235 3         11 return $self->_extract_conditions;
236             }
237              
238              
239             sub get_condition {
240 17     17 1 25 my ($self, $type) = @_;
241 17         18 for my $cond (@{ $self->_extract_conditions }) {
  17         31  
242 22         31 my $ctype = _condition_field($cond, 'type');
243 22 100 66     65 return $cond if defined $ctype && $ctype eq $type;
244             }
245 10         32 return undef;
246             }
247              
248              
249             sub is_condition_true {
250 12     12 1 24 my ($self, $type) = @_;
251 12         23 my $cond = $self->get_condition($type);
252 12 100       34 return 0 unless defined $cond;
253 4         8 my $status = _condition_field($cond, 'status');
254 4 100 66     34 return defined $status && $status eq 'True' ? 1 : 0;
255             }
256              
257              
258             sub is_ready {
259 5     5 1 9962 my ($self) = @_;
260 5 100       12 return 1 if $self->is_condition_true('Ready');
261 4 100       8 return 1 if $self->is_condition_true('Available');
262 3         12 return 0;
263             }
264              
265              
266             sub condition_message {
267 3     3 1 1280 my ($self, $type) = @_;
268 3         6 my $cond = $self->get_condition($type);
269 3 100       8 return undef unless defined $cond;
270 2         5 return _condition_field($cond, 'message');
271             }
272              
273             # ============================================================
274             # Owner reference convenience methods
275             # ============================================================
276              
277              
278             sub set_owner {
279 0     0 1   my ($self, $owner) = @_;
280 0           require IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::OwnerReference;
281 0           my $meta = $self->_ensure_metadata;
282 0   0       my $refs = $meta->ownerReferences // [];
283              
284 0   0       my $ref = IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::OwnerReference->new(
285             apiVersion => $owner->api_version,
286             kind => $owner->kind,
287             name => $owner->metadata->name,
288             uid => $owner->metadata->uid // '',
289             controller => 1,
290             );
291              
292 0           $meta->ownerReferences([@$refs, $ref]);
293 0           return $self;
294             }
295              
296              
297             sub is_owned_by {
298 0     0 1   my ($self, $owner) = @_;
299 0           my $refs = $self->owner_refs;
300 0 0         my $owner_uid = $owner->metadata ? $owner->metadata->uid : undef;
301 0 0         my $owner_name = $owner->metadata ? $owner->metadata->name : undef;
302 0           my $owner_kind = $owner->kind;
303              
304 0           for my $ref (@$refs) {
305 0           my ($rname, $ruid, $rkind);
306 0 0 0       if (blessed($ref) && $ref->can('name')) {
    0          
307 0           $rname = $ref->name;
308 0           $ruid = $ref->uid;
309 0           $rkind = $ref->kind;
310             } elsif (ref $ref eq 'HASH') {
311 0           $rname = $ref->{name};
312 0           $ruid = $ref->{uid};
313 0           $rkind = $ref->{kind};
314             }
315              
316             # Match by UID if both have it, otherwise by name+kind
317 0 0 0       if (defined $owner_uid && $owner_uid ne '' && defined $ruid && $ruid ne '') {
    0 0        
      0        
      0        
      0        
      0        
318 0 0         return 1 if $ruid eq $owner_uid;
319             } elsif (defined $owner_name && defined $rname && defined $owner_kind && defined $rkind) {
320 0 0 0       return 1 if $rname eq $owner_name && $rkind eq $owner_kind;
321             }
322             }
323 0           return 0;
324             }
325              
326              
327             sub owner_refs {
328 0     0 1   my ($self) = @_;
329 0 0         return [] unless $self->metadata;
330 0   0       return $self->metadata->ownerReferences // [];
331             }
332              
333             1;
334              
335             __END__