File Coverage

blib/lib/IO/K8s/Role/NetworkPolicy.pm
Criterion Covered Total %
statement 89 173 51.4
branch 25 72 34.7
condition 17 53 32.0
subroutine 14 21 66.6
pod 0 9 0.0
total 145 328 44.2


line stmt bran cond sub pod time code
1             package IO::K8s::Role::NetworkPolicy;
2             # ABSTRACT: Role for building network policies (core K8s and Cilium)
3             our $VERSION = '1.008';
4 9     9   6599 use Moo::Role;
  9         26  
  9         73  
5 9     9   10777 use IO::K8s::Types::Net qw( cidr_contains );
  9         46  
  9         1062  
6 9     9   87 use Carp qw(croak);
  9         23  
  9         27317  
7              
8             requires '_netpol_format';
9              
10             sub select_pods {
11 3     3 0 371207 my ($self, %labels) = @_;
12 3         22 my $format = $self->_netpol_format;
13              
14 3 50       19 if ($format eq 'core') {
    50          
15 0         0 $self->_ensure_spec;
16 0         0 $self->spec->podSelector(
17             IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::LabelSelector->new(
18             matchLabels => \%labels,
19             )
20             );
21             } elsif ($format eq 'cilium') {
22 3   50     95 my $spec = $self->spec // {};
23 3         48 $spec->{endpointSelector} = { matchLabels => \%labels };
24 3         105 $self->spec($spec);
25             }
26 3         118 return $self;
27             }
28              
29             sub allow_ingress_from_pods {
30 2     2 0 5909 my ($self, $labels, %opts) = @_;
31 2         11 my $format = $self->_netpol_format;
32              
33 2 50       13 if ($format eq 'core') {
    50          
34             $self->_add_core_ingress_rule(
35             { podSelector => { matchLabels => $labels } },
36             $opts{ports},
37 0         0 );
38             } elsif ($format eq 'cilium') {
39             $self->_add_cilium_ingress_rule(
40             { matchLabels => $labels },
41             $opts{ports},
42 2         15 );
43             }
44 2         66 return $self;
45             }
46              
47             sub allow_ingress_from_cidrs {
48 2     2 0 13275 my ($self, $cidrs, %opts) = @_;
49 2         11 _validate_cidrs($cidrs);
50 1         1169 my $format = $self->_netpol_format;
51              
52 1 50       12 if ($format eq 'core') {
    50          
53 0         0 my @from = map { { ipBlock => { cidr => $_ } } } @$cidrs;
  0         0  
54 0         0 $self->_add_core_ingress_rule_multi(\@from, $opts{ports});
55             } elsif ($format eq 'cilium') {
56 1   50     57 my $spec = $self->spec // {};
57 1   50     18 my $ingress = $spec->{ingress} //= [];
58             push @$ingress, {
59             fromCIDR => $cidrs,
60 1 50       8 $opts{ports} ? (toPorts => [{ ports => $opts{ports} }]) : (),
61             };
62 1         25 $self->spec($spec);
63             }
64 1         37 return $self;
65             }
66              
67             sub allow_ingress_from_namespace {
68 1     1 0 4937 my ($self, $namespace, %opts) = @_;
69 1         6 my $format = $self->_netpol_format;
70              
71 1 50       8 if ($format eq 'core') {
    50          
72             $self->_add_core_ingress_rule(
73             { namespaceSelector => { matchLabels => { 'kubernetes.io/metadata.name' => $namespace } } },
74             $opts{ports},
75 0         0 );
76             } elsif ($format eq 'cilium') {
77 1   50     34 my $spec = $self->spec // {};
78 1   50     17 my $ingress = $spec->{ingress} //= [];
79             push @$ingress, {
80             fromEndpoints => [{ matchLabels => { 'k8s:io.kubernetes.pod.namespace' => $namespace } }],
81 1 50       11 $opts{ports} ? (toPorts => [{ ports => $opts{ports} }]) : (),
82             };
83 1         28 $self->spec($spec);
84             }
85 1         32 return $self;
86             }
87              
88             sub allow_egress_to_pods {
89 1     1 0 4556 my ($self, $labels, %opts) = @_;
90 1         6 my $format = $self->_netpol_format;
91              
92 1 50       7 if ($format eq 'core') {
    50          
93             $self->_add_core_egress_rule(
94             { podSelector => { matchLabels => $labels } },
95             $opts{ports},
96 0         0 );
97             } elsif ($format eq 'cilium') {
98 1   50     35 my $spec = $self->spec // {};
99 1   50     15 my $egress = $spec->{egress} //= [];
100             push @$egress, {
101             toEndpoints => [{ matchLabels => $labels }],
102 1 50       89 $opts{ports} ? (toPorts => [{ ports => $opts{ports} }]) : (),
103             };
104 1         39 $self->spec($spec);
105             }
106 1         36 return $self;
107             }
108              
109             sub allow_egress_to_cidrs {
110 2     2 0 6630 my ($self, $cidrs) = @_;
111 2         11 _validate_cidrs($cidrs);
112 1         1240 my $format = $self->_netpol_format;
113              
114 1 50       8 if ($format eq 'core') {
    50          
115             $self->_add_core_egress_rule_multi(
116 0         0 [ map { { ipBlock => { cidr => $_ } } } @$cidrs ],
  0         0  
117             );
118             } elsif ($format eq 'cilium') {
119 1   50     68 my $spec = $self->spec // {};
120 1   50     20 my $egress = $spec->{egress} //= [];
121 1         5 push @$egress, { toCIDR => $cidrs };
122 1         31 $self->spec($spec);
123             }
124 1         36 return $self;
125             }
126              
127             sub allow_egress_to_dns {
128 2     2 0 4857 my ($self) = @_;
129 2         11 my $dns_ports = [
130             { port => 53, protocol => 'UDP' },
131             { port => 53, protocol => 'TCP' },
132             ];
133 2         11 my $format = $self->_netpol_format;
134              
135 2 50       40 if ($format eq 'core') {
    50          
136 0         0 $self->_add_core_egress_rule(undef, $dns_ports);
137             } elsif ($format eq 'cilium') {
138 2   50     62 my $spec = $self->spec // {};
139 2   50     32 my $egress = $spec->{egress} //= [];
140 2         18 push @$egress, {
141             toEndpoints => [{ matchLabels => { 'k8s:io.kubernetes.pod.namespace' => 'kube-system', 'k8s:k8s-app' => 'kube-dns' } }],
142             toPorts => [{ ports => $dns_ports }],
143             };
144 2         50 $self->spec($spec);
145             }
146 2         64 return $self;
147             }
148              
149             sub deny_all_ingress {
150 2     2 0 5615 my ($self) = @_;
151 2         9 my $format = $self->_netpol_format;
152              
153 2 50       25 if ($format eq 'core') {
    50          
154 0         0 $self->_ensure_spec;
155 0         0 $self->_ensure_policy_types('Ingress');
156             # Empty ingress array = deny all
157 0         0 $self->spec->ingress([]);
158             } elsif ($format eq 'cilium') {
159 2   50     52 my $spec = $self->spec // {};
160 2         20 $spec->{ingress} = [];
161 2         7 $spec->{ingressDeny} = [{}];
162 2         45 $self->spec($spec);
163             }
164 2         60 return $self;
165             }
166              
167             sub deny_all_egress {
168 1     1 0 5365 my ($self) = @_;
169 1         6 my $format = $self->_netpol_format;
170              
171 1 50       7 if ($format eq 'core') {
    50          
172 0         0 $self->_ensure_spec;
173 0         0 $self->_ensure_policy_types('Egress');
174 0         0 $self->spec->egress([]);
175             } elsif ($format eq 'cilium') {
176 1   50     30 my $spec = $self->spec // {};
177 1         11 $spec->{egress} = [];
178 1         3 $spec->{egressDeny} = [{}];
179 1         26 $self->spec($spec);
180             }
181 1         32 return $self;
182             }
183              
184             # --- Private helpers ---
185              
186             sub _validate_cidrs {
187 4     4   11 my ($cidrs) = @_;
188 4         37 require IO::K8s::Types::Net;
189 4         15 for my $cidr (@$cidrs) {
190 5 100 66     1833 croak "'$cidr' is not valid CIDR notation"
191             unless $cidr =~ /\// && defined Net::IP->new($cidr);
192             }
193             }
194              
195             # Core K8s helpers (work on typed spec objects)
196             sub _ensure_spec {
197 0     0   0 my ($self) = @_;
198 0 0       0 unless ($self->spec) {
199 0 0       0 if ($self->_netpol_format eq 'core') {
200 0         0 require IO::K8s::Api::Networking::V1::NetworkPolicySpec;
201 0         0 $self->spec(IO::K8s::Api::Networking::V1::NetworkPolicySpec->new(
202             podSelector => IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::LabelSelector->new,
203             ));
204             }
205             }
206             }
207              
208             sub _ensure_policy_types {
209 0     0   0 my ($self, $type) = @_;
210 0 0       0 return unless $self->_netpol_format eq 'core';
211 0         0 my $spec = $self->spec;
212 0   0     0 my $types = $spec->policyTypes // [];
213 0 0       0 unless (grep { $_ eq $type } @$types) {
  0         0  
214 0         0 push @$types, $type;
215 0         0 $spec->policyTypes($types);
216             }
217             }
218              
219             sub _core_ports {
220 0     0   0 my ($ports) = @_;
221 0 0       0 return () unless $ports;
222             return (ports => [
223             map {
224 0         0 IO::K8s::Api::Networking::V1::NetworkPolicyPort->new(
225             port => $_->{port},
226 0   0     0 protocol => $_->{protocol} // 'TCP',
227             )
228             } @$ports
229             ]);
230             }
231              
232             sub _add_core_ingress_rule {
233 0     0   0 my ($self, $from, $ports) = @_;
234 0         0 $self->_ensure_spec;
235 0         0 $self->_ensure_policy_types('Ingress');
236 0         0 my $spec = $self->spec;
237 0   0     0 my $ingress = $spec->ingress // [];
238              
239 0         0 my %rule;
240             $rule{from} = [
241 0 0       0 IO::K8s::Api::Networking::V1::NetworkPolicyPeer->new(%$from)
242             ] if $from;
243 0 0       0 if ($ports) {
244             $rule{ports} = [
245             map {
246 0         0 IO::K8s::Api::Networking::V1::NetworkPolicyPort->new(
247 0   0     0 port => $_->{port}, protocol => $_->{protocol} // 'TCP',
248             )
249             } @$ports
250             ];
251             }
252              
253 0         0 push @$ingress, IO::K8s::Api::Networking::V1::NetworkPolicyIngressRule->new(%rule);
254 0         0 $spec->ingress($ingress);
255             }
256              
257             sub _add_core_ingress_rule_multi {
258 0     0   0 my ($self, $from_list, $ports) = @_;
259 0         0 $self->_ensure_spec;
260 0         0 $self->_ensure_policy_types('Ingress');
261 0         0 my $spec = $self->spec;
262 0   0     0 my $ingress = $spec->ingress // [];
263              
264 0         0 my %rule;
265             $rule{from} = [
266 0 0       0 map { IO::K8s::Api::Networking::V1::NetworkPolicyPeer->new(%$_) } @$from_list
  0         0  
267             ] if $from_list;
268 0 0       0 if ($ports) {
269             $rule{ports} = [
270             map {
271 0         0 IO::K8s::Api::Networking::V1::NetworkPolicyPort->new(
272 0   0     0 port => $_->{port}, protocol => $_->{protocol} // 'TCP',
273             )
274             } @$ports
275             ];
276             }
277              
278 0         0 push @$ingress, IO::K8s::Api::Networking::V1::NetworkPolicyIngressRule->new(%rule);
279 0         0 $spec->ingress($ingress);
280             }
281              
282             sub _add_core_egress_rule {
283 0     0   0 my ($self, $to, $ports) = @_;
284 0         0 $self->_ensure_spec;
285 0         0 $self->_ensure_policy_types('Egress');
286 0         0 my $spec = $self->spec;
287 0   0     0 my $egress = $spec->egress // [];
288              
289 0         0 my %rule;
290             $rule{to} = [
291 0 0       0 IO::K8s::Api::Networking::V1::NetworkPolicyPeer->new(%$to)
292             ] if $to;
293 0 0       0 if ($ports) {
294             $rule{ports} = [
295             map {
296 0         0 IO::K8s::Api::Networking::V1::NetworkPolicyPort->new(
297 0   0     0 port => $_->{port}, protocol => $_->{protocol} // 'TCP',
298             )
299             } @$ports
300             ];
301             }
302              
303 0         0 push @$egress, IO::K8s::Api::Networking::V1::NetworkPolicyEgressRule->new(%rule);
304 0         0 $spec->egress($egress);
305             }
306              
307             sub _add_core_egress_rule_multi {
308 0     0   0 my ($self, $to_list, $ports) = @_;
309 0         0 $self->_ensure_spec;
310 0         0 $self->_ensure_policy_types('Egress');
311 0         0 my $spec = $self->spec;
312 0   0     0 my $egress = $spec->egress // [];
313              
314 0         0 my %rule;
315             $rule{to} = [
316 0 0       0 map { IO::K8s::Api::Networking::V1::NetworkPolicyPeer->new(%$_) } @$to_list
  0         0  
317             ] if $to_list;
318 0 0       0 if ($ports) {
319             $rule{ports} = [
320             map {
321 0         0 IO::K8s::Api::Networking::V1::NetworkPolicyPort->new(
322 0   0     0 port => $_->{port}, protocol => $_->{protocol} // 'TCP',
323             )
324             } @$ports
325             ];
326             }
327              
328 0         0 push @$egress, IO::K8s::Api::Networking::V1::NetworkPolicyEgressRule->new(%rule);
329 0         0 $spec->egress($egress);
330             }
331              
332             sub _add_cilium_ingress_rule {
333 2     2   9 my ($self, $endpoint_selector, $ports) = @_;
334 2   50     56 my $spec = $self->spec // {};
335 2   50     32 my $ingress = $spec->{ingress} //= [];
336 2 100       17 push @$ingress, {
337             fromEndpoints => [$endpoint_selector],
338             $ports ? (toPorts => [{ ports => $ports }]) : (),
339             };
340 2         51 $self->spec($spec);
341             }
342              
343             1;
344              
345             __END__