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.009';
4 26     26   24729 use Moo::Role;
  26         84  
  26         229  
5 26     26   15314 use Types::Standard qw( InstanceOf Maybe );
  26         79  
  26         364  
6 26     26   78608 use Scalar::Util qw(blessed);
  26         57  
  26         60186  
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 797 my ($self) = @_;
39 385   66     1092 my $class = ref($self) || $self;
40              
41             # Standard API: IO::K8s::Api::Group::Version::Kind
42 385 50       2092 if ($class =~ /^IO::K8s::Api::(\w+)::(\w+)::/) {
43 385         1383 my ($group, $version) = ($1, $2);
44 385         752 $version = lc($version);
45 385 100       1720 return $version if $group eq 'Core';
46 171         339 my $group_lc = lc($group);
47 171   66     1334 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 123804 my ($self) = @_;
66 524   66     1499 my $class = ref($self) || $self;
67              
68 524 50       3488 if ($class =~ /::(\w+)$/) {
69 524         2676 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   873 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 598 my ($self, $file) = @_;
90 1 50       95 open my $fh, '>', $file or die "Cannot write to $file: $!";
91 1         7 print $fh $self->to_yaml;
92 1         4935 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   16 my ($self) = @_;
103 7 100       153 unless ($self->metadata) {
104 1         13 require IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ObjectMeta;
105 1         14 $self->metadata(IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ObjectMeta->new);
106             }
107 7         243 return $self->metadata;
108             }
109              
110              
111             sub add_label {
112 4     4 1 308669 my ($self, $key, $value) = @_;
113 4         14 my $meta = $self->_ensure_metadata;
114 4   100     95 my $labels = $meta->labels // {};
115 4         44 $labels->{$key} = $value;
116 4         71 $meta->labels($labels);
117 4         121 return $self;
118             }
119              
120              
121             sub add_labels {
122 1     1 1 4715 my ($self, %pairs) = @_;
123 1         4 my $meta = $self->_ensure_metadata;
124 1   50     16 my $labels = $meta->labels // {};
125 1         38 @{$labels}{keys %pairs} = values %pairs;
  1         3  
126 1         16 $meta->labels($labels);
127 1         22 return $self;
128             }
129              
130              
131             sub label {
132 9     9 1 5100 my ($self, $key) = @_;
133 9 100       231 my $labels = $self->metadata ? $self->metadata->labels : undef;
134 9 100       395 return defined $labels ? $labels->{$key} : undef;
135             }
136              
137              
138             sub has_label {
139 5     5 1 5013 my ($self, $key) = @_;
140 5 100       151 my $labels = $self->metadata ? $self->metadata->labels : undef;
141 5 100 100     239 return defined $labels && exists $labels->{$key} ? 1 : 0;
142             }
143              
144              
145             sub remove_label {
146 1     1 1 2562 my ($self, $key) = @_;
147 1 50 33     14 if ($self->metadata && $self->metadata->labels) {
148 1         40 delete $self->metadata->labels->{$key};
149             }
150 1         14 return $self;
151             }
152              
153              
154             sub match_labels {
155 5     5 1 2438 my ($self, %expected) = @_;
156 5 100       92 my $labels = $self->metadata ? $self->metadata->labels : undef;
157 5 100       121 return 0 unless defined $labels;
158 4         9 for my $key (keys %expected) {
159 5 100 100     24 return 0 unless exists $labels->{$key} && $labels->{$key} eq $expected{$key};
160             }
161 2         38 return 1;
162             }
163              
164              
165             sub add_annotation {
166 2     2 1 2464 my ($self, $key, $value) = @_;
167 2         8 my $meta = $self->_ensure_metadata;
168 2   50     45 my $annotations = $meta->annotations // {};
169 2         25 $annotations->{$key} = $value;
170 2         43 $meta->annotations($annotations);
171 2         67 return $self;
172             }
173              
174              
175             sub annotation {
176 3     3 1 12 my ($self, $key) = @_;
177 3 100       67 my $annotations = $self->metadata ? $self->metadata->annotations : undef;
178 3 100       88 return defined $annotations ? $annotations->{$key} : undef;
179             }
180              
181              
182             sub has_annotation {
183 5     5 1 15 my ($self, $key) = @_;
184 5 100       119 my $annotations = $self->metadata ? $self->metadata->annotations : undef;
185 5 100 100     225 return defined $annotations && exists $annotations->{$key} ? 1 : 0;
186             }
187              
188              
189             sub remove_annotation {
190 1     1 1 2842 my ($self, $key) = @_;
191 1 50 33     13 if ($self->metadata && $self->metadata->annotations) {
192 1         42 delete $self->metadata->annotations->{$key};
193             }
194 1         15 return $self;
195             }
196              
197             # ============================================================
198             # Status condition convenience methods
199             # ============================================================
200              
201             sub _extract_conditions {
202 20     20   41 my ($self) = @_;
203 20 100 66     717 return [] unless $self->can('status') && defined $self->status;
204 16         445 my $status = $self->status;
205              
206             # Typed status object with conditions accessor
207 16 50 33     140 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     93 if (ref $status eq 'HASH' && ref $status->{conditions} eq 'ARRAY') {
215 13         46 return $status->{conditions};
216             }
217              
218 3         11 return [];
219             }
220              
221             sub _condition_field {
222 28     28   54 my ($cond, $field) = @_;
223 28 50 33     68 if (blessed($cond) && $cond->can($field)) {
224 0         0 return $cond->$field;
225             }
226 28 50       66 if (ref $cond eq 'HASH') {
227 28         80 return $cond->{$field};
228             }
229 0         0 return undef;
230             }
231              
232              
233             sub conditions {
234 3     3 1 475527 my ($self) = @_;
235 3         47 return $self->_extract_conditions;
236             }
237              
238              
239             sub get_condition {
240 17     17 1 37 my ($self, $type) = @_;
241 17         59 for my $cond (@{ $self->_extract_conditions }) {
  17         43  
242 22         46 my $ctype = _condition_field($cond, 'type');
243 22 100 66     101 return $cond if defined $ctype && $ctype eq $type;
244             }
245 10         73 return undef;
246             }
247              
248              
249             sub is_condition_true {
250 12     12 1 31 my ($self, $type) = @_;
251 12         37 my $cond = $self->get_condition($type);
252 12 100       48 return 0 unless defined $cond;
253 4         10 my $status = _condition_field($cond, 'status');
254 4 100 66     45 return defined $status && $status eq 'True' ? 1 : 0;
255             }
256              
257              
258             sub is_ready {
259 5     5 1 18324 my ($self) = @_;
260 5 100       27 return 1 if $self->is_condition_true('Ready');
261 4 100       13 return 1 if $self->is_condition_true('Available');
262 3         88 return 0;
263             }
264              
265              
266             sub condition_message {
267 3     3 1 2103 my ($self, $type) = @_;
268 3         10 my $cond = $self->get_condition($type);
269 3 100       13 return undef unless defined $cond;
270 2         6 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__