| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package IO::K8s; |
|
2
|
|
|
|
|
|
|
# ABSTRACT: Objects representing things found in the Kubernetes API |
|
3
|
|
|
|
|
|
|
|
|
4
|
18
|
|
|
18
|
|
3175059
|
use v5.10; |
|
|
18
|
|
|
|
|
68
|
|
|
5
|
18
|
|
|
18
|
|
11875
|
use Moo; |
|
|
18
|
|
|
|
|
156782
|
|
|
|
18
|
|
|
|
|
126
|
|
|
6
|
18
|
|
|
18
|
|
43125
|
use Module::Runtime qw(require_module); |
|
|
18
|
|
|
|
|
35822
|
|
|
|
18
|
|
|
|
|
117
|
|
|
7
|
18
|
|
|
18
|
|
9368
|
use JSON::MaybeXS; |
|
|
18
|
|
|
|
|
235750
|
|
|
|
18
|
|
|
|
|
1518
|
|
|
8
|
18
|
|
|
18
|
|
169
|
use Scalar::Util (); |
|
|
18
|
|
|
|
|
29
|
|
|
|
18
|
|
|
|
|
426
|
|
|
9
|
18
|
|
|
18
|
|
9370
|
use namespace::clean; |
|
|
18
|
|
|
|
|
292612
|
|
|
|
18
|
|
|
|
|
159
|
|
|
10
|
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
our $VERSION = '1.008'; |
|
12
|
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
# Track which classes we've auto-generated |
|
14
|
|
|
|
|
|
|
my %_autogen_cache; |
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
# Default resource map - maps short names to class paths relative to IO::K8s |
|
17
|
|
|
|
|
|
|
my %DEFAULT_RESOURCE_MAP = ( |
|
18
|
|
|
|
|
|
|
# Core API resources |
|
19
|
|
|
|
|
|
|
Binding => 'Api::Core::V1::Binding', |
|
20
|
|
|
|
|
|
|
ComponentStatus => 'Api::Core::V1::ComponentStatus', |
|
21
|
|
|
|
|
|
|
ConfigMap => 'Api::Core::V1::ConfigMap', |
|
22
|
|
|
|
|
|
|
Endpoints => 'Api::Core::V1::Endpoints', |
|
23
|
|
|
|
|
|
|
Event => 'Api::Core::V1::Event', |
|
24
|
|
|
|
|
|
|
LimitRange => 'Api::Core::V1::LimitRange', |
|
25
|
|
|
|
|
|
|
Namespace => 'Api::Core::V1::Namespace', |
|
26
|
|
|
|
|
|
|
Node => 'Api::Core::V1::Node', |
|
27
|
|
|
|
|
|
|
PersistentVolume => 'Api::Core::V1::PersistentVolume', |
|
28
|
|
|
|
|
|
|
PersistentVolumeClaim => 'Api::Core::V1::PersistentVolumeClaim', |
|
29
|
|
|
|
|
|
|
Pod => 'Api::Core::V1::Pod', |
|
30
|
|
|
|
|
|
|
PodTemplate => 'Api::Core::V1::PodTemplate', |
|
31
|
|
|
|
|
|
|
ReplicationController => 'Api::Core::V1::ReplicationController', |
|
32
|
|
|
|
|
|
|
ResourceQuota => 'Api::Core::V1::ResourceQuota', |
|
33
|
|
|
|
|
|
|
Secret => 'Api::Core::V1::Secret', |
|
34
|
|
|
|
|
|
|
Service => 'Api::Core::V1::Service', |
|
35
|
|
|
|
|
|
|
ServiceAccount => 'Api::Core::V1::ServiceAccount', |
|
36
|
|
|
|
|
|
|
# Apps |
|
37
|
|
|
|
|
|
|
ControllerRevision => 'Api::Apps::V1::ControllerRevision', |
|
38
|
|
|
|
|
|
|
DaemonSet => 'Api::Apps::V1::DaemonSet', |
|
39
|
|
|
|
|
|
|
Deployment => 'Api::Apps::V1::Deployment', |
|
40
|
|
|
|
|
|
|
ReplicaSet => 'Api::Apps::V1::ReplicaSet', |
|
41
|
|
|
|
|
|
|
StatefulSet => 'Api::Apps::V1::StatefulSet', |
|
42
|
|
|
|
|
|
|
# Batch |
|
43
|
|
|
|
|
|
|
CronJob => 'Api::Batch::V1::CronJob', |
|
44
|
|
|
|
|
|
|
Job => 'Api::Batch::V1::Job', |
|
45
|
|
|
|
|
|
|
# Networking |
|
46
|
|
|
|
|
|
|
Ingress => 'Api::Networking::V1::Ingress', |
|
47
|
|
|
|
|
|
|
IngressClass => 'Api::Networking::V1::IngressClass', |
|
48
|
|
|
|
|
|
|
NetworkPolicy => 'Api::Networking::V1::NetworkPolicy', |
|
49
|
|
|
|
|
|
|
# Storage |
|
50
|
|
|
|
|
|
|
CSIDriver => 'Api::Storage::V1::CSIDriver', |
|
51
|
|
|
|
|
|
|
CSINode => 'Api::Storage::V1::CSINode', |
|
52
|
|
|
|
|
|
|
CSIStorageCapacity => 'Api::Storage::V1::CSIStorageCapacity', |
|
53
|
|
|
|
|
|
|
StorageClass => 'Api::Storage::V1::StorageClass', |
|
54
|
|
|
|
|
|
|
VolumeAttachment => 'Api::Storage::V1::VolumeAttachment', |
|
55
|
|
|
|
|
|
|
# Authorization |
|
56
|
|
|
|
|
|
|
LocalSubjectAccessReview => 'Api::Authorization::V1::LocalSubjectAccessReview', |
|
57
|
|
|
|
|
|
|
SelfSubjectAccessReview => 'Api::Authorization::V1::SelfSubjectAccessReview', |
|
58
|
|
|
|
|
|
|
SelfSubjectRulesReview => 'Api::Authorization::V1::SelfSubjectRulesReview', |
|
59
|
|
|
|
|
|
|
SubjectAccessReview => 'Api::Authorization::V1::SubjectAccessReview', |
|
60
|
|
|
|
|
|
|
# Authentication |
|
61
|
|
|
|
|
|
|
SelfSubjectReview => 'Api::Authentication::V1::SelfSubjectReview', |
|
62
|
|
|
|
|
|
|
TokenRequest => 'Api::Authentication::V1::TokenRequest', |
|
63
|
|
|
|
|
|
|
TokenReview => 'Api::Authentication::V1::TokenReview', |
|
64
|
|
|
|
|
|
|
# RBAC |
|
65
|
|
|
|
|
|
|
ClusterRole => 'Api::Rbac::V1::ClusterRole', |
|
66
|
|
|
|
|
|
|
ClusterRoleBinding => 'Api::Rbac::V1::ClusterRoleBinding', |
|
67
|
|
|
|
|
|
|
Role => 'Api::Rbac::V1::Role', |
|
68
|
|
|
|
|
|
|
RoleBinding => 'Api::Rbac::V1::RoleBinding', |
|
69
|
|
|
|
|
|
|
# Policy |
|
70
|
|
|
|
|
|
|
Eviction => 'Api::Policy::V1::Eviction', |
|
71
|
|
|
|
|
|
|
PodDisruptionBudget => 'Api::Policy::V1::PodDisruptionBudget', |
|
72
|
|
|
|
|
|
|
# Autoscaling |
|
73
|
|
|
|
|
|
|
HorizontalPodAutoscaler => 'Api::Autoscaling::V2::HorizontalPodAutoscaler', |
|
74
|
|
|
|
|
|
|
Scale => 'Api::Autoscaling::V1::Scale', |
|
75
|
|
|
|
|
|
|
# Certificates |
|
76
|
|
|
|
|
|
|
CertificateSigningRequest => 'Api::Certificates::V1::CertificateSigningRequest', |
|
77
|
|
|
|
|
|
|
# Coordination |
|
78
|
|
|
|
|
|
|
Lease => 'Api::Coordination::V1::Lease', |
|
79
|
|
|
|
|
|
|
# Discovery |
|
80
|
|
|
|
|
|
|
EndpointSlice => 'Api::Discovery::V1::EndpointSlice', |
|
81
|
|
|
|
|
|
|
# Scheduling |
|
82
|
|
|
|
|
|
|
PriorityClass => 'Api::Scheduling::V1::PriorityClass', |
|
83
|
|
|
|
|
|
|
# Node |
|
84
|
|
|
|
|
|
|
RuntimeClass => 'Api::Node::V1::RuntimeClass', |
|
85
|
|
|
|
|
|
|
# Flowcontrol |
|
86
|
|
|
|
|
|
|
FlowSchema => 'Api::Flowcontrol::V1::FlowSchema', |
|
87
|
|
|
|
|
|
|
PriorityLevelConfiguration => 'Api::Flowcontrol::V1::PriorityLevelConfiguration', |
|
88
|
|
|
|
|
|
|
# Admissionregistration |
|
89
|
|
|
|
|
|
|
MutatingWebhookConfiguration => 'Api::Admissionregistration::V1::MutatingWebhookConfiguration', |
|
90
|
|
|
|
|
|
|
ValidatingAdmissionPolicy => 'Api::Admissionregistration::V1::ValidatingAdmissionPolicy', |
|
91
|
|
|
|
|
|
|
ValidatingAdmissionPolicyBinding => 'Api::Admissionregistration::V1::ValidatingAdmissionPolicyBinding', |
|
92
|
|
|
|
|
|
|
ValidatingWebhookConfiguration => 'Api::Admissionregistration::V1::ValidatingWebhookConfiguration', |
|
93
|
|
|
|
|
|
|
# Extension APIs (different base paths) |
|
94
|
|
|
|
|
|
|
CustomResourceDefinition => 'ApiextensionsApiserver::Pkg::Apis::Apiextensions::V1::CustomResourceDefinition', |
|
95
|
|
|
|
|
|
|
APIService => 'KubeAggregator::Pkg::Apis::Apiregistration::V1::APIService', |
|
96
|
|
|
|
|
|
|
); |
|
97
|
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
has json => (is => 'ro', default => sub { |
|
99
|
|
|
|
|
|
|
return JSON::MaybeXS->new(utf8 => 1, canonical => 1); |
|
100
|
|
|
|
|
|
|
}); |
|
101
|
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
# Resource map - can be customized per instance |
|
103
|
|
|
|
|
|
|
# Returns a copy so add() can safely mutate without affecting other instances |
|
104
|
|
|
|
|
|
|
has resource_map => ( |
|
105
|
|
|
|
|
|
|
is => 'ro', |
|
106
|
|
|
|
|
|
|
lazy => 1, |
|
107
|
|
|
|
|
|
|
default => sub { +{ %DEFAULT_RESOURCE_MAP } }, |
|
108
|
|
|
|
|
|
|
); |
|
109
|
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
# OpenAPI spec for auto-generating unknown types |
|
111
|
|
|
|
|
|
|
has openapi_spec => ( |
|
112
|
|
|
|
|
|
|
is => 'ro', |
|
113
|
|
|
|
|
|
|
predicate => 1, |
|
114
|
|
|
|
|
|
|
); |
|
115
|
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
# External resource map providers to merge at construction time |
|
117
|
|
|
|
|
|
|
# e.g. with => ['IO::K8s::Cilium'] or with => [IO::K8s::Cilium->new] |
|
118
|
|
|
|
|
|
|
has with => ( |
|
119
|
|
|
|
|
|
|
is => 'ro', |
|
120
|
|
|
|
|
|
|
default => sub { [] }, |
|
121
|
|
|
|
|
|
|
); |
|
122
|
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
# User namespaces to search for pre-built classes (checked before IO::K8s::) |
|
124
|
|
|
|
|
|
|
# e.g. ['MyProject::K8s'] will look for MyProject::K8s::HelmChart before IO::K8s::... |
|
125
|
|
|
|
|
|
|
has class_namespaces => ( |
|
126
|
|
|
|
|
|
|
is => 'ro', |
|
127
|
|
|
|
|
|
|
default => sub { [] }, |
|
128
|
|
|
|
|
|
|
); |
|
129
|
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
# Internal: unique autogen namespace for this instance (isolated, collision-free) |
|
131
|
|
|
|
|
|
|
has _autogen_namespace => ( |
|
132
|
|
|
|
|
|
|
is => 'ro', |
|
133
|
|
|
|
|
|
|
lazy => 1, |
|
134
|
|
|
|
|
|
|
default => sub { |
|
135
|
|
|
|
|
|
|
my $self = shift; |
|
136
|
|
|
|
|
|
|
# Create unique identifier based on object address |
|
137
|
|
|
|
|
|
|
my $id = sprintf('%x', 0 + $self); |
|
138
|
|
|
|
|
|
|
return "IO::K8s::_AUTOGEN_$id"; |
|
139
|
|
|
|
|
|
|
}, |
|
140
|
|
|
|
|
|
|
); |
|
141
|
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
# Class method to get default resource map |
|
143
|
0
|
|
|
0
|
0
|
0
|
sub default_resource_map { \%DEFAULT_RESOURCE_MAP } |
|
144
|
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
sub BUILD { |
|
146
|
60
|
|
|
60
|
0
|
2395
|
my ($self) = @_; |
|
147
|
60
|
100
|
|
|
|
130
|
$self->add(@{$self->with}) if @{$self->with}; |
|
|
33
|
|
|
|
|
169
|
|
|
|
60
|
|
|
|
|
631
|
|
|
148
|
|
|
|
|
|
|
} |
|
149
|
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
# Merge external resource maps into this instance |
|
151
|
|
|
|
|
|
|
# Accepts: class names, objects with resource_map(), or plain hashrefs |
|
152
|
|
|
|
|
|
|
sub add { |
|
153
|
48
|
|
|
48
|
1
|
2628
|
my ($self, @providers) = @_; |
|
154
|
48
|
|
|
|
|
1243
|
my $map = $self->resource_map; |
|
155
|
|
|
|
|
|
|
|
|
156
|
48
|
|
|
|
|
214
|
for my $provider (@providers) { |
|
157
|
49
|
|
|
|
|
118
|
my $ext_map; |
|
158
|
49
|
100
|
|
|
|
223
|
if (ref $provider eq 'HASH') { |
|
159
|
5
|
|
|
|
|
10
|
$ext_map = $provider; |
|
160
|
|
|
|
|
|
|
} else { |
|
161
|
44
|
100
|
|
|
|
142
|
my $obj = ref $provider ? $provider : do { |
|
162
|
34
|
|
|
|
|
220
|
require_module($provider); $provider->new; |
|
|
34
|
|
|
|
|
1773
|
|
|
163
|
|
|
|
|
|
|
}; |
|
164
|
44
|
|
|
|
|
1129
|
$ext_map = $obj->resource_map; |
|
165
|
|
|
|
|
|
|
} |
|
166
|
|
|
|
|
|
|
|
|
167
|
49
|
|
|
|
|
311
|
for my $kind (keys %$ext_map) { |
|
168
|
299
|
|
|
|
|
1395
|
my $class_path = $ext_map->{$kind}; |
|
169
|
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
# Resolve full class name for api_version lookup |
|
171
|
299
|
100
|
|
|
|
929
|
my $full_class = $class_path =~ /^\+/ |
|
172
|
|
|
|
|
|
|
? substr($class_path, 1) : "IO::K8s::$class_path"; |
|
173
|
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
# Try to load the class and get its api_version |
|
175
|
299
|
|
|
|
|
501
|
my $api_version; |
|
176
|
299
|
50
|
33
|
|
|
595
|
if (_class_exists($full_class) && $full_class->can('api_version')) { |
|
177
|
299
|
|
|
|
|
1198
|
$api_version = $full_class->api_version; |
|
178
|
|
|
|
|
|
|
} |
|
179
|
|
|
|
|
|
|
|
|
180
|
299
|
100
|
|
|
|
777
|
if (exists $map->{$kind}) { |
|
181
|
|
|
|
|
|
|
# COLLISION: short name already taken |
|
182
|
|
|
|
|
|
|
# Ensure the original entry also has a domain-qualified key |
|
183
|
17
|
|
|
|
|
36
|
my $orig_path = $map->{$kind}; |
|
184
|
17
|
50
|
|
|
|
78
|
my $orig_class = $orig_path =~ /^\+/ |
|
185
|
|
|
|
|
|
|
? substr($orig_path, 1) : "IO::K8s::$orig_path"; |
|
186
|
17
|
50
|
33
|
|
|
44
|
if (_class_exists($orig_class) && $orig_class->can('api_version')) { |
|
187
|
17
|
|
|
|
|
80
|
my $orig_av = $orig_class->api_version; |
|
188
|
17
|
100
|
66
|
|
|
107
|
if ($orig_av && !exists $map->{"$orig_av/$kind"}) { |
|
189
|
14
|
|
|
|
|
55
|
$map->{"$orig_av/$kind"} = $orig_path; |
|
190
|
|
|
|
|
|
|
} |
|
191
|
|
|
|
|
|
|
} |
|
192
|
|
|
|
|
|
|
# New entry: domain-qualified only (no short name) |
|
193
|
17
|
50
|
|
|
|
45
|
if ($api_version) { |
|
194
|
17
|
|
|
|
|
89
|
$map->{"$api_version/$kind"} = $class_path; |
|
195
|
|
|
|
|
|
|
} |
|
196
|
|
|
|
|
|
|
} else { |
|
197
|
|
|
|
|
|
|
# No collision: register short name |
|
198
|
282
|
|
|
|
|
640
|
$map->{$kind} = $class_path; |
|
199
|
|
|
|
|
|
|
# Also register domain-qualified |
|
200
|
282
|
50
|
|
|
|
665
|
if ($api_version) { |
|
201
|
282
|
|
|
|
|
1249
|
$map->{"$api_version/$kind"} = $class_path; |
|
202
|
|
|
|
|
|
|
} |
|
203
|
|
|
|
|
|
|
} |
|
204
|
|
|
|
|
|
|
} |
|
205
|
|
|
|
|
|
|
} |
|
206
|
48
|
|
|
|
|
353
|
return $self; |
|
207
|
|
|
|
|
|
|
} |
|
208
|
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
# Expand short class name to full class path |
|
210
|
|
|
|
|
|
|
# Supports: |
|
211
|
|
|
|
|
|
|
# 'Pod' -> lookup in resource_map -> IO::K8s::Api::Core::V1::Pod |
|
212
|
|
|
|
|
|
|
# 'Api::Core::V1::Pod' -> IO::K8s::Api::Core::V1::Pod |
|
213
|
|
|
|
|
|
|
# 'IO::K8s::...' -> returned as-is |
|
214
|
|
|
|
|
|
|
# '+MyApp::K8s::Resource' -> MyApp::K8s::Resource (+ prefix = full class name) |
|
215
|
|
|
|
|
|
|
# |
|
216
|
|
|
|
|
|
|
# Search order: |
|
217
|
|
|
|
|
|
|
# 1. User's class_namespaces (if class exists) |
|
218
|
|
|
|
|
|
|
# 2. IO::K8s built-in (resource_map or relative path) |
|
219
|
|
|
|
|
|
|
# 3. Auto-generate from openapi_spec (if available) |
|
220
|
|
|
|
|
|
|
sub expand_class { |
|
221
|
1894
|
|
|
1894
|
0
|
112008
|
my ($self, $class, $api_version) = @_; |
|
222
|
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
# +FullClassName - strip + and use as-is |
|
224
|
1894
|
50
|
|
|
|
7004
|
return substr($class, 1) if $class =~ /^\+/; |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
# Already a full IO::K8s class name - return as-is |
|
227
|
1894
|
100
|
|
|
|
7779
|
return $class if $class =~ /^IO::K8s::/; |
|
228
|
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
# Already a loaded class (e.g. CRD class passed by ref) - return as-is |
|
230
|
405
|
100
|
|
|
|
1317
|
return $class if _class_exists($class); |
|
231
|
|
|
|
|
|
|
|
|
232
|
394
|
50
|
|
|
|
17785
|
my $map = ref($self) ? $self->resource_map : \%DEFAULT_RESOURCE_MAP; |
|
233
|
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
# Domain-qualified string: 'cilium.io/v2/NetworkPolicy' |
|
235
|
394
|
100
|
|
|
|
5102
|
if ($class =~ m{/}) { |
|
236
|
71
|
100
|
|
|
|
327
|
if (my $mapped = $map->{$class}) { |
|
237
|
70
|
100
|
|
|
|
672
|
return $mapped =~ /^\+/ ? substr($mapped, 1) : "IO::K8s::$mapped"; |
|
238
|
|
|
|
|
|
|
} |
|
239
|
1
|
|
|
|
|
6
|
return undef; |
|
240
|
|
|
|
|
|
|
} |
|
241
|
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
# Short name + api_version disambiguation: 'NetworkPolicy' + 'cilium.io/v2' |
|
243
|
323
|
100
|
|
|
|
1172
|
if ($api_version) { |
|
244
|
91
|
|
|
|
|
265
|
my $qualified = "$api_version/$class"; |
|
245
|
91
|
100
|
|
|
|
446
|
if (my $mapped = $map->{$qualified}) { |
|
246
|
29
|
100
|
|
|
|
194
|
return $mapped =~ /^\+/ ? substr($mapped, 1) : "IO::K8s::$mapped"; |
|
247
|
|
|
|
|
|
|
} |
|
248
|
|
|
|
|
|
|
} |
|
249
|
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
# Short name like "Pod" - look up in resource_map |
|
251
|
294
|
50
|
|
|
|
1450
|
if (my $mapped = $map->{$class}) { |
|
252
|
|
|
|
|
|
|
# Mapped value with + prefix = full class name |
|
253
|
294
|
100
|
|
|
|
1148
|
return substr($mapped, 1) if $mapped =~ /^\+/; |
|
254
|
|
|
|
|
|
|
|
|
255
|
281
|
|
|
|
|
615
|
my $rel_path = $mapped; |
|
256
|
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
# 1. Check user's class_namespaces first |
|
258
|
281
|
50
|
|
|
|
813
|
if (ref($self)) { |
|
259
|
281
|
|
|
|
|
539
|
for my $ns (@{$self->class_namespaces}) { |
|
|
281
|
|
|
|
|
1448
|
|
|
260
|
1
|
|
|
|
|
2
|
my $user_class = "${ns}::${rel_path}"; |
|
261
|
1
|
50
|
|
|
|
3
|
return $user_class if _class_exists($user_class); |
|
262
|
|
|
|
|
|
|
} |
|
263
|
|
|
|
|
|
|
} |
|
264
|
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
# 2. Check IO::K8s built-in |
|
266
|
280
|
|
|
|
|
880
|
my $builtin_class = 'IO::K8s::' . $rel_path; |
|
267
|
280
|
50
|
|
|
|
768
|
return $builtin_class if _class_exists($builtin_class); |
|
268
|
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
# 3. Try auto-generation if we have openapi_spec |
|
270
|
0
|
0
|
0
|
|
|
0
|
if (ref($self) && $self->has_openapi_spec) { |
|
271
|
0
|
|
|
|
|
0
|
my $autogen = $self->_autogen_class_for($class); |
|
272
|
0
|
0
|
|
|
|
0
|
return $autogen if $autogen; |
|
273
|
|
|
|
|
|
|
} |
|
274
|
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
# Fall back to IO::K8s:: path (might not exist, but let load_class handle error) |
|
276
|
0
|
|
|
|
|
0
|
return $builtin_class; |
|
277
|
|
|
|
|
|
|
} |
|
278
|
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
# Not in resource_map - might be a CRD or relative path |
|
280
|
|
|
|
|
|
|
# 1. Check user's class_namespaces |
|
281
|
0
|
0
|
|
|
|
0
|
if (ref($self)) { |
|
282
|
0
|
|
|
|
|
0
|
for my $ns (@{$self->class_namespaces}) { |
|
|
0
|
|
|
|
|
0
|
|
|
283
|
0
|
|
|
|
|
0
|
my $user_class = "${ns}::${class}"; |
|
284
|
0
|
0
|
|
|
|
0
|
return $user_class if _class_exists($user_class); |
|
285
|
|
|
|
|
|
|
} |
|
286
|
|
|
|
|
|
|
} |
|
287
|
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
# 2. Check IO::K8s relative path |
|
289
|
0
|
|
|
|
|
0
|
my $builtin_class = 'IO::K8s::' . $class; |
|
290
|
0
|
0
|
|
|
|
0
|
return $builtin_class if _class_exists($builtin_class); |
|
291
|
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
# 3. Try auto-generation for unknown types |
|
293
|
0
|
0
|
0
|
|
|
0
|
if (ref($self) && $self->has_openapi_spec) { |
|
294
|
0
|
|
|
|
|
0
|
my $autogen = $self->_autogen_class_for($class); |
|
295
|
0
|
0
|
|
|
|
0
|
return $autogen if $autogen; |
|
296
|
|
|
|
|
|
|
} |
|
297
|
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
# Fall back |
|
299
|
0
|
|
|
|
|
0
|
return $builtin_class; |
|
300
|
|
|
|
|
|
|
} |
|
301
|
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
# Check if a class exists (is loaded or can be loaded) |
|
303
|
|
|
|
|
|
|
sub _class_exists { |
|
304
|
1002
|
|
|
1002
|
|
2235
|
my ($class) = @_; |
|
305
|
|
|
|
|
|
|
# Check if already loaded |
|
306
|
1002
|
100
|
|
|
|
13535
|
return 1 if $class->can('new'); |
|
307
|
|
|
|
|
|
|
# Try to load it |
|
308
|
486
|
|
|
|
|
1084
|
eval { require_module($class) }; |
|
|
486
|
|
|
|
|
2187
|
|
|
309
|
486
|
|
|
|
|
109275
|
return !$@; |
|
310
|
|
|
|
|
|
|
} |
|
311
|
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
# Auto-generate a class from OpenAPI spec for unknown type |
|
313
|
|
|
|
|
|
|
sub _autogen_class_for { |
|
314
|
0
|
|
|
0
|
|
0
|
my ($self, $kind) = @_; |
|
315
|
|
|
|
|
|
|
|
|
316
|
0
|
0
|
|
|
|
0
|
return unless $self->has_openapi_spec; |
|
317
|
|
|
|
|
|
|
|
|
318
|
0
|
|
|
|
|
0
|
my $spec = $self->openapi_spec; |
|
319
|
0
|
|
0
|
|
|
0
|
my $defs = $spec->{definitions} // {}; |
|
320
|
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
# Find the definition for this kind |
|
322
|
0
|
|
|
|
|
0
|
my $def_name = $self->_find_definition_for_kind($kind, $defs); |
|
323
|
0
|
0
|
|
|
|
0
|
return unless $def_name; |
|
324
|
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
# Check cache first |
|
326
|
0
|
|
|
|
|
0
|
my $cache_key = $self->_autogen_namespace . '::' . $def_name; |
|
327
|
0
|
0
|
|
|
|
0
|
return $_autogen_cache{$cache_key} if $_autogen_cache{$cache_key}; |
|
328
|
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
# Generate the class |
|
330
|
0
|
|
|
|
|
0
|
require IO::K8s::AutoGen; |
|
331
|
|
|
|
|
|
|
my $class = IO::K8s::AutoGen::get_or_generate( |
|
332
|
|
|
|
|
|
|
$def_name, |
|
333
|
0
|
|
|
|
|
0
|
$defs->{$def_name}, |
|
334
|
|
|
|
|
|
|
$defs, |
|
335
|
|
|
|
|
|
|
$self->_autogen_namespace, |
|
336
|
|
|
|
|
|
|
); |
|
337
|
|
|
|
|
|
|
|
|
338
|
0
|
|
|
|
|
0
|
$_autogen_cache{$cache_key} = $class; |
|
339
|
0
|
|
|
|
|
0
|
return $class; |
|
340
|
|
|
|
|
|
|
} |
|
341
|
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
# Find OpenAPI definition name for a given kind |
|
343
|
|
|
|
|
|
|
sub _find_definition_for_kind { |
|
344
|
0
|
|
|
0
|
|
0
|
my ($self, $kind, $defs) = @_; |
|
345
|
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
# Direct match by kind name at end of definition |
|
347
|
0
|
|
|
|
|
0
|
for my $def_name (keys %$defs) { |
|
348
|
0
|
|
|
|
|
0
|
my $def = $defs->{$def_name}; |
|
349
|
|
|
|
|
|
|
# Check x-kubernetes-group-version-kind |
|
350
|
0
|
0
|
|
|
|
0
|
if (my $gvk_list = $def->{'x-kubernetes-group-version-kind'}) { |
|
351
|
0
|
|
|
|
|
0
|
for my $gvk (@$gvk_list) { |
|
352
|
0
|
0
|
|
|
|
0
|
if ($gvk->{kind} eq $kind) { |
|
353
|
0
|
|
|
|
|
0
|
return $def_name; |
|
354
|
|
|
|
|
|
|
} |
|
355
|
|
|
|
|
|
|
} |
|
356
|
|
|
|
|
|
|
} |
|
357
|
|
|
|
|
|
|
# Also check if definition name ends with the kind |
|
358
|
0
|
0
|
|
|
|
0
|
if ($def_name =~ /\.\Q$kind\E$/) { |
|
359
|
0
|
|
|
|
|
0
|
return $def_name; |
|
360
|
|
|
|
|
|
|
} |
|
361
|
|
|
|
|
|
|
} |
|
362
|
|
|
|
|
|
|
|
|
363
|
0
|
|
|
|
|
0
|
return undef; |
|
364
|
|
|
|
|
|
|
} |
|
365
|
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
sub load_class { |
|
367
|
1564
|
|
|
1564
|
0
|
3468
|
my ($self, $class) = @_; |
|
368
|
1564
|
|
|
|
|
5868
|
require_module $class; |
|
369
|
|
|
|
|
|
|
} |
|
370
|
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
sub json_to_object { |
|
372
|
9
|
|
|
9
|
1
|
19450
|
my ($self, $class_or_json, $json) = @_; |
|
373
|
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
# If only one argument, auto-detect class from kind |
|
375
|
9
|
100
|
|
|
|
36
|
if (!defined $json) { |
|
376
|
7
|
|
|
|
|
26
|
return $self->inflate($class_or_json); |
|
377
|
|
|
|
|
|
|
} |
|
378
|
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
# Two arguments: class and JSON |
|
380
|
2
|
|
|
|
|
9
|
my $class = $self->expand_class($class_or_json); |
|
381
|
2
|
|
|
|
|
352
|
my $struct = $self->json->decode($json); |
|
382
|
2
|
|
|
|
|
11
|
return $self->struct_to_object($class, $struct); |
|
383
|
|
|
|
|
|
|
} |
|
384
|
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
sub struct_to_object { |
|
386
|
1502
|
|
|
1502
|
1
|
163800
|
my ($self, $class_or_struct, $params) = @_; |
|
387
|
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
# If only one argument (a hashref), auto-detect class from kind |
|
389
|
1502
|
50
|
33
|
|
|
5009
|
if (!defined $params && ref($class_or_struct) eq 'HASH') { |
|
390
|
0
|
|
|
|
|
0
|
return $self->inflate($class_or_struct); |
|
391
|
|
|
|
|
|
|
} |
|
392
|
|
|
|
|
|
|
|
|
393
|
|
|
|
|
|
|
# Two arguments: class and params |
|
394
|
1502
|
|
|
|
|
4743
|
my $class = $self->expand_class($class_or_struct); |
|
395
|
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
# Already an object of the right class — pass through as-is |
|
397
|
1502
|
100
|
66
|
|
|
4995
|
return $params if Scalar::Util::blessed($params) && $params->isa($class); |
|
398
|
|
|
|
|
|
|
|
|
399
|
1484
|
|
|
|
|
4412
|
$self->load_class($class); |
|
400
|
1484
|
|
|
|
|
45667
|
my $inflated = $self->_inflate_struct($class, $params); |
|
401
|
1478
|
|
|
|
|
42383
|
return $class->new(%$inflated); |
|
402
|
|
|
|
|
|
|
} |
|
403
|
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
sub inflate { |
|
405
|
80
|
|
|
80
|
1
|
2729176
|
my ($self, $data) = @_; |
|
406
|
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
# Accept both JSON string and hashref |
|
408
|
80
|
100
|
|
|
|
1001
|
my $struct = ref($data) eq 'HASH' ? $data : $self->json->decode($data); |
|
409
|
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
my $kind = $struct->{kind} |
|
411
|
80
|
50
|
|
|
|
457
|
or die "Cannot inflate: missing 'kind' field in data"; |
|
412
|
80
|
|
|
|
|
252
|
my $api_version = $struct->{apiVersion}; |
|
413
|
|
|
|
|
|
|
|
|
414
|
80
|
|
|
|
|
444
|
my $class = $self->expand_class($kind, $api_version); |
|
415
|
80
|
|
|
|
|
448
|
$self->load_class($class); |
|
416
|
80
|
|
|
|
|
3156
|
my $inflated = $self->_inflate_struct($class, $struct); |
|
417
|
77
|
|
|
|
|
2264
|
return $class->new(%$inflated); |
|
418
|
|
|
|
|
|
|
} |
|
419
|
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
sub new_object { |
|
421
|
155
|
|
|
155
|
1
|
1278553
|
my ($self, $short_class, @args) = @_; |
|
422
|
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
# Support: |
|
424
|
|
|
|
|
|
|
# ->new_object('Pod', { ... }) |
|
425
|
|
|
|
|
|
|
# ->new_object('Pod', foo => 'bar') |
|
426
|
|
|
|
|
|
|
# ->new_object('Pod', { ... }, 'cilium.io/v2') # with api_version |
|
427
|
155
|
|
|
|
|
405
|
my ($params, $api_version); |
|
428
|
155
|
100
|
100
|
|
|
1992
|
if (@args >= 2 && ref($args[0]) eq 'HASH' && !ref($args[1])) { |
|
|
|
100
|
66
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
429
|
4
|
|
|
|
|
13
|
($params, $api_version) = @args; |
|
430
|
|
|
|
|
|
|
} elsif (@args == 1 && ref($args[0]) eq 'HASH') { |
|
431
|
126
|
|
|
|
|
325
|
$params = $args[0]; |
|
432
|
|
|
|
|
|
|
} else { |
|
433
|
25
|
|
|
|
|
108
|
$params = { @args }; |
|
434
|
|
|
|
|
|
|
} |
|
435
|
|
|
|
|
|
|
|
|
436
|
155
|
|
|
|
|
983
|
my $class = $self->expand_class($short_class, $api_version); |
|
437
|
155
|
|
|
|
|
739
|
return $self->struct_to_object($class, $params); |
|
438
|
|
|
|
|
|
|
} |
|
439
|
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
sub _inflate_struct { |
|
441
|
1564
|
|
|
1564
|
|
4575
|
my ($self, $class, $params) = @_; |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
# Blessed objects should be caught by struct_to_object before reaching |
|
444
|
|
|
|
|
|
|
# here. If one does slip through (defensive), extract its data rather |
|
445
|
|
|
|
|
|
|
# than silently returning {} which would create an empty object. |
|
446
|
1564
|
50
|
|
|
|
4724
|
if (Scalar::Util::blessed($params)) { |
|
447
|
0
|
0
|
|
|
|
0
|
return $params->TO_JSON if $params->can('TO_JSON'); |
|
448
|
0
|
|
|
|
|
0
|
return {}; |
|
449
|
|
|
|
|
|
|
} |
|
450
|
|
|
|
|
|
|
|
|
451
|
1564
|
50
|
|
|
|
5145
|
return {} unless ref $params eq 'HASH'; |
|
452
|
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
# Opaque fields that should be passed through as-is (complex JSON structures) |
|
454
|
1564
|
|
|
|
|
3680
|
my %opaque_fields = map { $_ => 1 } qw(fieldsV1 rawExtension raw); |
|
|
4692
|
|
|
|
|
13161
|
|
|
455
|
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
# Get attribute info from the registry (keyed by Perl attr name) |
|
457
|
1564
|
|
|
|
|
11684
|
my $attr_info = $class->_k8s_attr_info; |
|
458
|
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
# Build reverse map: JSON key → Perl attr name (for sanitized names) |
|
460
|
1564
|
|
|
|
|
2931
|
my %json_to_perl; |
|
461
|
1564
|
|
|
|
|
11896
|
for my $perl_name (keys %$attr_info) { |
|
462
|
15592
|
|
33
|
|
|
51291
|
my $json_key = $attr_info->{$perl_name}{json_key} // $perl_name; |
|
463
|
15592
|
|
|
|
|
34165
|
$json_to_perl{$json_key} = $perl_name; |
|
464
|
|
|
|
|
|
|
} |
|
465
|
|
|
|
|
|
|
|
|
466
|
1564
|
|
|
|
|
4984
|
my %args; |
|
467
|
|
|
|
|
|
|
|
|
468
|
1564
|
|
|
|
|
4797
|
for my $attr (keys %$params) { |
|
469
|
3496
|
|
|
|
|
613056
|
my $value = $params->{$attr}; |
|
470
|
3496
|
50
|
|
|
|
8006
|
next unless defined $value; |
|
471
|
|
|
|
|
|
|
|
|
472
|
|
|
|
|
|
|
# Pass through opaque fields without type coercion |
|
473
|
3496
|
50
|
|
|
|
7930
|
if ($opaque_fields{$attr}) { |
|
474
|
0
|
|
|
|
|
0
|
$args{$attr} = $value; |
|
475
|
0
|
|
|
|
|
0
|
next; |
|
476
|
|
|
|
|
|
|
} |
|
477
|
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
# Look up by Perl attr name (handles sanitized JSON keys like x-kubernetes-*) |
|
479
|
3496
|
|
66
|
|
|
8670
|
my $perl_name = $json_to_perl{$attr} // $attr; |
|
480
|
3496
|
|
100
|
|
|
8495
|
my $info = $attr_info->{$perl_name} // {}; |
|
481
|
|
|
|
|
|
|
|
|
482
|
3496
|
100
|
|
|
|
14008
|
if ($info->{is_array_of_objects}) { |
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
483
|
299
|
|
|
|
|
877
|
my $inner_class = $info->{class}; |
|
484
|
299
|
|
|
|
|
874
|
$args{$attr} = [ map { $self->struct_to_object($inner_class, $_) } @$value ]; |
|
|
419
|
|
|
|
|
204923
|
|
|
485
|
|
|
|
|
|
|
} elsif ($info->{is_hash_of_objects}) { |
|
486
|
2
|
|
|
|
|
6
|
my $inner_class = $info->{class}; |
|
487
|
2
|
|
|
|
|
6
|
$args{$attr} = { map { $_ => $self->struct_to_object($inner_class, $value->{$_}) } keys %$value }; |
|
|
3
|
|
|
|
|
9
|
|
|
488
|
|
|
|
|
|
|
} elsif ($info->{is_object}) { |
|
489
|
892
|
|
|
|
|
3412
|
$args{$attr} = $self->struct_to_object($info->{class}, $value); |
|
490
|
|
|
|
|
|
|
} elsif ($info->{is_bool}) { |
|
491
|
79
|
100
|
66
|
|
|
698
|
$args{$attr} = (ref($value) eq '' && lc($value) eq 'true') || $value ? 1 : 0; |
|
492
|
|
|
|
|
|
|
} else { |
|
493
|
2224
|
|
|
|
|
6536
|
$args{$attr} = $value; |
|
494
|
|
|
|
|
|
|
} |
|
495
|
|
|
|
|
|
|
} |
|
496
|
|
|
|
|
|
|
|
|
497
|
1555
|
|
|
|
|
643732
|
return \%args; |
|
498
|
|
|
|
|
|
|
} |
|
499
|
|
|
|
|
|
|
|
|
500
|
|
|
|
|
|
|
sub object_to_struct { |
|
501
|
0
|
|
|
0
|
1
|
0
|
my ($self, $object) = @_; |
|
502
|
0
|
|
|
|
|
0
|
return $object->TO_JSON; |
|
503
|
|
|
|
|
|
|
} |
|
504
|
|
|
|
|
|
|
|
|
505
|
|
|
|
|
|
|
sub object_to_json { |
|
506
|
24
|
|
|
24
|
1
|
95976
|
my ($self, $object) = @_; |
|
507
|
24
|
|
|
|
|
187
|
return $object->to_json; |
|
508
|
|
|
|
|
|
|
} |
|
509
|
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
sub load { |
|
511
|
7
|
|
|
7
|
1
|
17129
|
my ($self, $file) = @_; |
|
512
|
|
|
|
|
|
|
|
|
513
|
7
|
|
|
|
|
8595
|
require IO::K8s::Manifest; |
|
514
|
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
# Set k8s instance for DSL functions |
|
516
|
7
|
|
|
|
|
34
|
local $IO::K8s::Manifest::_k8s_instance = $self; |
|
517
|
|
|
|
|
|
|
|
|
518
|
7
|
|
|
|
|
35
|
return IO::K8s::Manifest->_load_file($file, $self); |
|
519
|
|
|
|
|
|
|
} |
|
520
|
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
sub load_yaml { |
|
522
|
6
|
|
|
6
|
1
|
57896
|
my ($self, $file_or_string, %opts) = @_; |
|
523
|
|
|
|
|
|
|
|
|
524
|
6
|
|
|
|
|
808
|
require YAML::PP; |
|
525
|
|
|
|
|
|
|
|
|
526
|
6
|
|
|
|
|
98797
|
my $content; |
|
527
|
6
|
100
|
66
|
|
|
90
|
if ($file_or_string !~ /\n/ && -f $file_or_string) { |
|
528
|
|
|
|
|
|
|
# It's a file path |
|
529
|
1
|
50
|
|
|
|
46
|
open my $fh, '<', $file_or_string or die "Cannot open $file_or_string: $!"; |
|
530
|
1
|
|
|
|
|
4
|
$content = do { local $/; <$fh> }; |
|
|
1
|
|
|
|
|
6
|
|
|
|
1
|
|
|
|
|
40
|
|
|
531
|
1
|
|
|
|
|
15
|
close $fh; |
|
532
|
|
|
|
|
|
|
} else { |
|
533
|
|
|
|
|
|
|
# It's YAML content |
|
534
|
5
|
|
|
|
|
16
|
$content = $file_or_string; |
|
535
|
|
|
|
|
|
|
} |
|
536
|
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
# Parse multi-document YAML (Load returns all docs in list context) |
|
538
|
6
|
|
|
|
|
72
|
my @docs = YAML::PP::Load($content); |
|
539
|
|
|
|
|
|
|
|
|
540
|
6
|
|
|
|
|
169197
|
my $collect_errors = $opts{collect_errors}; |
|
541
|
6
|
|
|
|
|
26
|
my @objects; |
|
542
|
|
|
|
|
|
|
my @errors; |
|
543
|
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
# Inflate each document - this validates types! |
|
545
|
6
|
|
|
|
|
36
|
for my $i (0 .. $#docs) { |
|
546
|
11
|
|
|
|
|
873
|
my $doc = $docs[$i]; |
|
547
|
11
|
50
|
33
|
|
|
123
|
next unless $doc && ref($doc) eq 'HASH'; |
|
548
|
|
|
|
|
|
|
|
|
549
|
11
|
100
|
|
|
|
44
|
if ($collect_errors) { |
|
550
|
2
|
|
|
|
|
6
|
eval { push @objects, $self->inflate($doc) }; |
|
|
2
|
|
|
|
|
14
|
|
|
551
|
2
|
50
|
|
|
|
707
|
if ($@) { |
|
552
|
2
|
|
33
|
|
|
22
|
my $name = $doc->{metadata}{name} // "document $i"; |
|
553
|
2
|
|
50
|
|
|
7
|
my $kind = $doc->{kind} // 'unknown'; |
|
554
|
2
|
|
|
|
|
14
|
push @errors, "$kind/$name: $@"; |
|
555
|
|
|
|
|
|
|
} |
|
556
|
|
|
|
|
|
|
} else { |
|
557
|
9
|
|
|
|
|
51
|
push @objects, $self->inflate($doc); |
|
558
|
|
|
|
|
|
|
} |
|
559
|
|
|
|
|
|
|
} |
|
560
|
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
# In collect_errors mode, return (objects, errors) in list context |
|
562
|
5
|
100
|
|
|
|
8532
|
if ($collect_errors) { |
|
563
|
1
|
|
|
|
|
18
|
return (\@objects, \@errors); |
|
564
|
|
|
|
|
|
|
} |
|
565
|
|
|
|
|
|
|
|
|
566
|
4
|
|
|
|
|
67
|
return \@objects; |
|
567
|
|
|
|
|
|
|
} |
|
568
|
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
1; |
|
570
|
|
|
|
|
|
|
|
|
571
|
|
|
|
|
|
|
__END__ |