1use std::{
7 collections::{BTreeMap, BTreeSet, HashSet},
8 fmt::Write as _,
9};
10
11use semver::Version;
12use serde::{Deserialize, de};
13use serde_json::{Map, Value, json};
14
15use crate::settings::{MountIntent, SecretGrant, SecretMode};
16use crate::{
17 DeploymentConnectionReference, DeploymentOperation, DeploymentPlan, DeploymentResource, DeploymentResourceId,
18 Diagnostic, DiagnosticCode, ExternalPrecondition, HostAlias, NetworkAttachment, NetworkIntent, NetworkRoute,
19 PortMapping, PortProtocol, ResourceKind, RestartPolicy, RouteType, SensitiveInputReference,
20};
21
22const RENDERING_CATALOGUE_JSON: &str = include_str!("../catalogue/v1/podman-deployment-rendering.json");
23const RENDERED_OPERATION_CATEGORIES: [&str; 8] = [
24 "network-create",
25 "volume-create",
26 "secret-create",
27 "image-pull",
28 "pod-create",
29 "container-create",
30 "pod-start",
31 "container-start",
32];
33
34#[derive(Deserialize)]
35#[serde(deny_unknown_fields)]
36struct RenderingCatalogue {
37 schema_version: u8,
38 provenance: String,
39 runtime_field_claims: Vec<RuntimeFieldClaimJson>,
40 reviewed_lines: Vec<ReviewedRenderingLine>,
41}
42
43#[derive(Deserialize)]
44#[serde(deny_unknown_fields)]
45struct ReviewedRenderingLine {
46 version: String,
47 revision: String,
48 tag: String,
49 common_module: ModulePin,
50 b4_evidence: ReviewedB4Evidence,
51 runtime_evidence: ReviewedRuntimeEvidence,
52 operations: Vec<ReviewedRenderingOperation>,
53 field_evidence: Vec<ReviewedFieldEvidence>,
54}
55
56#[derive(Deserialize)]
61#[serde(deny_unknown_fields)]
62struct ReviewedB4Evidence {
63 exact_fields: Vec<B4RenderedField>,
64 target_gated_fields: Vec<B4RenderedField>,
65 manual_fields: Vec<B4RenderedField>,
66 blocked_fields: Vec<B4RenderedField>,
67 cli_sources: Vec<SourceReference>,
68 model_sources: Vec<SourceReference>,
69 handler_sources: Vec<SourceReference>,
70}
71
72#[derive(Deserialize)]
78#[serde(deny_unknown_fields)]
79struct ReviewedRuntimeEvidence {
80 exact_fields: Vec<RuntimeRenderedField>,
81 target_gated_fields: Vec<RuntimeRenderedField>,
82 cli_flag_source: SourceReference,
83 cli_transform_source: SourceReference,
84 command_route_source: SourceReference,
85 model_sources: Vec<SourceReference>,
86 route_source: SourceReference,
87 handler_source: SourceReference,
88}
89
90#[derive(Deserialize)]
91#[serde(deny_unknown_fields)]
92struct RuntimeFieldClaimJson {
93 field: RuntimeRenderedField,
94 cli: CliFieldClaim,
95 libpod: RuntimeLibpodFieldClaim,
96}
97
98#[derive(Deserialize)]
99#[serde(deny_unknown_fields)]
100struct RuntimeLibpodFieldClaim {
101 json_member: LibpodBodyMember,
102 value_shape: RuntimeLibpodValueShape,
103}
104
105#[derive(Deserialize)]
106#[serde(deny_unknown_fields)]
107struct ReviewedRenderingOperation {
108 category: RenderingOperationCategory,
109 cli_source: SourceReference,
110 libpod_endpoint_source: SourceReference,
111 body_source: Option<SourceReference>,
112}
113
114#[derive(Deserialize)]
115#[serde(deny_unknown_fields)]
116struct ReviewedFieldEvidence {
117 field: RenderedField,
118 availability: FieldAvailability,
119 operation: RenderingOperationCategory,
120 cli: CliFieldClaim,
121 libpod: LibpodFieldClaim,
122 cli_source: SourceReference,
123 model_sources: Vec<SourceReference>,
124 handler_source: SourceReference,
125}
126
127#[derive(Clone, Copy, Deserialize, Eq, PartialEq)]
128#[serde(rename_all = "kebab-case")]
129enum FieldAvailability {
130 Exact,
131 Unsupported,
132}
133
134#[derive(Deserialize, Eq, PartialEq)]
141#[serde(deny_unknown_fields)]
142struct SourceReference {
143 repository: SourceRepository,
144 revision: String,
145 path: String,
146 module: Option<ModulePin>,
147}
148
149#[derive(Clone, Copy, Deserialize, Eq, PartialEq)]
150#[serde(rename_all = "kebab-case")]
151enum SourceRepository {
152 ContainersPodman,
153 ContainersCommon,
154 ContainersContainerLibs,
155 PodmanContainerToolsContainerLibs,
156}
157
158#[derive(Deserialize, Eq, PartialEq)]
159#[serde(deny_unknown_fields)]
160struct ModulePin {
161 repository: SourceRepository,
162 path: String,
163 version: String,
164 revision: String,
165}
166
167#[derive(Deserialize)]
168#[serde(deny_unknown_fields)]
169struct CliFieldClaim {
170 flag: Option<CliFlag>,
171 value_shape: CliValueShape,
172}
173
174#[derive(Deserialize)]
175#[serde(deny_unknown_fields)]
176struct LibpodFieldClaim {
177 json_member: LibpodBodyMember,
178}
179
180#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
181#[serde(rename_all = "kebab-case")]
182enum RenderingOperationCategory {
183 NetworkCreate,
184 VolumeCreate,
185 SecretCreate,
186 ImagePull,
187 PodCreate,
188 ContainerCreate,
189 PodStart,
190 ContainerStart,
191}
192
193impl RenderingOperationCategory {
194 const fn as_str(self) -> &'static str {
195 match self {
196 Self::NetworkCreate => "network-create",
197 Self::VolumeCreate => "volume-create",
198 Self::SecretCreate => "secret-create",
199 Self::ImagePull => "image-pull",
200 Self::PodCreate => "pod-create",
201 Self::ContainerCreate => "container-create",
202 Self::PodStart => "pod-start",
203 Self::ContainerStart => "container-start",
204 }
205 }
206}
207
208#[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
209#[serde(rename_all = "kebab-case")]
210enum RenderedField {
211 ContainerCommand,
212 ContainerEntrypoint,
213 ContainerUser,
214 ContainerWorkdir,
215 ContainerHostname,
216 ContainerLabel,
217 ContainerEnvironment,
218 ContainerRestartPolicy,
219 ContainerNamedVolumeMount,
220 ContainerNetworkAttachment,
221 PodNetworkAttachment,
222 ContainerNetworkAlias,
223 PodNetworkAlias,
224 ContainerNetworkStaticIpv4,
225 PodNetworkStaticIpv4,
226 ContainerNetworkStaticIpv6,
227 PodNetworkStaticIpv6,
228 ContainerNetworkStaticMac,
229 PodNetworkStaticMac,
230 ContainerPortMapping,
231 PodPortMapping,
232 ContainerDnsServers,
233 ContainerDnsSearch,
234 ContainerDnsOptions,
235 PodDnsServers,
236 PodDnsSearch,
237 PodDnsOptions,
238 ContainerHostAlias,
239 PodHostAlias,
240 ContainerNetworkOrder,
241 NetworkIpamSubnet,
242 NetworkIpamGateway,
243 NetworkIpamRange,
244 NetworkRouteDestination,
245 NetworkRouteGateway,
246 NetworkRouteMetric,
247 NetworkRouteTypeUnicast,
248 NetworkRouteTypeBlackhole,
249 NetworkRouteTypeUnreachable,
250 NetworkRouteTypeProhibit,
251}
252
253#[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
255#[serde(rename_all = "kebab-case")]
256#[allow(clippy::enum_variant_names)] enum RuntimeRenderedField {
258 ContainerHealthDisabled,
259 ContainerHealthCommand,
260 ContainerHealthInterval,
261 ContainerHealthTimeout,
262 ContainerHealthRetries,
263 ContainerHealthStartPeriod,
264 ContainerHealthOnFailure,
265 ContainerStartupHealthCommand,
266 ContainerStartupHealthInterval,
267 ContainerStartupHealthTimeout,
268 ContainerStartupHealthRetries,
269 ContainerStartupHealthSuccesses,
270 ContainerLogDriver,
271 ContainerLogMaxSize,
272 ContainerLogJournaldLabels,
273 ContainerPrivileged,
274 ContainerCapabilityAdd,
275 ContainerCapabilityDrop,
276 ContainerNoNewPrivileges,
277 ContainerReadOnlyFilesystem,
278 ContainerReadWriteTmpfs,
279 ContainerPidNamespace,
280 ContainerIpcNamespace,
281 ContainerUtsNamespace,
282 ContainerCgroupNamespace,
283 ContainerCpuShares,
284 ContainerCpuPeriod,
285 ContainerCpuQuota,
286 ContainerMemoryLimit,
287 ContainerPidsLimit,
288 ContainerRlimitFinite,
289 ContainerRlimitUnlimited,
290}
291
292#[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
294#[serde(rename_all = "kebab-case")]
295#[allow(clippy::enum_variant_names)] enum B4RenderedField {
297 ContainerNamedVolumeCopyMount,
298 ContainerNamedVolumeSubpathCopyMount,
299 ContainerNamedVolumeNoCopyMount,
300 ContainerNamedVolumeSubpathNoCopyMount,
301 ContainerBindMount,
302 ContainerTmpfsMount,
303 ContainerSecretMountGrant,
304 ContainerSecretEnvironmentGrant,
305 VolumeUid,
306 VolumeGid,
307 ImagePullPolicyAlways,
308 ImagePullPolicyMissing,
309 ImagePullPolicyNever,
310 ImagePullPolicyNewer,
311 ImageSourcePortability,
312 PodInfraMount,
313}
314
315#[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
316enum CliFlag {
317 #[serde(rename = "--entrypoint")]
318 Entrypoint,
319 #[serde(rename = "--user")]
320 User,
321 #[serde(rename = "--workdir")]
322 Workdir,
323 #[serde(rename = "--hostname")]
324 Hostname,
325 #[serde(rename = "--label")]
326 Label,
327 #[serde(rename = "--env")]
328 Environment,
329 #[serde(rename = "--restart")]
330 Restart,
331 #[serde(rename = "--volume")]
332 Volume,
333 #[serde(rename = "--network")]
334 Network,
335 #[serde(rename = "--network-alias")]
336 NetworkAlias,
337 #[serde(rename = "--ip")]
338 Ip,
339 #[serde(rename = "--ip6")]
340 Ip6,
341 #[serde(rename = "--mac-address")]
342 MacAddress,
343 #[serde(rename = "--publish")]
344 Publish,
345 #[serde(rename = "--dns")]
346 Dns,
347 #[serde(rename = "--dns-search")]
348 DnsSearch,
349 #[serde(rename = "--dns-option")]
350 DnsOption,
351 #[serde(rename = "--add-host")]
352 AddHost,
353 #[serde(rename = "--subnet")]
354 Subnet,
355 #[serde(rename = "--gateway")]
356 Gateway,
357 #[serde(rename = "--ip-range")]
358 IpRange,
359 #[serde(rename = "--route")]
360 Route,
361 #[serde(rename = "--no-healthcheck")]
362 NoHealthcheck,
363 #[serde(rename = "--health-cmd")]
364 HealthCommand,
365 #[serde(rename = "--health-interval")]
366 HealthInterval,
367 #[serde(rename = "--health-timeout")]
368 HealthTimeout,
369 #[serde(rename = "--health-retries")]
370 HealthRetries,
371 #[serde(rename = "--health-start-period")]
372 HealthStartPeriod,
373 #[serde(rename = "--health-on-failure")]
374 HealthOnFailure,
375 #[serde(rename = "--health-startup-cmd")]
376 HealthStartupCommand,
377 #[serde(rename = "--health-startup-interval")]
378 HealthStartupInterval,
379 #[serde(rename = "--health-startup-timeout")]
380 HealthStartupTimeout,
381 #[serde(rename = "--health-startup-retries")]
382 HealthStartupRetries,
383 #[serde(rename = "--health-startup-success")]
384 HealthStartupSuccess,
385 #[serde(rename = "--log-driver")]
386 LogDriver,
387 #[serde(rename = "--log-opt")]
388 LogOption,
389 #[serde(rename = "--privileged")]
390 Privileged,
391 #[serde(rename = "--cap-add")]
392 CapabilityAdd,
393 #[serde(rename = "--cap-drop")]
394 CapabilityDrop,
395 #[serde(rename = "--security-opt")]
396 SecurityOption,
397 #[serde(rename = "--read-only")]
398 ReadOnly,
399 #[serde(rename = "--read-only-tmpfs")]
400 ReadOnlyTmpfs,
401 #[serde(rename = "--pid")]
402 PidNamespace,
403 #[serde(rename = "--ipc")]
404 IpcNamespace,
405 #[serde(rename = "--uts")]
406 UtsNamespace,
407 #[serde(rename = "--cgroupns")]
408 CgroupNamespace,
409 #[serde(rename = "--cpu-shares")]
410 CpuShares,
411 #[serde(rename = "--cpu-period")]
412 CpuPeriod,
413 #[serde(rename = "--cpu-quota")]
414 CpuQuota,
415 #[serde(rename = "--memory")]
416 Memory,
417 #[serde(rename = "--pids-limit")]
418 PidsLimit,
419 #[serde(rename = "--ulimit")]
420 Ulimit,
421}
422
423#[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
424#[serde(rename_all = "kebab-case")]
425enum CliValueShape {
426 ArgumentArray,
427 ContainerUser,
428 AbsoluteContainerPath,
429 Hostname,
430 LabelAssignment,
431 EnvironmentAssignment,
432 RestartPolicy,
433 NamedVolumeMount,
434 NetworkAttachment,
435 NetworkAlias,
436 StaticIp,
437 StaticMac,
438 PortMapping,
439 DnsServer,
440 DnsSearch,
441 DnsOption,
442 HostAlias,
443 NetworkOrder,
444 NetworkSubnet,
445 NetworkGateway,
446 NetworkIpRange,
447 NetworkRoute,
448 FlagOnly,
449 Boolean,
450 HealthCommand,
451 HealthInterval,
452 HealthDuration,
453 HealthRetries,
454 HealthOnFailure,
455 StartupHealthSuccesses,
456 LogDriver,
457 LogMaxSize,
458 JournaldLabel,
459 Capability,
460 NoNewPrivileges,
461 NamespaceMode,
462 CpuShares,
463 CpuPeriod,
464 CpuQuota,
465 MemoryBytes,
466 PidsLimit,
467 Rlimit,
468}
469
470#[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
471#[serde(rename_all = "snake_case")]
472enum LibpodBodyMember {
473 Command,
474 Entrypoint,
475 User,
476 WorkDir,
477 Hostname,
478 Labels,
479 Env,
480 RestartPolicy,
481 Volumes,
482 #[serde(rename = "Networks")]
483 Networks,
484 Portmappings,
485 DnsServer,
486 DnsSearch,
487 DnsOption,
488 Hostadd,
489 #[serde(rename = "networkOrder")]
490 NetworkOrder,
491 Subnets,
492 Routes,
493 Healthconfig,
494 HealthCheckOnFailureAction,
495 #[serde(rename = "startupHealthConfig")]
496 StartupHealthConfig,
497 #[serde(rename = "log_configuration.driver")]
498 LogConfigurationDriver,
499 #[serde(rename = "log_configuration.size")]
500 LogConfigurationSize,
501 #[serde(rename = "log_configuration.labels")]
502 LogConfigurationLabels,
503 Privileged,
504 CapAdd,
505 CapDrop,
506 NoNewPrivileges,
507 ReadOnlyFilesystem,
508 ReadWriteTmpfs,
509 Pidns,
510 Ipcns,
511 Utsns,
512 Cgroupns,
513 #[serde(rename = "resource_limits.cpu.shares")]
514 ResourceLimitsCpuShares,
515 #[serde(rename = "resource_limits.cpu.period")]
516 ResourceLimitsCpuPeriod,
517 #[serde(rename = "resource_limits.cpu.quota")]
518 ResourceLimitsCpuQuota,
519 #[serde(rename = "resource_limits.memory.limit")]
520 ResourceLimitsMemoryLimit,
521 #[serde(rename = "resource_limits.pids.limit")]
522 ResourceLimitsPidsLimit,
523 RLimits,
524}
525
526#[derive(Clone, Copy, Deserialize, Eq, PartialEq)]
527#[serde(rename_all = "kebab-case")]
528enum RuntimeLibpodValueShape {
529 HealthConfiguration,
530 HealthOnFailureAction,
531 StartupHealthConfiguration,
532 String,
533 Integer,
534 StringMap,
535 Boolean,
536 StringArray,
537 Namespace,
538 ResourceLimitInteger,
539 RlimitArray,
540}
541
542#[derive(Clone, Copy, Debug, Eq, PartialEq)]
544#[non_exhaustive]
545pub enum RenderStatus {
546 Exact,
548 DeferredSensitiveInput,
550 Manual,
552 Approximate,
554 Unsupported,
556}
557
558#[derive(Clone, Debug, Eq, PartialEq)]
560pub struct RenderingFinding {
561 code: DiagnosticCode,
562 subject: Option<DeploymentResourceId>,
563 field: Option<&'static str>,
564}
565
566impl Ord for RenderingFinding {
567 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
568 (self.code.as_str(), &self.subject, self.field).cmp(&(other.code.as_str(), &other.subject, other.field))
569 }
570}
571
572impl PartialOrd for RenderingFinding {
573 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
574 Some(self.cmp(other))
575 }
576}
577
578impl RenderingFinding {
579 fn new(code: DiagnosticCode, subject: Option<DeploymentResourceId>, field: Option<&'static str>) -> Self {
580 Self { code, subject, field }
581 }
582
583 #[must_use]
585 pub const fn code(&self) -> DiagnosticCode {
586 self.code
587 }
588
589 #[must_use]
591 pub const fn message(&self) -> &'static str {
592 Diagnostic::new(self.code).message()
593 }
594
595 #[must_use]
597 pub fn subject(&self) -> Option<&DeploymentResourceId> {
598 self.subject.as_ref()
599 }
600
601 #[must_use]
603 pub const fn field(&self) -> Option<&'static str> {
604 self.field
605 }
606}
607
608#[derive(Clone, Debug, Eq, PartialEq)]
610pub struct CliInvocation {
611 program: &'static str,
612 argv: Vec<String>,
613 external_input: Option<SensitiveInputReference>,
614}
615
616impl CliInvocation {
617 fn new(argv: Vec<String>, external_input: Option<SensitiveInputReference>) -> Self {
618 Self {
619 program: "podman",
620 argv,
621 external_input,
622 }
623 }
624
625 #[must_use]
627 pub const fn program(&self) -> &'static str {
628 self.program
629 }
630
631 #[must_use]
633 pub fn argv(&self) -> &[String] {
634 &self.argv
635 }
636
637 #[must_use]
639 pub fn external_input(&self) -> Option<&SensitiveInputReference> {
640 self.external_input.as_ref()
641 }
642}
643
644#[derive(Clone, Copy, Debug, Eq, PartialEq)]
646pub enum RenderedHttpMethod {
647 Get,
649 Post,
651}
652
653#[derive(Clone, Debug, PartialEq)]
655pub enum RenderedHttpBody {
656 Empty,
658 Json(Value),
660 ExternalSensitiveInput(SensitiveInputReference),
662}
663
664#[derive(Clone, Debug, PartialEq)]
666pub struct LibpodInvocation {
667 method: RenderedHttpMethod,
668 path_and_query: String,
669 body: RenderedHttpBody,
670}
671
672impl LibpodInvocation {
673 fn new(method: RenderedHttpMethod, path_and_query: String, body: RenderedHttpBody) -> Self {
674 Self {
675 method,
676 path_and_query,
677 body,
678 }
679 }
680
681 #[must_use]
683 pub const fn method(&self) -> RenderedHttpMethod {
684 self.method
685 }
686
687 #[must_use]
689 pub fn path_and_query(&self) -> &str {
690 &self.path_and_query
691 }
692
693 #[must_use]
695 pub fn body(&self) -> &RenderedHttpBody {
696 &self.body
697 }
698}
699
700#[derive(Clone, Debug, PartialEq)]
702pub struct RenderedOperation {
703 operation: DeploymentOperation,
704 status: RenderStatus,
705 cli: CliInvocation,
706 libpod: LibpodInvocation,
707}
708
709impl RenderedOperation {
710 #[must_use]
712 pub fn operation(&self) -> &DeploymentOperation {
713 &self.operation
714 }
715
716 #[must_use]
718 pub const fn status(&self) -> RenderStatus {
719 self.status
720 }
721
722 #[must_use]
724 pub fn cli(&self) -> &CliInvocation {
725 &self.cli
726 }
727
728 #[must_use]
730 pub fn libpod(&self) -> &LibpodInvocation {
731 &self.libpod
732 }
733}
734
735#[derive(Clone, Debug, PartialEq)]
737pub struct DeploymentRendering {
738 status: RenderStatus,
739 connection: Option<DeploymentConnectionReference>,
740 external_preconditions: Vec<ExternalPrecondition>,
741 operations: Vec<RenderedOperation>,
742}
743
744impl DeploymentRendering {
745 #[must_use]
747 pub const fn status(&self) -> RenderStatus {
748 self.status
749 }
750
751 #[must_use]
756 pub fn connection(&self) -> Option<&DeploymentConnectionReference> {
757 self.connection.as_ref()
758 }
759
760 #[must_use]
762 pub fn external_preconditions(&self) -> &[ExternalPrecondition] {
763 &self.external_preconditions
764 }
765
766 #[must_use]
768 pub fn operations(&self) -> &[RenderedOperation] {
769 &self.operations
770 }
771
772 #[must_use]
779 pub fn shell_script(&self) -> String {
780 let mut script =
781 String::from("#!/bin/sh\n# Review generated Podman commands before running this file.\nset -eu\n");
782 for precondition in &self.external_preconditions {
783 let _ = writeln!(
784 script,
785 "# Requires external {}: {}",
786 resource_kind_name(precondition.identity().kind()),
787 shell_quote(precondition.identity().name())
788 );
789 }
790 let mut secret_number = 0_usize;
791 for operation in &self.operations {
792 if operation.cli.external_input().is_some() {
793 secret_number += 1;
794 let _ = writeln!(
795 script,
796 ": \"${{PODMAN_LENS_SECRET_INPUT_{secret_number}:?set to a readable secret file path}}\""
797 );
798 }
799 }
800 secret_number = 0;
801 for operation in &self.operations {
802 script.push_str("podman");
803 for value in operation.cli.argv() {
804 script.push(' ');
805 script.push_str(&shell_quote(value));
806 }
807 if operation.cli.external_input().is_some() {
808 secret_number += 1;
809 let _ = write!(script, " < \"${{PODMAN_LENS_SECRET_INPUT_{secret_number}}}\"");
810 }
811 script.push('\n');
812 }
813 script
814 }
815}
816
817#[derive(Clone, Debug, PartialEq)]
819pub struct RenderingOutcome {
820 rendering: Option<DeploymentRendering>,
821 findings: Vec<RenderingFinding>,
822}
823
824impl RenderingOutcome {
825 #[must_use]
827 pub fn rendering(&self) -> Option<&DeploymentRendering> {
828 self.rendering.as_ref()
829 }
830
831 #[must_use]
833 pub fn findings(&self) -> &[RenderingFinding] {
834 &self.findings
835 }
836
837 #[must_use]
839 pub const fn is_success(&self) -> bool {
840 self.rendering.is_some()
841 }
842}
843
844#[must_use]
850pub fn render_deployment(plan: &DeploymentPlan) -> RenderingOutcome {
851 let Some(version) = reviewed_renderer_target_version(plan) else {
852 return RenderingOutcome {
853 rendering: None,
854 findings: vec![RenderingFinding::new(
855 DiagnosticCode::RenderingTargetMismatch,
856 None,
857 Some("target"),
858 )],
859 };
860 };
861 let connection = plan.connection().map(DeploymentConnectionReference::as_str);
862 let managed_image_sources = managed_image_sources(plan);
863 let mut deferred = false;
864 let mut operations = Vec::with_capacity(plan.operations().len());
865 let mut findings = Vec::new();
866 let mut blocked_resources = BTreeSet::new();
867 for operation in plan.operations() {
868 if blocked_resources.contains(operation.id().resource()) {
869 continue;
870 }
871 let unsupported_fields =
872 unsupported_fields(operation.resource_intent(), &version, plan.target().execution_context());
873 if !unsupported_fields.is_empty() {
874 blocked_resources.insert(operation.id().resource().clone());
875 findings.extend(unsupported_fields.into_iter().map(|field| {
876 RenderingFinding::new(
877 if field == "source.portability" {
878 DiagnosticCode::ImagePortabilityManual
879 } else {
880 DiagnosticCode::RenderingUnsupported
881 },
882 Some(operation.id().resource().clone()),
883 Some(field),
884 )
885 }));
886 continue;
887 }
888 match render_operation(operation, &version, connection, &managed_image_sources) {
889 Ok(rendered) => {
890 deferred |= rendered.status == RenderStatus::DeferredSensitiveInput;
891 operations.push(rendered);
892 }
893 Err(finding) => findings.push(finding),
894 }
895 }
896 findings.sort_unstable();
897 if findings.is_empty() {
898 RenderingOutcome {
899 rendering: Some(DeploymentRendering {
900 status: if deferred {
901 RenderStatus::DeferredSensitiveInput
902 } else {
903 RenderStatus::Exact
904 },
905 connection: plan.connection().cloned(),
906 external_preconditions: plan.external_preconditions().to_vec(),
907 operations,
908 }),
909 findings,
910 }
911 } else {
912 RenderingOutcome {
913 rendering: None,
914 findings,
915 }
916 }
917}
918
919#[allow(clippy::too_many_lines)] fn render_operation(
921 operation: &DeploymentOperation,
922 version: &str,
923 connection: Option<&str>,
924 managed_image_sources: &BTreeMap<DeploymentResourceId, String>,
925) -> Result<RenderedOperation, RenderingFinding> {
926 let id = operation.id().resource();
927 let mut prefix = connection.map_or_else(Vec::new, |value| vec!["--connection".to_owned(), value.to_owned()]);
928 let (status, cli_suffix, method, path, body, input) = match operation.resource_intent() {
929 DeploymentResource::Network(network) => {
930 let mut cli_suffix = vec!["network".to_owned(), "create".to_owned()];
931 append_network_create_arguments(&mut cli_suffix, network);
932 cli_suffix.push(network.identity().name().to_owned());
933 (
934 RenderStatus::Exact,
935 cli_suffix,
936 RenderedHttpMethod::Post,
937 format!("/v{version}/libpod/networks/create"),
938 RenderedHttpBody::Json(network_create_configuration(
939 network,
940 supports_network_route_type(version),
941 )),
942 None,
943 )
944 }
945 DeploymentResource::Volume(volume) => (
946 RenderStatus::Exact,
947 volume_create_cli_arguments(volume),
948 RenderedHttpMethod::Post,
949 format!("/v{version}/libpod/volumes/create"),
950 RenderedHttpBody::Json(volume_create_json(volume)),
951 None,
952 ),
953 DeploymentResource::Secret(secret) => (
954 RenderStatus::DeferredSensitiveInput,
955 vec![
956 "secret".to_owned(),
957 "create".to_owned(),
958 secret.identity().name().to_owned(),
959 "-".to_owned(),
960 ],
961 RenderedHttpMethod::Post,
962 format!(
963 "/v{version}/libpod/secrets/create?name={}",
964 percent_encode(secret.identity().name())
965 ),
966 RenderedHttpBody::ExternalSensitiveInput(secret.material().clone()),
967 Some(secret.material().clone()),
968 ),
969 DeploymentResource::Image(image) => (
970 RenderStatus::Exact,
971 vec![
972 "image".to_owned(),
973 "pull".to_owned(),
974 format!("--policy={}", image.pull_policy().as_str()),
975 image.source().as_str().to_owned(),
976 ],
977 RenderedHttpMethod::Post,
978 format!(
979 "/v{version}/libpod/images/pull?reference={}&policy={}",
980 percent_encode(image.source().as_str()),
981 image.pull_policy().as_str(),
982 ),
983 RenderedHttpBody::Empty,
984 None,
985 ),
986 DeploymentResource::Pod(pod) => {
987 if operation.id().action() == crate::SemanticOperationAction::StartPod {
988 (
989 RenderStatus::Exact,
990 vec!["pod".to_owned(), "start".to_owned(), pod.identity().name().to_owned()],
991 RenderedHttpMethod::Post,
992 format!(
993 "/v{version}/libpod/pods/{}/start",
994 percent_encode(pod.identity().name())
995 ),
996 RenderedHttpBody::Empty,
997 None,
998 )
999 } else {
1000 let networks = network_configuration(pod.networks());
1001 let mut cli_suffix = vec![
1002 "pod".to_owned(),
1003 "create".to_owned(),
1004 "--name".to_owned(),
1005 pod.identity().name().to_owned(),
1006 ];
1007 if !append_network_arguments(&mut cli_suffix, pod.networks(), None) {
1008 return Err(RenderingFinding::new(
1009 DiagnosticCode::RenderingUnsupported,
1010 Some(id.clone()),
1011 Some("networks.cli_ambiguous"),
1012 ));
1013 }
1014 append_port_arguments(&mut cli_suffix, pod.ports());
1015 append_dns_arguments(&mut cli_suffix, pod.dns());
1016 append_host_alias_arguments(&mut cli_suffix, pod.host_aliases());
1017 let mut body = Map::new();
1018 body.insert("name".to_owned(), Value::String(pod.identity().name().to_owned()));
1019 body.insert("Networks".to_owned(), networks);
1020 append_networking_json(&mut body, pod.ports(), pod.dns(), pod.host_aliases(), None);
1021 (
1022 RenderStatus::Exact,
1023 cli_suffix,
1024 RenderedHttpMethod::Post,
1025 format!("/v{version}/libpod/pods/create"),
1026 RenderedHttpBody::Json(Value::Object(body)),
1027 None,
1028 )
1029 }
1030 }
1031 DeploymentResource::Container(container) => {
1032 if operation.id().action() == crate::SemanticOperationAction::StartContainer {
1033 (
1034 RenderStatus::Exact,
1035 vec![
1036 "container".to_owned(),
1037 "start".to_owned(),
1038 container.identity().name().to_owned(),
1039 ],
1040 RenderedHttpMethod::Post,
1041 format!(
1042 "/v{version}/libpod/containers/{}/start",
1043 percent_encode(container.identity().name())
1044 ),
1045 RenderedHttpBody::Empty,
1046 None,
1047 )
1048 } else {
1049 let image = managed_image_sources
1050 .get(container.image())
1051 .map_or_else(|| container.image().name(), String::as_str);
1052 let mut cli_suffix = vec![
1053 "container".to_owned(),
1054 "create".to_owned(),
1055 "--name".to_owned(),
1056 container.identity().name().to_owned(),
1057 "--pull=never".to_owned(),
1058 ];
1059 let mut body = if let Some(pod) = container.pod() {
1060 cli_suffix.push("--pod".to_owned());
1061 cli_suffix.push(pod.name().to_owned());
1062 json!({"image": image, "pod": pod.name()})
1063 } else {
1064 if !append_network_arguments(&mut cli_suffix, container.networks(), container.network_order()) {
1065 return Err(RenderingFinding::new(
1066 DiagnosticCode::RenderingUnsupported,
1067 Some(id.clone()),
1068 Some("networks.cli_ambiguous"),
1069 ));
1070 }
1071 append_port_arguments(&mut cli_suffix, container.ports());
1072 append_dns_arguments(&mut cli_suffix, container.dns());
1073 append_host_alias_arguments(&mut cli_suffix, container.host_aliases());
1074 let mut body = json!({"image": image, "Networks": network_configuration(container.networks())});
1075 let Some(body_map) = body.as_object_mut() else {
1076 return Err(RenderingFinding::new(
1077 DiagnosticCode::RenderingUnsupported,
1078 Some(id.clone()),
1079 Some("container_body"),
1080 ));
1081 };
1082 append_networking_json(
1083 body_map,
1084 container.ports(),
1085 container.dns(),
1086 container.host_aliases(),
1087 container.network_order(),
1088 );
1089 body
1090 };
1091 if !append_mount_arguments(&mut cli_suffix, container.mounts()) {
1092 return Err(RenderingFinding::new(
1093 DiagnosticCode::RenderingUnsupported,
1094 Some(id.clone()),
1095 Some("mounts.cli_ambiguous"),
1096 ));
1097 }
1098 if !append_secret_grants_arguments(&mut cli_suffix, container.secret_grants()) {
1099 return Err(RenderingFinding::new(
1100 DiagnosticCode::RenderingUnsupported,
1101 Some(id.clone()),
1102 Some("secret_grants.cli_ambiguous"),
1103 ));
1104 }
1105 append_container_setting_arguments(&mut cli_suffix, container, id)?;
1106 append_container_runtime_arguments(&mut cli_suffix, container, id, version)?;
1107 let Some(body_map) = body.as_object_mut() else {
1108 return Err(RenderingFinding::new(
1109 DiagnosticCode::RenderingUnsupported,
1110 Some(id.clone()),
1111 Some("container_body"),
1112 ));
1113 };
1114 append_container_setting_json(body_map, container, id)?;
1115 append_secret_grants_json(body_map, container.secret_grants());
1116 append_container_runtime_json(body_map, container, id, version)?;
1117 cli_suffix.push(image.to_owned());
1118 if let Some(command) = container.settings().command() {
1119 cli_suffix.extend(command.values().iter().cloned());
1120 }
1121 (
1122 RenderStatus::Exact,
1123 cli_suffix,
1124 RenderedHttpMethod::Post,
1125 format!(
1126 "/v{version}/libpod/containers/create?name={}",
1127 percent_encode(container.identity().name())
1128 ),
1129 RenderedHttpBody::Json(body),
1130 None,
1131 )
1132 }
1133 }
1134 DeploymentResource::ExternalPrecondition(_) => {
1135 return Err(RenderingFinding::new(
1136 DiagnosticCode::RenderingUnsupported,
1137 Some(id.clone()),
1138 Some("resource"),
1139 ));
1140 }
1141 };
1142 prefix.extend(cli_suffix);
1143 Ok(RenderedOperation {
1144 operation: operation.clone(),
1145 status,
1146 cli: CliInvocation::new(prefix, input),
1147 libpod: LibpodInvocation::new(method, path, body),
1148 })
1149}
1150
1151fn reviewed_renderer_target_version(plan: &DeploymentPlan) -> Option<String> {
1152 let engine = plan.target().podman_version().as_semver();
1153 let api = plan.target().api_version().as_semver();
1154 if engine != api || !engine.build.is_empty() || !api.build.is_empty() {
1155 return None;
1156 }
1157 let canonical = canonical_version(engine);
1158 renderer_catalogue_versions()
1159 .ok()?
1160 .into_iter()
1161 .find(|version| version == &canonical)
1162}
1163
1164fn renderer_catalogue_versions() -> Result<Vec<String>, ()> {
1165 parse_renderer_catalogue(RENDERING_CATALOGUE_JSON)
1166}
1167
1168fn parse_renderer_catalogue(source: &str) -> Result<Vec<String>, ()> {
1169 reject_duplicate_json_keys(source)?;
1170 let catalogue: RenderingCatalogue = serde_json::from_str(source).map_err(|_| ())?;
1171 let expected_operations = RENDERED_OPERATION_CATEGORIES.into_iter().collect::<BTreeSet<_>>();
1172 let capabilities = crate::capability_catalogue()
1173 .map_err(|_| ())?
1174 .into_iter()
1175 .filter(crate::CapabilityCatalogueEntry::output_supported)
1176 .collect::<Vec<_>>();
1177 if catalogue.schema_version != 8
1178 || catalogue.provenance.trim().is_empty()
1179 || catalogue.reviewed_lines.len() != capabilities.len()
1180 || !validated_runtime_field_claims(&catalogue.runtime_field_claims)
1181 {
1182 return Err(());
1183 }
1184 let mut versions = Vec::with_capacity(catalogue.reviewed_lines.len());
1185 for (line, capability) in catalogue.reviewed_lines.into_iter().zip(capabilities) {
1186 let version = Version::parse(&line.version).map_err(|_| ())?;
1187 if !version.pre.is_empty()
1188 || !version.build.is_empty()
1189 || line.version != canonical_version(&version)
1190 || line.version != capability.observed_podman_version()
1191 || line.revision != capability.evidence().revision()
1192 || line.tag != capability.evidence().release_tag()
1193 || !is_lowercase_sha40(&line.revision)
1194 || !valid_common_module(&line.version, &line.common_module)
1195 || !validated_renderer_operations(&line, &expected_operations)
1196 || !validated_b4_evidence(&line)
1197 || !validated_field_evidence(&line)
1198 || !validated_runtime_evidence(&line)
1199 {
1200 return Err(());
1201 }
1202 versions.push(line.version);
1203 }
1204 Ok(versions)
1205}
1206
1207fn validated_b4_evidence(line: &ReviewedRenderingLine) -> bool {
1208 let evidence = &line.b4_evidence;
1209 let exact = evidence.exact_fields.iter().copied().collect::<BTreeSet<_>>();
1210 let target_gated = evidence.target_gated_fields.iter().copied().collect::<BTreeSet<_>>();
1211 let manual = evidence.manual_fields.iter().copied().collect::<BTreeSet<_>>();
1212 let blocked = evidence.blocked_fields.iter().copied().collect::<BTreeSet<_>>();
1213 let expected_target_gated = expected_b4_target_gated_fields(&line.version);
1214 exact.len() == evidence.exact_fields.len()
1215 && target_gated.len() == evidence.target_gated_fields.len()
1216 && manual.len() == evidence.manual_fields.len()
1217 && blocked.len() == evidence.blocked_fields.len()
1218 && exact.is_disjoint(&target_gated)
1219 && exact.is_disjoint(&manual)
1220 && exact.is_disjoint(&blocked)
1221 && target_gated.is_disjoint(&manual)
1222 && target_gated.is_disjoint(&blocked)
1223 && manual.is_disjoint(&blocked)
1224 && exact.union(&target_gated).copied().collect::<BTreeSet<_>>() == expected_b4_rendered_fields()
1225 && target_gated == expected_target_gated
1226 && manual == expected_b4_manual_fields()
1227 && blocked == expected_b4_blocked_fields()
1228 && evidence.cli_sources.len() == 4
1229 && evidence
1230 .cli_sources
1231 .iter()
1232 .zip([
1233 "cmd/podman/common/create.go",
1234 "cmd/podman/containers/create.go",
1235 "cmd/podman/volumes/create.go",
1236 "cmd/podman/images/pull.go",
1237 ])
1238 .all(|(source, path)| source_matches_podman(source, &line.revision, path))
1239 && evidence.model_sources.len() == 3
1240 && evidence
1241 .model_sources
1242 .iter()
1243 .zip([
1244 "pkg/specgenutil/volumes.go",
1245 "pkg/specgen/specgen.go",
1246 "pkg/specgen/volumes.go",
1247 ])
1248 .all(|(source, path)| source_matches_podman(source, &line.revision, path))
1249 && evidence.handler_sources.len() == 3
1250 && evidence
1251 .handler_sources
1252 .iter()
1253 .zip([
1254 "pkg/api/handlers/libpod/containers_create.go",
1255 "pkg/api/handlers/libpod/volumes.go",
1256 "pkg/api/handlers/libpod/images_pull.go",
1257 ])
1258 .all(|(source, path)| source_matches_podman(source, &line.revision, path))
1259}
1260
1261fn expected_b4_rendered_fields() -> BTreeSet<B4RenderedField> {
1262 use B4RenderedField::{
1263 ContainerBindMount, ContainerNamedVolumeCopyMount, ContainerNamedVolumeNoCopyMount,
1264 ContainerNamedVolumeSubpathCopyMount, ContainerSecretEnvironmentGrant, ContainerSecretMountGrant,
1265 ContainerTmpfsMount, ImagePullPolicyAlways, ImagePullPolicyMissing, ImagePullPolicyNever, ImagePullPolicyNewer,
1266 VolumeGid, VolumeUid,
1267 };
1268 [
1269 ContainerNamedVolumeCopyMount,
1270 ContainerNamedVolumeSubpathCopyMount,
1271 ContainerNamedVolumeNoCopyMount,
1272 ContainerBindMount,
1273 ContainerTmpfsMount,
1274 ContainerSecretMountGrant,
1275 ContainerSecretEnvironmentGrant,
1276 VolumeUid,
1277 VolumeGid,
1278 ImagePullPolicyAlways,
1279 ImagePullPolicyMissing,
1280 ImagePullPolicyNever,
1281 ImagePullPolicyNewer,
1282 ]
1283 .into_iter()
1284 .collect()
1285}
1286
1287fn expected_b4_target_gated_fields(version: &str) -> BTreeSet<B4RenderedField> {
1288 if supports_image_policy(version) {
1289 BTreeSet::new()
1290 } else {
1291 use B4RenderedField::{
1292 ImagePullPolicyAlways, ImagePullPolicyMissing, ImagePullPolicyNever, ImagePullPolicyNewer, VolumeGid,
1293 VolumeUid,
1294 };
1295 [
1296 VolumeUid,
1297 VolumeGid,
1298 ImagePullPolicyAlways,
1299 ImagePullPolicyMissing,
1300 ImagePullPolicyNever,
1301 ImagePullPolicyNewer,
1302 ]
1303 .into_iter()
1304 .collect()
1305 }
1306}
1307
1308fn expected_b4_manual_fields() -> BTreeSet<B4RenderedField> {
1309 [B4RenderedField::ImageSourcePortability].into_iter().collect()
1310}
1311
1312fn expected_b4_blocked_fields() -> BTreeSet<B4RenderedField> {
1313 [
1314 B4RenderedField::ContainerNamedVolumeSubpathNoCopyMount,
1315 B4RenderedField::PodInfraMount,
1316 ]
1317 .into_iter()
1318 .collect()
1319}
1320
1321fn validated_runtime_field_claims(claims: &[RuntimeFieldClaimJson]) -> bool {
1322 let expected = expected_runtime_field_claims();
1323 let observed = claims.iter().map(|claim| claim.field).collect::<BTreeSet<_>>();
1324 claims.len() == expected.len()
1325 && observed.len() == claims.len()
1326 && observed == expected.keys().copied().collect()
1327 && claims.iter().all(|claim| {
1328 expected.get(&claim.field).is_some_and(|expected| {
1329 claim.cli.flag == Some(expected.0)
1330 && claim.cli.value_shape == expected.1
1331 && claim.libpod.json_member == expected.2
1332 && claim.libpod.value_shape == expected_runtime_libpod_value_shape(claim.field)
1333 })
1334 })
1335}
1336
1337fn validated_runtime_evidence(line: &ReviewedRenderingLine) -> bool {
1338 let evidence = &line.runtime_evidence;
1339 let exact = evidence.exact_fields.iter().copied().collect::<BTreeSet<_>>();
1340 let target_gated = evidence.target_gated_fields.iter().copied().collect::<BTreeSet<_>>();
1341 let expected_target_gated = expected_runtime_target_gated_fields(&line.version);
1342 exact.len() == evidence.exact_fields.len()
1343 && target_gated.len() == evidence.target_gated_fields.len()
1344 && exact.is_disjoint(&target_gated)
1345 && exact.union(&target_gated).copied().collect::<BTreeSet<_>>() == expected_runtime_fields()
1346 && target_gated == expected_target_gated
1347 && source_matches_podman(&evidence.cli_flag_source, &line.revision, "cmd/podman/common/create.go")
1348 && source_matches_podman(
1349 &evidence.cli_transform_source,
1350 &line.revision,
1351 "pkg/specgenutil/specgen.go",
1352 )
1353 && source_matches_podman(
1354 &evidence.command_route_source,
1355 &line.revision,
1356 "cmd/podman/containers/create.go",
1357 )
1358 && source_matches_podman(
1359 &evidence.route_source,
1360 &line.revision,
1361 "pkg/api/server/register_containers.go",
1362 )
1363 && source_matches_podman(
1364 &evidence.handler_source,
1365 &line.revision,
1366 "pkg/api/handlers/libpod/containers_create.go",
1367 )
1368 && evidence.model_sources.len() == 3
1369 && evidence
1370 .model_sources
1371 .iter()
1372 .zip([
1373 "pkg/specgen/specgen.go",
1374 "pkg/specgen/namespaces.go",
1375 "libpod/define/healthchecks.go",
1376 ])
1377 .all(|(source, path)| source_matches_podman(source, &line.revision, path))
1378 && target_gated.iter().all(|field| {
1379 matches!(
1380 field,
1381 RuntimeRenderedField::ContainerLogJournaldLabels | RuntimeRenderedField::ContainerRlimitUnlimited
1382 )
1383 })
1384}
1385
1386fn expected_runtime_fields() -> BTreeSet<RuntimeRenderedField> {
1387 [
1388 RuntimeRenderedField::ContainerHealthDisabled,
1389 RuntimeRenderedField::ContainerHealthCommand,
1390 RuntimeRenderedField::ContainerHealthInterval,
1391 RuntimeRenderedField::ContainerHealthTimeout,
1392 RuntimeRenderedField::ContainerHealthRetries,
1393 RuntimeRenderedField::ContainerHealthStartPeriod,
1394 RuntimeRenderedField::ContainerHealthOnFailure,
1395 RuntimeRenderedField::ContainerStartupHealthCommand,
1396 RuntimeRenderedField::ContainerStartupHealthInterval,
1397 RuntimeRenderedField::ContainerStartupHealthTimeout,
1398 RuntimeRenderedField::ContainerStartupHealthRetries,
1399 RuntimeRenderedField::ContainerStartupHealthSuccesses,
1400 RuntimeRenderedField::ContainerLogDriver,
1401 RuntimeRenderedField::ContainerLogMaxSize,
1402 RuntimeRenderedField::ContainerLogJournaldLabels,
1403 RuntimeRenderedField::ContainerPrivileged,
1404 RuntimeRenderedField::ContainerCapabilityAdd,
1405 RuntimeRenderedField::ContainerCapabilityDrop,
1406 RuntimeRenderedField::ContainerNoNewPrivileges,
1407 RuntimeRenderedField::ContainerReadOnlyFilesystem,
1408 RuntimeRenderedField::ContainerReadWriteTmpfs,
1409 RuntimeRenderedField::ContainerPidNamespace,
1410 RuntimeRenderedField::ContainerIpcNamespace,
1411 RuntimeRenderedField::ContainerUtsNamespace,
1412 RuntimeRenderedField::ContainerCgroupNamespace,
1413 RuntimeRenderedField::ContainerCpuShares,
1414 RuntimeRenderedField::ContainerCpuPeriod,
1415 RuntimeRenderedField::ContainerCpuQuota,
1416 RuntimeRenderedField::ContainerMemoryLimit,
1417 RuntimeRenderedField::ContainerPidsLimit,
1418 RuntimeRenderedField::ContainerRlimitFinite,
1419 RuntimeRenderedField::ContainerRlimitUnlimited,
1420 ]
1421 .into_iter()
1422 .collect()
1423}
1424
1425fn expected_runtime_target_gated_fields(version: &str) -> BTreeSet<RuntimeRenderedField> {
1426 let mut fields = BTreeSet::new();
1427 if !supports_journald_labels(version) {
1428 fields.insert(RuntimeRenderedField::ContainerLogJournaldLabels);
1429 }
1430 if !supports_unlimited_rlimit(version) {
1431 fields.insert(RuntimeRenderedField::ContainerRlimitUnlimited);
1432 }
1433 fields
1434}
1435
1436type RuntimeFieldClaim = (CliFlag, CliValueShape, LibpodBodyMember);
1437
1438const fn expected_runtime_libpod_value_shape(field: RuntimeRenderedField) -> RuntimeLibpodValueShape {
1439 use RuntimeLibpodValueShape::{
1440 Boolean, HealthConfiguration, HealthOnFailureAction, Integer, Namespace, ResourceLimitInteger, RlimitArray,
1441 StartupHealthConfiguration, String, StringArray, StringMap,
1442 };
1443 use RuntimeRenderedField::{
1444 ContainerCapabilityAdd, ContainerCapabilityDrop, ContainerCgroupNamespace, ContainerCpuPeriod,
1445 ContainerCpuQuota, ContainerCpuShares, ContainerHealthCommand, ContainerHealthDisabled,
1446 ContainerHealthInterval, ContainerHealthOnFailure, ContainerHealthRetries, ContainerHealthStartPeriod,
1447 ContainerHealthTimeout, ContainerIpcNamespace, ContainerLogDriver, ContainerLogJournaldLabels,
1448 ContainerLogMaxSize, ContainerMemoryLimit, ContainerNoNewPrivileges, ContainerPidNamespace, ContainerPidsLimit,
1449 ContainerPrivileged, ContainerReadOnlyFilesystem, ContainerReadWriteTmpfs, ContainerRlimitFinite,
1450 ContainerRlimitUnlimited, ContainerStartupHealthCommand, ContainerStartupHealthInterval,
1451 ContainerStartupHealthRetries, ContainerStartupHealthSuccesses, ContainerStartupHealthTimeout,
1452 ContainerUtsNamespace,
1453 };
1454 match field {
1455 ContainerHealthDisabled
1456 | ContainerHealthCommand
1457 | ContainerHealthInterval
1458 | ContainerHealthTimeout
1459 | ContainerHealthRetries
1460 | ContainerHealthStartPeriod => HealthConfiguration,
1461 ContainerHealthOnFailure => HealthOnFailureAction,
1462 ContainerStartupHealthCommand
1463 | ContainerStartupHealthInterval
1464 | ContainerStartupHealthTimeout
1465 | ContainerStartupHealthRetries
1466 | ContainerStartupHealthSuccesses => StartupHealthConfiguration,
1467 ContainerLogDriver => String,
1468 ContainerLogMaxSize => Integer,
1469 ContainerLogJournaldLabels => StringMap,
1470 ContainerPrivileged | ContainerNoNewPrivileges | ContainerReadOnlyFilesystem | ContainerReadWriteTmpfs => {
1471 Boolean
1472 }
1473 ContainerCapabilityAdd | ContainerCapabilityDrop => StringArray,
1474 ContainerPidNamespace | ContainerIpcNamespace | ContainerUtsNamespace | ContainerCgroupNamespace => Namespace,
1475 ContainerCpuShares | ContainerCpuPeriod | ContainerCpuQuota | ContainerMemoryLimit | ContainerPidsLimit => {
1476 ResourceLimitInteger
1477 }
1478 ContainerRlimitFinite | ContainerRlimitUnlimited => RlimitArray,
1479 }
1480}
1481
1482#[allow(clippy::too_many_lines)] fn expected_runtime_field_claims() -> BTreeMap<RuntimeRenderedField, RuntimeFieldClaim> {
1484 use RuntimeRenderedField::{
1485 ContainerCapabilityAdd, ContainerCapabilityDrop, ContainerCgroupNamespace, ContainerCpuPeriod,
1486 ContainerCpuQuota, ContainerCpuShares, ContainerHealthCommand, ContainerHealthDisabled,
1487 ContainerHealthInterval, ContainerHealthOnFailure, ContainerHealthRetries, ContainerHealthStartPeriod,
1488 ContainerHealthTimeout, ContainerIpcNamespace, ContainerLogDriver, ContainerLogJournaldLabels,
1489 ContainerLogMaxSize, ContainerMemoryLimit, ContainerNoNewPrivileges, ContainerPidNamespace, ContainerPidsLimit,
1490 ContainerPrivileged, ContainerReadOnlyFilesystem, ContainerReadWriteTmpfs, ContainerRlimitFinite,
1491 ContainerRlimitUnlimited, ContainerStartupHealthCommand, ContainerStartupHealthInterval,
1492 ContainerStartupHealthRetries, ContainerStartupHealthSuccesses, ContainerStartupHealthTimeout,
1493 ContainerUtsNamespace,
1494 };
1495 [
1496 (
1497 ContainerHealthDisabled,
1498 (
1499 CliFlag::NoHealthcheck,
1500 CliValueShape::FlagOnly,
1501 LibpodBodyMember::Healthconfig,
1502 ),
1503 ),
1504 (
1505 ContainerHealthCommand,
1506 (
1507 CliFlag::HealthCommand,
1508 CliValueShape::HealthCommand,
1509 LibpodBodyMember::Healthconfig,
1510 ),
1511 ),
1512 (
1513 ContainerHealthInterval,
1514 (
1515 CliFlag::HealthInterval,
1516 CliValueShape::HealthInterval,
1517 LibpodBodyMember::Healthconfig,
1518 ),
1519 ),
1520 (
1521 ContainerHealthTimeout,
1522 (
1523 CliFlag::HealthTimeout,
1524 CliValueShape::HealthDuration,
1525 LibpodBodyMember::Healthconfig,
1526 ),
1527 ),
1528 (
1529 ContainerHealthRetries,
1530 (
1531 CliFlag::HealthRetries,
1532 CliValueShape::HealthRetries,
1533 LibpodBodyMember::Healthconfig,
1534 ),
1535 ),
1536 (
1537 ContainerHealthStartPeriod,
1538 (
1539 CliFlag::HealthStartPeriod,
1540 CliValueShape::HealthDuration,
1541 LibpodBodyMember::Healthconfig,
1542 ),
1543 ),
1544 (
1545 ContainerHealthOnFailure,
1546 (
1547 CliFlag::HealthOnFailure,
1548 CliValueShape::HealthOnFailure,
1549 LibpodBodyMember::HealthCheckOnFailureAction,
1550 ),
1551 ),
1552 (
1553 ContainerStartupHealthCommand,
1554 (
1555 CliFlag::HealthStartupCommand,
1556 CliValueShape::HealthCommand,
1557 LibpodBodyMember::StartupHealthConfig,
1558 ),
1559 ),
1560 (
1561 ContainerStartupHealthInterval,
1562 (
1563 CliFlag::HealthStartupInterval,
1564 CliValueShape::HealthInterval,
1565 LibpodBodyMember::StartupHealthConfig,
1566 ),
1567 ),
1568 (
1569 ContainerStartupHealthTimeout,
1570 (
1571 CliFlag::HealthStartupTimeout,
1572 CliValueShape::HealthDuration,
1573 LibpodBodyMember::StartupHealthConfig,
1574 ),
1575 ),
1576 (
1577 ContainerStartupHealthRetries,
1578 (
1579 CliFlag::HealthStartupRetries,
1580 CliValueShape::HealthRetries,
1581 LibpodBodyMember::StartupHealthConfig,
1582 ),
1583 ),
1584 (
1585 ContainerStartupHealthSuccesses,
1586 (
1587 CliFlag::HealthStartupSuccess,
1588 CliValueShape::StartupHealthSuccesses,
1589 LibpodBodyMember::StartupHealthConfig,
1590 ),
1591 ),
1592 (
1593 ContainerLogDriver,
1594 (
1595 CliFlag::LogDriver,
1596 CliValueShape::LogDriver,
1597 LibpodBodyMember::LogConfigurationDriver,
1598 ),
1599 ),
1600 (
1601 ContainerLogMaxSize,
1602 (
1603 CliFlag::LogOption,
1604 CliValueShape::LogMaxSize,
1605 LibpodBodyMember::LogConfigurationSize,
1606 ),
1607 ),
1608 (
1609 ContainerLogJournaldLabels,
1610 (
1611 CliFlag::LogOption,
1612 CliValueShape::JournaldLabel,
1613 LibpodBodyMember::LogConfigurationLabels,
1614 ),
1615 ),
1616 (
1617 ContainerPrivileged,
1618 (
1619 CliFlag::Privileged,
1620 CliValueShape::Boolean,
1621 LibpodBodyMember::Privileged,
1622 ),
1623 ),
1624 (
1625 ContainerCapabilityAdd,
1626 (
1627 CliFlag::CapabilityAdd,
1628 CliValueShape::Capability,
1629 LibpodBodyMember::CapAdd,
1630 ),
1631 ),
1632 (
1633 ContainerCapabilityDrop,
1634 (
1635 CliFlag::CapabilityDrop,
1636 CliValueShape::Capability,
1637 LibpodBodyMember::CapDrop,
1638 ),
1639 ),
1640 (
1641 ContainerNoNewPrivileges,
1642 (
1643 CliFlag::SecurityOption,
1644 CliValueShape::NoNewPrivileges,
1645 LibpodBodyMember::NoNewPrivileges,
1646 ),
1647 ),
1648 (
1649 ContainerReadOnlyFilesystem,
1650 (
1651 CliFlag::ReadOnly,
1652 CliValueShape::Boolean,
1653 LibpodBodyMember::ReadOnlyFilesystem,
1654 ),
1655 ),
1656 (
1657 ContainerReadWriteTmpfs,
1658 (
1659 CliFlag::ReadOnlyTmpfs,
1660 CliValueShape::Boolean,
1661 LibpodBodyMember::ReadWriteTmpfs,
1662 ),
1663 ),
1664 (
1665 ContainerPidNamespace,
1666 (
1667 CliFlag::PidNamespace,
1668 CliValueShape::NamespaceMode,
1669 LibpodBodyMember::Pidns,
1670 ),
1671 ),
1672 (
1673 ContainerIpcNamespace,
1674 (
1675 CliFlag::IpcNamespace,
1676 CliValueShape::NamespaceMode,
1677 LibpodBodyMember::Ipcns,
1678 ),
1679 ),
1680 (
1681 ContainerUtsNamespace,
1682 (
1683 CliFlag::UtsNamespace,
1684 CliValueShape::NamespaceMode,
1685 LibpodBodyMember::Utsns,
1686 ),
1687 ),
1688 (
1689 ContainerCgroupNamespace,
1690 (
1691 CliFlag::CgroupNamespace,
1692 CliValueShape::NamespaceMode,
1693 LibpodBodyMember::Cgroupns,
1694 ),
1695 ),
1696 (
1697 ContainerCpuShares,
1698 (
1699 CliFlag::CpuShares,
1700 CliValueShape::CpuShares,
1701 LibpodBodyMember::ResourceLimitsCpuShares,
1702 ),
1703 ),
1704 (
1705 ContainerCpuPeriod,
1706 (
1707 CliFlag::CpuPeriod,
1708 CliValueShape::CpuPeriod,
1709 LibpodBodyMember::ResourceLimitsCpuPeriod,
1710 ),
1711 ),
1712 (
1713 ContainerCpuQuota,
1714 (
1715 CliFlag::CpuQuota,
1716 CliValueShape::CpuQuota,
1717 LibpodBodyMember::ResourceLimitsCpuQuota,
1718 ),
1719 ),
1720 (
1721 ContainerMemoryLimit,
1722 (
1723 CliFlag::Memory,
1724 CliValueShape::MemoryBytes,
1725 LibpodBodyMember::ResourceLimitsMemoryLimit,
1726 ),
1727 ),
1728 (
1729 ContainerPidsLimit,
1730 (
1731 CliFlag::PidsLimit,
1732 CliValueShape::PidsLimit,
1733 LibpodBodyMember::ResourceLimitsPidsLimit,
1734 ),
1735 ),
1736 (
1737 ContainerRlimitFinite,
1738 (CliFlag::Ulimit, CliValueShape::Rlimit, LibpodBodyMember::RLimits),
1739 ),
1740 (
1741 ContainerRlimitUnlimited,
1742 (CliFlag::Ulimit, CliValueShape::Rlimit, LibpodBodyMember::RLimits),
1743 ),
1744 ]
1745 .into_iter()
1746 .collect()
1747}
1748
1749fn reject_duplicate_json_keys(source: &str) -> Result<(), ()> {
1750 let mut deserializer = serde_json::Deserializer::from_str(source);
1751 RejectDuplicateJsonKeys::deserialize(&mut deserializer).map_err(|_| ())?;
1752 deserializer.end().map_err(|_| ())
1753}
1754
1755struct RejectDuplicateJsonKeys;
1756
1757impl<'de> Deserialize<'de> for RejectDuplicateJsonKeys {
1758 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1759 where
1760 D: serde::Deserializer<'de>,
1761 {
1762 deserializer.deserialize_any(RejectDuplicateJsonKeysVisitor)
1763 }
1764}
1765
1766struct RejectDuplicateJsonKeysVisitor;
1767
1768impl<'de> de::Visitor<'de> for RejectDuplicateJsonKeysVisitor {
1769 type Value = RejectDuplicateJsonKeys;
1770
1771 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1772 formatter.write_str("a JSON value without duplicate object keys")
1773 }
1774
1775 fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E>
1776 where
1777 E: de::Error,
1778 {
1779 Ok(RejectDuplicateJsonKeys)
1780 }
1781
1782 fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E>
1783 where
1784 E: de::Error,
1785 {
1786 Ok(RejectDuplicateJsonKeys)
1787 }
1788
1789 fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E>
1790 where
1791 E: de::Error,
1792 {
1793 Ok(RejectDuplicateJsonKeys)
1794 }
1795
1796 fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E>
1797 where
1798 E: de::Error,
1799 {
1800 Ok(RejectDuplicateJsonKeys)
1801 }
1802
1803 fn visit_str<E>(self, _value: &str) -> Result<Self::Value, E>
1804 where
1805 E: de::Error,
1806 {
1807 Ok(RejectDuplicateJsonKeys)
1808 }
1809
1810 fn visit_string<E>(self, _value: String) -> Result<Self::Value, E>
1811 where
1812 E: de::Error,
1813 {
1814 Ok(RejectDuplicateJsonKeys)
1815 }
1816
1817 fn visit_none<E>(self) -> Result<Self::Value, E>
1818 where
1819 E: de::Error,
1820 {
1821 Ok(RejectDuplicateJsonKeys)
1822 }
1823
1824 fn visit_unit<E>(self) -> Result<Self::Value, E>
1825 where
1826 E: de::Error,
1827 {
1828 Ok(RejectDuplicateJsonKeys)
1829 }
1830
1831 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1832 where
1833 A: de::SeqAccess<'de>,
1834 {
1835 while sequence.next_element::<RejectDuplicateJsonKeys>()?.is_some() {}
1836 Ok(RejectDuplicateJsonKeys)
1837 }
1838
1839 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1840 where
1841 A: de::MapAccess<'de>,
1842 {
1843 let mut keys = HashSet::new();
1844 while let Some(key) = map.next_key::<String>()? {
1845 if !keys.insert(key) {
1846 return Err(de::Error::custom("duplicate JSON object key"));
1847 }
1848 map.next_value::<RejectDuplicateJsonKeys>()?;
1849 }
1850 Ok(RejectDuplicateJsonKeys)
1851 }
1852}
1853
1854fn validated_renderer_operations(line: &ReviewedRenderingLine, expected_categories: &BTreeSet<&str>) -> bool {
1855 let observed_categories = line
1856 .operations
1857 .iter()
1858 .map(|operation| operation.category.as_str())
1859 .collect::<BTreeSet<_>>();
1860 line.operations.len() == expected_categories.len()
1861 && observed_categories == expected_categories.clone()
1862 && line.operations.iter().all(|operation| {
1863 let Some((cli_path, libpod_path, body_path)) = rendering_source_paths(operation.category.as_str()) else {
1864 return false;
1865 };
1866 source_matches_podman(&operation.cli_source, &line.revision, cli_path)
1867 && source_matches_podman(&operation.libpod_endpoint_source, &line.revision, libpod_path)
1868 && match body_path {
1869 Some(body_path) => operation
1870 .body_source
1871 .as_ref()
1872 .is_some_and(|source| source_matches_podman(source, &line.revision, body_path)),
1873 None => operation.body_source.is_none(),
1874 }
1875 })
1876}
1877
1878fn validated_field_evidence(line: &ReviewedRenderingLine) -> bool {
1879 let observed = line
1880 .field_evidence
1881 .iter()
1882 .map(|evidence| evidence.field)
1883 .collect::<BTreeSet<_>>();
1884 line.field_evidence.len() == expected_rendered_fields().len()
1885 && observed == expected_rendered_fields()
1886 && line.field_evidence.iter().all(|evidence| {
1887 if evidence.availability != expected_field_availability(evidence.field, &line.version) {
1888 return false;
1889 }
1890 if let Some((operation, flag, shape, member, cli_source, model_sources, handler_source)) =
1891 expected_network_field_claim(evidence.field)
1892 {
1893 return evidence.operation == operation
1894 && evidence.cli.flag == flag
1895 && evidence.cli.value_shape == shape
1896 && evidence.libpod.json_member == member
1897 && source_matches_network_evidence(&evidence.cli_source, line, cli_source)
1898 && evidence.model_sources.len() == model_sources.len()
1899 && evidence
1900 .model_sources
1901 .iter()
1902 .zip(model_sources)
1903 .all(|(source, expected)| source_matches_network_evidence(source, line, *expected))
1904 && source_matches_network_evidence(&evidence.handler_source, line, handler_source);
1905 }
1906 let (operation, flag, shape, member, cli_path, model_paths, handler_path) =
1907 expected_field_claim(evidence.field);
1908 evidence.operation == operation
1909 && evidence.cli.flag == flag
1910 && evidence.cli.value_shape == shape
1911 && evidence.libpod.json_member == member
1912 && source_matches_podman(&evidence.cli_source, &line.revision, cli_path)
1913 && evidence.model_sources.len() == model_paths.len()
1914 && evidence
1915 .model_sources
1916 .iter()
1917 .zip(model_paths)
1918 .all(|(source, path)| source_matches_podman(source, &line.revision, path))
1919 && source_matches_podman(&evidence.handler_source, &line.revision, handler_path)
1920 })
1921}
1922
1923fn expected_field_availability(field: RenderedField, version: &str) -> FieldAvailability {
1924 match field {
1925 RenderedField::ContainerNetworkOrder
1926 | RenderedField::NetworkRouteTypeBlackhole
1927 | RenderedField::NetworkRouteTypeUnreachable
1928 | RenderedField::NetworkRouteTypeProhibit
1929 if !supports_network_order(version) =>
1930 {
1931 FieldAvailability::Unsupported
1932 }
1933 _ => FieldAvailability::Exact,
1934 }
1935}
1936
1937fn expected_rendered_fields() -> BTreeSet<RenderedField> {
1938 [
1939 RenderedField::ContainerCommand,
1940 RenderedField::ContainerEntrypoint,
1941 RenderedField::ContainerUser,
1942 RenderedField::ContainerWorkdir,
1943 RenderedField::ContainerHostname,
1944 RenderedField::ContainerLabel,
1945 RenderedField::ContainerEnvironment,
1946 RenderedField::ContainerRestartPolicy,
1947 RenderedField::ContainerNamedVolumeMount,
1948 RenderedField::ContainerNetworkAttachment,
1949 RenderedField::PodNetworkAttachment,
1950 RenderedField::ContainerNetworkAlias,
1951 RenderedField::PodNetworkAlias,
1952 RenderedField::ContainerNetworkStaticIpv4,
1953 RenderedField::PodNetworkStaticIpv4,
1954 RenderedField::ContainerNetworkStaticIpv6,
1955 RenderedField::PodNetworkStaticIpv6,
1956 RenderedField::ContainerNetworkStaticMac,
1957 RenderedField::PodNetworkStaticMac,
1958 RenderedField::ContainerPortMapping,
1959 RenderedField::PodPortMapping,
1960 RenderedField::ContainerDnsServers,
1961 RenderedField::ContainerDnsSearch,
1962 RenderedField::ContainerDnsOptions,
1963 RenderedField::PodDnsServers,
1964 RenderedField::PodDnsSearch,
1965 RenderedField::PodDnsOptions,
1966 RenderedField::ContainerHostAlias,
1967 RenderedField::PodHostAlias,
1968 RenderedField::ContainerNetworkOrder,
1969 RenderedField::NetworkIpamSubnet,
1970 RenderedField::NetworkIpamGateway,
1971 RenderedField::NetworkIpamRange,
1972 RenderedField::NetworkRouteDestination,
1973 RenderedField::NetworkRouteGateway,
1974 RenderedField::NetworkRouteMetric,
1975 RenderedField::NetworkRouteTypeUnicast,
1976 RenderedField::NetworkRouteTypeBlackhole,
1977 RenderedField::NetworkRouteTypeUnreachable,
1978 RenderedField::NetworkRouteTypeProhibit,
1979 ]
1980 .into_iter()
1981 .collect()
1982}
1983
1984#[allow(clippy::too_many_lines)]
1985fn expected_field_claim(
1986 field: RenderedField,
1987) -> (
1988 RenderingOperationCategory,
1989 Option<CliFlag>,
1990 CliValueShape,
1991 LibpodBodyMember,
1992 &'static str,
1993 &'static [&'static str],
1994 &'static str,
1995) {
1996 const CONTAINER_MODEL: &[&str] = &["pkg/specgen/specgen.go"];
1997 const CONTAINER_VOLUME_MODEL: &[&str] = &["pkg/specgen/specgen.go", "pkg/specgen/volumes.go"];
1998 const CONTAINER_HANDLER: &str = "pkg/api/handlers/libpod/containers_create.go";
1999 match field {
2000 RenderedField::ContainerCommand => (
2001 RenderingOperationCategory::ContainerCreate,
2002 None,
2003 CliValueShape::ArgumentArray,
2004 LibpodBodyMember::Command,
2005 "cmd/podman/containers/create.go",
2006 CONTAINER_MODEL,
2007 CONTAINER_HANDLER,
2008 ),
2009 RenderedField::ContainerEntrypoint => (
2010 RenderingOperationCategory::ContainerCreate,
2011 Some(CliFlag::Entrypoint),
2012 CliValueShape::ArgumentArray,
2013 LibpodBodyMember::Entrypoint,
2014 "cmd/podman/common/create.go",
2015 CONTAINER_MODEL,
2016 CONTAINER_HANDLER,
2017 ),
2018 RenderedField::ContainerUser => (
2019 RenderingOperationCategory::ContainerCreate,
2020 Some(CliFlag::User),
2021 CliValueShape::ContainerUser,
2022 LibpodBodyMember::User,
2023 "cmd/podman/common/create.go",
2024 CONTAINER_MODEL,
2025 CONTAINER_HANDLER,
2026 ),
2027 RenderedField::ContainerWorkdir => (
2028 RenderingOperationCategory::ContainerCreate,
2029 Some(CliFlag::Workdir),
2030 CliValueShape::AbsoluteContainerPath,
2031 LibpodBodyMember::WorkDir,
2032 "cmd/podman/common/create.go",
2033 CONTAINER_MODEL,
2034 CONTAINER_HANDLER,
2035 ),
2036 RenderedField::ContainerHostname => (
2037 RenderingOperationCategory::ContainerCreate,
2038 Some(CliFlag::Hostname),
2039 CliValueShape::Hostname,
2040 LibpodBodyMember::Hostname,
2041 "cmd/podman/common/create.go",
2042 CONTAINER_MODEL,
2043 CONTAINER_HANDLER,
2044 ),
2045 RenderedField::ContainerLabel => (
2046 RenderingOperationCategory::ContainerCreate,
2047 Some(CliFlag::Label),
2048 CliValueShape::LabelAssignment,
2049 LibpodBodyMember::Labels,
2050 "cmd/podman/common/create.go",
2051 CONTAINER_MODEL,
2052 CONTAINER_HANDLER,
2053 ),
2054 RenderedField::ContainerEnvironment => (
2055 RenderingOperationCategory::ContainerCreate,
2056 Some(CliFlag::Environment),
2057 CliValueShape::EnvironmentAssignment,
2058 LibpodBodyMember::Env,
2059 "cmd/podman/common/create.go",
2060 CONTAINER_MODEL,
2061 CONTAINER_HANDLER,
2062 ),
2063 RenderedField::ContainerRestartPolicy => (
2064 RenderingOperationCategory::ContainerCreate,
2065 Some(CliFlag::Restart),
2066 CliValueShape::RestartPolicy,
2067 LibpodBodyMember::RestartPolicy,
2068 "cmd/podman/common/create.go",
2069 CONTAINER_MODEL,
2070 CONTAINER_HANDLER,
2071 ),
2072 RenderedField::ContainerNamedVolumeMount => (
2073 RenderingOperationCategory::ContainerCreate,
2074 Some(CliFlag::Volume),
2075 CliValueShape::NamedVolumeMount,
2076 LibpodBodyMember::Volumes,
2077 "cmd/podman/common/create.go",
2078 CONTAINER_VOLUME_MODEL,
2079 CONTAINER_HANDLER,
2080 ),
2081 _ => unreachable!("networking fields are handled by the dedicated evidence matrix"),
2082 }
2083}
2084
2085#[derive(Clone, Copy)]
2086enum ExpectedNetworkSource {
2087 Podman(&'static str),
2088 Common,
2089}
2090
2091type NetworkFieldClaim = (
2092 RenderingOperationCategory,
2093 Option<CliFlag>,
2094 CliValueShape,
2095 LibpodBodyMember,
2096 ExpectedNetworkSource,
2097 &'static [ExpectedNetworkSource],
2098 ExpectedNetworkSource,
2099);
2100
2101#[allow(clippy::too_many_lines)] fn expected_network_field_claim(field: RenderedField) -> Option<NetworkFieldClaim> {
2103 const CONTAINER_NETWORK: &[ExpectedNetworkSource] = &[
2104 ExpectedNetworkSource::Podman("pkg/specgen/specgen.go"),
2105 ExpectedNetworkSource::Common,
2106 ];
2107 const POD_NETWORK: &[ExpectedNetworkSource] = &[
2108 ExpectedNetworkSource::Podman("pkg/specgen/podspecgen.go"),
2109 ExpectedNetworkSource::Common,
2110 ];
2111 const CONTAINER_NETWORK_CONFIG: &[ExpectedNetworkSource] =
2112 &[ExpectedNetworkSource::Podman("pkg/specgen/specgen.go")];
2113 const POD_NETWORK_CONFIG: &[ExpectedNetworkSource] = &[ExpectedNetworkSource::Podman("pkg/specgen/podspecgen.go")];
2114 const CONTAINER_NETWORK_ORDER: &[ExpectedNetworkSource] = &[
2115 ExpectedNetworkSource::Podman("pkg/specgen/specgen.go"),
2116 ExpectedNetworkSource::Podman("pkg/specgen/namespaces.go"),
2117 ];
2118 const NETWORK: &[ExpectedNetworkSource] = &[ExpectedNetworkSource::Common];
2119 const CONTAINER_HANDLER: ExpectedNetworkSource =
2120 ExpectedNetworkSource::Podman("pkg/api/handlers/libpod/containers_create.go");
2121 const POD_HANDLER: ExpectedNetworkSource = ExpectedNetworkSource::Podman("pkg/api/handlers/libpod/pods.go");
2122 const NETWORK_HANDLER: ExpectedNetworkSource = ExpectedNetworkSource::Podman("pkg/api/handlers/libpod/networks.go");
2123 let container_network = |shape| {
2124 (
2125 RenderingOperationCategory::ContainerCreate,
2126 Some(CliFlag::Network),
2127 shape,
2128 LibpodBodyMember::Networks,
2129 ExpectedNetworkSource::Podman("cmd/podman/common/netflags.go"),
2130 CONTAINER_NETWORK,
2131 CONTAINER_HANDLER,
2132 )
2133 };
2134 let pod_network = |shape| {
2135 (
2136 RenderingOperationCategory::PodCreate,
2137 Some(CliFlag::Network),
2138 shape,
2139 LibpodBodyMember::Networks,
2140 ExpectedNetworkSource::Podman("cmd/podman/pods/create.go"),
2141 POD_NETWORK,
2142 POD_HANDLER,
2143 )
2144 };
2145 let container_network_config = |flag, shape, member| {
2146 (
2147 RenderingOperationCategory::ContainerCreate,
2148 Some(flag),
2149 shape,
2150 member,
2151 ExpectedNetworkSource::Podman("cmd/podman/common/netflags.go"),
2152 CONTAINER_NETWORK_CONFIG,
2153 CONTAINER_HANDLER,
2154 )
2155 };
2156 let pod_network_config = |flag, shape, member| {
2157 (
2158 RenderingOperationCategory::PodCreate,
2159 Some(flag),
2160 shape,
2161 member,
2162 ExpectedNetworkSource::Podman("cmd/podman/pods/create.go"),
2163 POD_NETWORK_CONFIG,
2164 POD_HANDLER,
2165 )
2166 };
2167 let network_create = |flag, shape, member| {
2168 (
2169 RenderingOperationCategory::NetworkCreate,
2170 Some(flag),
2171 shape,
2172 member,
2173 ExpectedNetworkSource::Podman("cmd/podman/networks/create.go"),
2174 NETWORK,
2175 NETWORK_HANDLER,
2176 )
2177 };
2178 Some(match field {
2179 RenderedField::ContainerNetworkAttachment => container_network(CliValueShape::NetworkAttachment),
2180 RenderedField::PodNetworkAttachment => pod_network(CliValueShape::NetworkAttachment),
2181 RenderedField::ContainerNetworkAlias => container_network(CliValueShape::NetworkAlias),
2182 RenderedField::PodNetworkAlias => pod_network(CliValueShape::NetworkAlias),
2183 RenderedField::ContainerNetworkStaticIpv4 | RenderedField::ContainerNetworkStaticIpv6 => {
2184 container_network(CliValueShape::StaticIp)
2185 }
2186 RenderedField::PodNetworkStaticIpv4 | RenderedField::PodNetworkStaticIpv6 => {
2187 pod_network(CliValueShape::StaticIp)
2188 }
2189 RenderedField::ContainerNetworkStaticMac => container_network(CliValueShape::StaticMac),
2190 RenderedField::PodNetworkStaticMac => pod_network(CliValueShape::StaticMac),
2191 RenderedField::ContainerPortMapping => (
2192 RenderingOperationCategory::ContainerCreate,
2193 Some(CliFlag::Publish),
2194 CliValueShape::PortMapping,
2195 LibpodBodyMember::Portmappings,
2196 ExpectedNetworkSource::Podman("cmd/podman/common/netflags.go"),
2197 CONTAINER_NETWORK,
2198 CONTAINER_HANDLER,
2199 ),
2200 RenderedField::PodPortMapping => (
2201 RenderingOperationCategory::PodCreate,
2202 Some(CliFlag::Publish),
2203 CliValueShape::PortMapping,
2204 LibpodBodyMember::Portmappings,
2205 ExpectedNetworkSource::Podman("cmd/podman/pods/create.go"),
2206 POD_NETWORK,
2207 POD_HANDLER,
2208 ),
2209 RenderedField::ContainerDnsServers => {
2210 container_network_config(CliFlag::Dns, CliValueShape::DnsServer, LibpodBodyMember::DnsServer)
2211 }
2212 RenderedField::ContainerDnsSearch => container_network_config(
2213 CliFlag::DnsSearch,
2214 CliValueShape::DnsSearch,
2215 LibpodBodyMember::DnsSearch,
2216 ),
2217 RenderedField::ContainerDnsOptions => container_network_config(
2218 CliFlag::DnsOption,
2219 CliValueShape::DnsOption,
2220 LibpodBodyMember::DnsOption,
2221 ),
2222 RenderedField::PodDnsServers => {
2223 pod_network_config(CliFlag::Dns, CliValueShape::DnsServer, LibpodBodyMember::DnsServer)
2224 }
2225 RenderedField::PodDnsSearch => pod_network_config(
2226 CliFlag::DnsSearch,
2227 CliValueShape::DnsSearch,
2228 LibpodBodyMember::DnsSearch,
2229 ),
2230 RenderedField::PodDnsOptions => pod_network_config(
2231 CliFlag::DnsOption,
2232 CliValueShape::DnsOption,
2233 LibpodBodyMember::DnsOption,
2234 ),
2235 RenderedField::ContainerHostAlias => {
2236 container_network_config(CliFlag::AddHost, CliValueShape::HostAlias, LibpodBodyMember::Hostadd)
2237 }
2238 RenderedField::PodHostAlias => {
2239 pod_network_config(CliFlag::AddHost, CliValueShape::HostAlias, LibpodBodyMember::Hostadd)
2240 }
2241 RenderedField::ContainerNetworkOrder => (
2242 RenderingOperationCategory::ContainerCreate,
2243 Some(CliFlag::Network),
2244 CliValueShape::NetworkOrder,
2245 LibpodBodyMember::NetworkOrder,
2246 ExpectedNetworkSource::Podman("cmd/podman/common/netflags.go"),
2247 CONTAINER_NETWORK_ORDER,
2248 CONTAINER_HANDLER,
2249 ),
2250 RenderedField::NetworkIpamSubnet => {
2251 network_create(CliFlag::Subnet, CliValueShape::NetworkSubnet, LibpodBodyMember::Subnets)
2252 }
2253 RenderedField::NetworkIpamGateway => network_create(
2254 CliFlag::Gateway,
2255 CliValueShape::NetworkGateway,
2256 LibpodBodyMember::Subnets,
2257 ),
2258 RenderedField::NetworkIpamRange => network_create(
2259 CliFlag::IpRange,
2260 CliValueShape::NetworkIpRange,
2261 LibpodBodyMember::Subnets,
2262 ),
2263 RenderedField::NetworkRouteDestination
2264 | RenderedField::NetworkRouteGateway
2265 | RenderedField::NetworkRouteMetric
2266 | RenderedField::NetworkRouteTypeUnicast
2267 | RenderedField::NetworkRouteTypeBlackhole
2268 | RenderedField::NetworkRouteTypeUnreachable
2269 | RenderedField::NetworkRouteTypeProhibit => {
2270 network_create(CliFlag::Route, CliValueShape::NetworkRoute, LibpodBodyMember::Routes)
2271 }
2272 _ => return None,
2273 })
2274}
2275
2276fn source_matches_network_evidence(
2277 source: &SourceReference,
2278 line: &ReviewedRenderingLine,
2279 expected: ExpectedNetworkSource,
2280) -> bool {
2281 match expected {
2282 ExpectedNetworkSource::Podman(path) => source_matches_podman(source, &line.revision, path),
2283 ExpectedNetworkSource::Common => {
2284 source.repository == line.common_module.repository
2285 && source.revision == line.common_module.revision
2286 && source.path == common_model_source_path(line)
2287 && source.module.as_ref() == Some(&line.common_module)
2288 && is_lowercase_sha40(&source.revision)
2289 }
2290 }
2291}
2292
2293fn common_model_source_path(line: &ReviewedRenderingLine) -> &'static str {
2294 match line.version.as_str() {
2295 "5.4.0" | "5.5.0" | "5.6.0" => "libnetwork/types/network.go",
2296 "5.7.0" | "5.8.6" | "6.0.0" | "6.1.0" => "common/libnetwork/types/network.go",
2297 _ => unreachable!("reviewed renderer versions are validated before field evidence"),
2298 }
2299}
2300
2301fn rendering_source_paths(category: &str) -> Option<(&'static str, &'static str, Option<&'static str>)> {
2302 match category {
2303 "network-create" => Some((
2304 "cmd/podman/networks/create.go",
2305 "pkg/api/server/register_networks.go",
2306 Some("pkg/api/handlers/libpod/networks.go"),
2307 )),
2308 "volume-create" => Some((
2309 "cmd/podman/volumes/create.go",
2310 "pkg/api/server/register_volumes.go",
2311 Some("pkg/api/handlers/libpod/volumes.go"),
2312 )),
2313 "secret-create" => Some((
2314 "cmd/podman/secrets/create.go",
2315 "pkg/api/server/register_secrets.go",
2316 Some("pkg/api/handlers/libpod/secrets.go"),
2317 )),
2318 "image-pull" => Some(("cmd/podman/images/pull.go", "pkg/api/server/register_images.go", None)),
2319 "pod-create" => Some((
2320 "cmd/podman/pods/create.go",
2321 "pkg/api/server/register_pods.go",
2322 Some("pkg/api/handlers/libpod/pods.go"),
2323 )),
2324 "container-create" => Some((
2325 "cmd/podman/containers/create.go",
2326 "pkg/api/server/register_containers.go",
2327 Some("pkg/api/handlers/libpod/containers_create.go"),
2328 )),
2329 "pod-start" => Some(("cmd/podman/pods/start.go", "pkg/api/server/register_pods.go", None)),
2330 "container-start" => Some((
2331 "cmd/podman/containers/start.go",
2332 "pkg/api/server/register_containers.go",
2333 None,
2334 )),
2335 _ => None,
2336 }
2337}
2338
2339fn source_matches_podman(source: &SourceReference, revision: &str, path: &str) -> bool {
2340 source.repository == SourceRepository::ContainersPodman
2341 && source.revision == revision
2342 && source.path == path
2343 && source.module.is_none()
2344 && is_lowercase_sha40(&source.revision)
2345}
2346
2347fn valid_common_module(version: &str, module: &ModulePin) -> bool {
2348 matches!(
2349 (
2350 version,
2351 module.repository,
2352 module.path.as_str(),
2353 module.version.as_str(),
2354 module.revision.as_str()
2355 ),
2356 (
2357 "5.4.0",
2358 SourceRepository::ContainersCommon,
2359 "github.com/containers/common",
2360 "v0.62.0",
2361 "cde1afdf623bdb9595d1fdc7acffa6e2f03a06b2"
2362 ) | (
2363 "5.5.0",
2364 SourceRepository::ContainersCommon,
2365 "github.com/containers/common",
2366 "v0.63.0",
2367 "92927328862e4837d87bcbc0725b713387399984"
2368 ) | (
2369 "5.6.0",
2370 SourceRepository::ContainersCommon,
2371 "github.com/containers/common",
2372 "v0.64.1",
2373 "c007f37a6c55a53a7d41419d094c815bf2e46ca3"
2374 ) | (
2375 "5.7.0",
2376 SourceRepository::ContainersContainerLibs,
2377 "go.podman.io/common",
2378 "v0.66.0",
2379 "8163ca799c317e3dea886be7228406ec8cf06abc"
2380 ) | (
2381 "5.8.6",
2382 SourceRepository::ContainersContainerLibs,
2383 "go.podman.io/common",
2384 "v0.67.1",
2385 "c8b7f74383aa8bac5a84118db1a6fa870602af63"
2386 ) | (
2387 "6.0.0",
2388 SourceRepository::ContainersContainerLibs,
2389 "go.podman.io/common",
2390 "v0.68.0",
2391 "bb6a37c8946a977f24a8eed4d97b2dc3608cd05e"
2392 ) | (
2393 "6.1.0",
2394 SourceRepository::PodmanContainerToolsContainerLibs,
2395 "go.podman.io/common",
2396 "v0.69.1",
2397 "e47c7ccf66a1b89b0807e053dd426ff26eedd7a7"
2398 )
2399 )
2400}
2401
2402fn canonical_version(version: &Version) -> String {
2403 format!("{}.{}.{}", version.major, version.minor, version.patch)
2404}
2405
2406fn is_lowercase_sha40(value: &str) -> bool {
2407 value.len() == 40
2408 && value
2409 .bytes()
2410 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
2411}
2412
2413fn managed_image_sources(plan: &DeploymentPlan) -> BTreeMap<DeploymentResourceId, String> {
2414 plan.operations()
2415 .iter()
2416 .filter_map(|operation| match operation.resource_intent() {
2417 DeploymentResource::Image(image) => Some((image.identity().clone(), image.source().as_str().to_owned())),
2418 _ => None,
2419 })
2420 .collect()
2421}
2422
2423#[allow(clippy::too_many_lines)] fn unsupported_fields(
2425 resource: &DeploymentResource,
2426 version: &str,
2427 context: crate::TargetExecutionContext,
2428) -> Vec<&'static str> {
2429 match resource {
2430 DeploymentResource::Image(image) => {
2431 let mut fields = Vec::new();
2432 if !matches!(
2433 image.source().classification(),
2434 crate::ImageSourceClassification::Portable
2435 ) {
2436 fields.push("source.portability");
2437 }
2438 if !supports_image_policy(version) {
2439 fields.push("pull_policy.target_version");
2440 }
2441 fields
2442 }
2443 DeploymentResource::Volume(volume) => {
2444 if !supports_volume_ownership(version) && (volume.uid().is_some() || volume.gid().is_some()) {
2445 vec!["owner.target_version"]
2446 } else {
2447 Vec::new()
2448 }
2449 }
2450 DeploymentResource::Network(network) => {
2451 let mut fields = Vec::new();
2452 if !supports_network_route_type(version)
2453 && network
2454 .routes()
2455 .iter()
2456 .any(|route| route.route_type() != RouteType::Unicast)
2457 {
2458 fields.push("routes.route_type");
2459 }
2460 fields
2461 }
2462 DeploymentResource::Pod(pod) => {
2463 let mut fields = Vec::new();
2464 if context != crate::TargetExecutionContext::Rootful
2465 && pod.networks().iter().any(network_attachment_has_static_address)
2466 {
2467 fields.push("networks.static_address_requires_rootful");
2468 }
2469 if !pod.infra_mounts().is_empty() {
2470 fields.push("infra_mounts");
2471 }
2472 fields
2473 }
2474 DeploymentResource::Container(container) => {
2475 let mut fields = Vec::new();
2476 fields.extend(unsupported_runtime_fields(container, version));
2477 if container.pod().is_some() && !container.networks().is_empty() {
2478 fields.push("networks");
2479 }
2480 if container.pod().is_some() && !container.ports().is_empty() {
2481 fields.push("ports");
2482 }
2483 if container.pod().is_some()
2484 && (!container.dns().servers().is_empty()
2485 || !container.dns().search().is_empty()
2486 || !container.dns().options().is_empty())
2487 {
2488 fields.push("dns");
2489 }
2490 if container.pod().is_some() && !container.host_aliases().is_empty() {
2491 fields.push("host_aliases");
2492 }
2493 if container.pod().is_some() && container.network_order().is_some() {
2494 fields.push("network_order");
2495 }
2496 if context != crate::TargetExecutionContext::Rootful
2497 && container.networks().iter().any(network_attachment_has_static_address)
2498 {
2499 fields.push("networks.static_address_requires_rootful");
2500 }
2501 if container.network_order().is_some() && !supports_network_order(version) {
2502 fields.push("network_order");
2503 }
2504 if !container.secret_grants().is_empty() && !secret_grants_renderable(container.secret_grants()) {
2505 fields.push("secret_grants");
2506 }
2507 if container.mounts().iter().any(mount_has_unsupported_subpath) {
2508 fields.push("mounts.subpath_nocopy");
2509 }
2510 let settings = container.settings();
2511 if settings.environment().iter().any(|assignment| {
2512 matches!(
2513 assignment.value(),
2514 crate::DeploymentEnvironmentValue::SensitiveInline(_)
2515 )
2516 }) {
2517 fields.push("environment.sensitive_inline");
2518 }
2519 if settings
2520 .environment()
2521 .iter()
2522 .any(|assignment| matches!(assignment.value(), crate::DeploymentEnvironmentValue::External(_)))
2523 {
2524 fields.push("environment.external");
2525 }
2526 if container.pod().is_some() && settings.restart_policy().is_some() {
2527 fields.push("restart_policy.pod_member");
2528 }
2529 fields
2530 }
2531 _ => Vec::new(),
2532 }
2533}
2534
2535fn network_attachment_has_static_address(attachment: &NetworkAttachment) -> bool {
2536 attachment.static_ipv4().is_some() || attachment.static_ipv6().is_some() || attachment.static_mac().is_some()
2537}
2538
2539fn supports_network_order(version: &str) -> bool {
2540 matches!(version, "6.0.0" | "6.1.0")
2541}
2542
2543fn supports_image_policy(version: &str) -> bool {
2544 !matches!(version, "5.4.0" | "5.5.0")
2545}
2546
2547fn supports_volume_ownership(version: &str) -> bool {
2548 supports_image_policy(version)
2549}
2550
2551fn supports_network_route_type(version: &str) -> bool {
2552 supports_network_order(version)
2553}
2554
2555fn append_network_arguments(
2556 arguments: &mut Vec<String>,
2557 networks: &[NetworkAttachment],
2558 order: Option<&[DeploymentResourceId]>,
2559) -> bool {
2560 let attachments = ordered_network_attachments(networks, order);
2561 let Some(attachments) = attachments else {
2562 return false;
2563 };
2564 for network in attachments {
2565 let Some(value) = network_attachment_cli_value(network) else {
2566 return false;
2567 };
2568 arguments.push("--network".to_owned());
2569 arguments.push(value);
2570 }
2571 true
2572}
2573
2574fn ordered_network_attachments<'a>(
2575 attachments: &'a [NetworkAttachment],
2576 order: Option<&[DeploymentResourceId]>,
2577) -> Option<Vec<&'a NetworkAttachment>> {
2578 match order {
2579 None => Some(attachments.iter().collect()),
2580 Some(order) => order
2581 .iter()
2582 .map(|identity| attachments.iter().find(|attachment| attachment.network() == identity))
2583 .collect(),
2584 }
2585}
2586
2587fn network_attachment_cli_value(attachment: &NetworkAttachment) -> Option<String> {
2588 let name = attachment.network().name();
2589 if name.contains([':', ',']) {
2590 return None;
2591 }
2592 let mut value = name.to_owned();
2593 let mut options = Vec::new();
2594 options.extend(attachment.aliases().iter().map(|alias| format!("alias={alias}")));
2595 if let Some(ipv4) = attachment.static_ipv4() {
2596 options.push(format!("ip={ipv4}"));
2597 }
2598 if let Some(ipv6) = attachment.static_ipv6() {
2599 options.push(format!("ip6={ipv6}"));
2600 }
2601 if let Some(mac) = attachment.static_mac() {
2602 options.push(format!("mac={}", mac.as_str()));
2603 }
2604 if !options.is_empty() {
2605 value.push(':');
2606 value.push_str(&options.join(","));
2607 }
2608 Some(value)
2609}
2610
2611fn network_configuration(networks: &[NetworkAttachment]) -> Value {
2612 let networks = networks
2613 .iter()
2614 .map(|network| {
2615 let mut options = Map::new();
2616 if !network.aliases().is_empty() {
2617 options.insert("aliases".to_owned(), string_array(network.aliases()));
2618 }
2619 let static_ips = [network.static_ipv4(), network.static_ipv6()]
2620 .into_iter()
2621 .flatten()
2622 .map(|address| Value::String(address.to_string()))
2623 .collect::<Vec<_>>();
2624 if !static_ips.is_empty() {
2625 options.insert("static_ips".to_owned(), Value::Array(static_ips));
2626 }
2627 if let Some(mac) = network.static_mac() {
2628 options.insert("static_mac".to_owned(), Value::String(mac.as_str().to_owned()));
2629 }
2630 (network.network().name().to_owned(), Value::Object(options))
2631 })
2632 .collect::<Map<String, Value>>();
2633 Value::Object(networks)
2634}
2635
2636fn append_port_arguments(arguments: &mut Vec<String>, ports: &[PortMapping]) {
2637 for port in ports {
2638 arguments.push("--publish".to_owned());
2639 arguments.push(port_mapping_cli_value(port));
2640 }
2641}
2642
2643fn port_mapping_cli_value(port: &PortMapping) -> String {
2644 let mut value = String::new();
2645 if let Some(host_ip) = port.host_ip() {
2646 if host_ip.is_ipv6() {
2647 value.push('[');
2648 value.push_str(&host_ip.to_string());
2649 value.push(']');
2650 } else {
2651 value.push_str(&host_ip.to_string());
2652 }
2653 value.push(':');
2654 }
2655 let protocol = match port.protocol() {
2656 PortProtocol::Tcp => "tcp",
2657 PortProtocol::Udp => "udp",
2658 PortProtocol::Sctp => "sctp",
2659 };
2660 format!("{value}{}:{}/{protocol}", port.host_port(), port.container_port())
2661}
2662
2663fn append_dns_arguments(arguments: &mut Vec<String>, dns: &crate::DnsConfiguration) {
2664 for server in dns.servers() {
2665 arguments.extend(["--dns".to_owned(), server.to_string()]);
2666 }
2667 for search in dns.search() {
2668 arguments.extend(["--dns-search".to_owned(), search.clone()]);
2669 }
2670 for option in dns.options() {
2671 arguments.extend(["--dns-option".to_owned(), option.clone()]);
2672 }
2673}
2674
2675fn append_host_alias_arguments(arguments: &mut Vec<String>, aliases: &[HostAlias]) {
2676 for alias in aliases {
2677 arguments.extend(["--add-host".to_owned(), host_alias_value(alias)]);
2678 }
2679}
2680
2681fn append_networking_json(
2682 body: &mut Map<String, Value>,
2683 ports: &[PortMapping],
2684 dns: &crate::DnsConfiguration,
2685 aliases: &[HostAlias],
2686 network_order: Option<&[DeploymentResourceId]>,
2687) {
2688 if !ports.is_empty() {
2689 body.insert(
2690 "portmappings".to_owned(),
2691 Value::Array(
2692 ports
2693 .iter()
2694 .map(|port| {
2695 let protocol = match port.protocol() {
2696 PortProtocol::Tcp => "tcp",
2697 PortProtocol::Udp => "udp",
2698 PortProtocol::Sctp => "sctp",
2699 };
2700 json!({
2701 "host_ip": port.host_ip().map_or_else(String::new, |address| address.to_string()),
2702 "host_port": port.host_port(),
2703 "container_port": port.container_port(),
2704 "range": 1,
2705 "protocol": protocol,
2706 })
2707 })
2708 .collect(),
2709 ),
2710 );
2711 }
2712 if !dns.servers().is_empty() {
2713 body.insert(
2714 "dns_server".to_owned(),
2715 Value::Array(
2716 dns.servers()
2717 .iter()
2718 .map(|server| Value::String(server.to_string()))
2719 .collect(),
2720 ),
2721 );
2722 }
2723 if !dns.search().is_empty() {
2724 body.insert("dns_search".to_owned(), string_array(dns.search()));
2725 }
2726 if !dns.options().is_empty() {
2727 body.insert("dns_option".to_owned(), string_array(dns.options()));
2728 }
2729 if !aliases.is_empty() {
2730 body.insert(
2731 "hostadd".to_owned(),
2732 Value::Array(
2733 aliases
2734 .iter()
2735 .map(|alias| Value::String(host_alias_value(alias)))
2736 .collect(),
2737 ),
2738 );
2739 }
2740 if let Some(order) = network_order {
2741 body.insert(
2742 "networkOrder".to_owned(),
2743 Value::Array(
2744 order
2745 .iter()
2746 .map(|network| Value::String(network.name().to_owned()))
2747 .collect(),
2748 ),
2749 );
2750 }
2751}
2752
2753fn host_alias_value(alias: &HostAlias) -> String {
2754 format!("{}:{}", alias.hostname(), alias.address())
2758}
2759
2760fn append_network_create_arguments(arguments: &mut Vec<String>, network: &NetworkIntent) {
2761 for subnet in network.subnets() {
2762 arguments.extend(["--subnet".to_owned(), subnet.subnet().as_str().to_owned()]);
2763 }
2764 for subnet in network.subnets() {
2765 if let Some(gateway) = subnet.gateway() {
2766 arguments.extend(["--gateway".to_owned(), gateway.to_string()]);
2767 }
2768 }
2769 for subnet in network.subnets() {
2770 if let Some((start, end)) = subnet.range() {
2771 arguments.extend(["--ip-range".to_owned(), format!("{start}-{end}")]);
2772 }
2773 }
2774 for route in network.routes() {
2775 arguments.extend(["--route".to_owned(), network_route_cli_value(route)]);
2776 }
2777}
2778
2779fn network_route_cli_value(route: &NetworkRoute) -> String {
2780 let next_hop = match route.route_type() {
2781 RouteType::Unicast => route.gateway().map_or_else(String::new, |gateway| gateway.to_string()),
2782 RouteType::Blackhole => "blackhole".to_owned(),
2783 RouteType::Unreachable => "unreachable".to_owned(),
2784 RouteType::Prohibit => "prohibit".to_owned(),
2785 };
2786 let mut value = format!("{},{}", route.destination().as_str(), next_hop);
2787 if let Some(metric) = route.metric() {
2788 let _ = write!(value, ",{metric}");
2789 }
2790 value
2791}
2792
2793fn network_create_configuration(network: &NetworkIntent, supports_route_type: bool) -> Value {
2794 let mut body = Map::new();
2795 body.insert("name".to_owned(), Value::String(network.identity().name().to_owned()));
2796 if !network.subnets().is_empty() {
2797 body.insert(
2798 "subnets".to_owned(),
2799 Value::Array(
2800 network
2801 .subnets()
2802 .iter()
2803 .map(|subnet| {
2804 let mut value = Map::new();
2805 value.insert("subnet".to_owned(), Value::String(subnet.subnet().as_str().to_owned()));
2806 if let Some(gateway) = subnet.gateway() {
2807 value.insert("gateway".to_owned(), Value::String(gateway.to_string()));
2808 }
2809 if let Some((start, end)) = subnet.range() {
2810 value.insert(
2811 "lease_range".to_owned(),
2812 json!({"start_ip": start.to_string(), "end_ip": end.to_string()}),
2813 );
2814 }
2815 Value::Object(value)
2816 })
2817 .collect(),
2818 ),
2819 );
2820 }
2821 if !network.routes().is_empty() {
2822 body.insert(
2823 "routes".to_owned(),
2824 Value::Array(
2825 network
2826 .routes()
2827 .iter()
2828 .map(|route| {
2829 let mut value = Map::new();
2830 value.insert(
2831 "destination".to_owned(),
2832 Value::String(route.destination().as_str().to_owned()),
2833 );
2834 if let Some(gateway) = route.gateway() {
2835 value.insert("gateway".to_owned(), Value::String(gateway.to_string()));
2836 }
2837 if let Some(metric) = route.metric() {
2838 value.insert("metric".to_owned(), json!(metric));
2839 }
2840 if supports_route_type {
2841 let route_type = match route.route_type() {
2842 RouteType::Unicast => "unicast",
2843 RouteType::Blackhole => "blackhole",
2844 RouteType::Unreachable => "unreachable",
2845 RouteType::Prohibit => "prohibit",
2846 };
2847 value.insert("route_type".to_owned(), Value::String(route_type.to_owned()));
2848 }
2849 Value::Object(value)
2850 })
2851 .collect(),
2852 ),
2853 );
2854 }
2855 Value::Object(body)
2856}
2857
2858fn append_mount_arguments(arguments: &mut Vec<String>, mounts: &[MountIntent]) -> bool {
2859 for mount in mounts {
2860 match mount {
2861 MountIntent::NamedVolume(mount) => {
2862 if mount.subpath().is_some() {
2863 if !cli_safe_mount_component(mount.source().name())
2864 || !cli_safe_mount_component(mount.destination().as_str())
2865 || mount
2866 .subpath()
2867 .is_some_and(|subpath| !cli_safe_mount_component(subpath.as_str()))
2868 {
2869 return false;
2870 }
2871 arguments.extend([
2872 "--mount".to_owned(),
2873 mount_cli_value(
2874 "volume",
2875 mount.source().name(),
2876 mount.destination().as_str(),
2877 mount.access(),
2878 mount.subpath().map(crate::VolumeSubpath::as_str),
2879 ),
2880 ]);
2881 continue;
2882 }
2883 if !cli_safe_mount_component(mount.source().name())
2884 || !cli_safe_mount_component(mount.destination().as_str())
2885 {
2886 return false;
2887 }
2888 let access = if mount.is_read_only() { "ro" } else { "rw" };
2889 let copy = match mount.copy_mode() {
2890 crate::NamedVolumeCopyMode::Copy => "copy",
2891 crate::NamedVolumeCopyMode::NoCopy => "nocopy",
2892 };
2893 let options = format!("{access},{copy}");
2894 arguments.extend([
2895 "--volume".to_owned(),
2896 format!("{}:{}:{options}", mount.source().name(), mount.destination().as_str()),
2897 ]);
2898 }
2899 MountIntent::Bind(mount) => {
2900 if !cli_safe_mount_component(mount.source().as_str())
2901 || !cli_safe_mount_component(mount.destination().as_str())
2902 {
2903 return false;
2904 }
2905 arguments.extend([
2906 "--mount".to_owned(),
2907 mount_cli_value(
2908 "bind",
2909 mount.source().as_str(),
2910 mount.destination().as_str(),
2911 mount.access(),
2912 None,
2913 ),
2914 ]);
2915 }
2916 MountIntent::Tmpfs(mount) => {
2917 if !cli_safe_mount_component(mount.destination().as_str()) {
2918 return false;
2919 }
2920 arguments.extend([
2921 "--mount".to_owned(),
2922 mount_cli_value("tmpfs", "", mount.destination().as_str(), mount.access(), None),
2923 ]);
2924 }
2925 }
2926 }
2927 true
2928}
2929
2930fn cli_safe_mount_component(value: &str) -> bool {
2931 !value.contains([':', ','])
2932}
2933
2934fn mount_json(mounts: &[MountIntent]) -> Value {
2935 Value::Array(
2936 mounts
2937 .iter()
2938 .filter_map(|mount| match mount {
2939 MountIntent::NamedVolume(mount) if mount.subpath().is_none() => {
2940 let access = if mount.is_read_only() { "ro" } else { "rw" };
2941 let copy = match mount.copy_mode() {
2942 crate::NamedVolumeCopyMode::Copy => "copy",
2943 crate::NamedVolumeCopyMode::NoCopy => "nocopy",
2944 };
2945 let options = vec![access.to_owned(), copy.to_owned()];
2946 Some(json!({"Name": mount.source().name(), "Dest": mount.destination().as_str(), "Options": options}))
2947 }
2948 MountIntent::NamedVolume(mount) => Some(json!({
2949 "Name": mount.source().name(),
2950 "Dest": mount.destination().as_str(),
2951 "Options": if mount.access().is_read_only() { vec!["ro"] } else { Vec::<&str>::new() },
2952 "SubPath": mount.subpath().map(crate::VolumeSubpath::as_str),
2953 })),
2954 MountIntent::Bind(_) | MountIntent::Tmpfs(_) => None,
2955 })
2956 .collect(),
2957 )
2958}
2959
2960fn mount_cli_value(
2961 kind: &str,
2962 source: &str,
2963 destination: &str,
2964 access: crate::MountAccess,
2965 subpath: Option<&str>,
2966) -> String {
2967 let mut value = format!("type={kind}");
2968 if !source.is_empty() {
2969 let _ = write!(value, ",source={source}");
2970 }
2971 let _ = write!(value, ",target={destination}");
2972 if access.is_read_only() {
2973 value.push_str(",readonly");
2974 }
2975 if let Some(subpath) = subpath {
2976 let _ = write!(value, ",subpath={subpath}");
2977 }
2978 value
2979}
2980
2981fn native_mount_json(mounts: &[MountIntent]) -> Value {
2982 Value::Array(
2983 mounts
2984 .iter()
2985 .filter_map(|mount| match mount {
2986 MountIntent::Bind(mount) => Some(json!({
2987 "destination": mount.destination().as_str(),
2988 "type": "bind",
2989 "source": mount.source().as_str(),
2990 "options": if mount.access().is_read_only() { ["ro"] } else { ["rw"] },
2991 })),
2992 MountIntent::Tmpfs(mount) => Some(json!({
2993 "destination": mount.destination().as_str(),
2994 "type": "tmpfs",
2995 "source": "tmpfs",
2996 "options": if mount.access().is_read_only() { ["ro"] } else { ["rw"] },
2997 })),
2998 MountIntent::NamedVolume(_) => None,
2999 })
3000 .collect(),
3001 )
3002}
3003
3004fn append_secret_grants_arguments(arguments: &mut Vec<String>, grants: &[SecretGrant]) -> bool {
3005 for grant in grants {
3006 if !cli_safe_mount_component(grant.source().name()) {
3007 return false;
3008 }
3009 let value = match grant {
3010 SecretGrant::Mount { .. } => {
3011 let mut value = format!("source={},type=mount", grant.source().name());
3012 if let Some(target) = grant.mount_target() {
3013 if !cli_safe_mount_component(target.as_str()) {
3014 return false;
3015 }
3016 let _ = write!(value, ",target={}", target.as_str());
3017 }
3018 if let Some(uid) = grant.mount_uid() {
3019 let _ = write!(value, ",uid={}", uid.get());
3020 }
3021 if let Some(gid) = grant.mount_gid() {
3022 let _ = write!(value, ",gid={}", gid.get());
3023 }
3024 if let Some(mode) = grant.mount_mode() {
3025 let _ = write!(value, ",mode={:o}", mode.get());
3026 }
3027 value
3028 }
3029 SecretGrant::Environment { target, .. } => {
3030 format!("source={},type=env,target={}", grant.source().name(), target.as_str())
3031 }
3032 };
3033 arguments.extend(["--secret".to_owned(), value]);
3034 }
3035 true
3036}
3037
3038fn append_secret_grants_json(body: &mut Map<String, Value>, grants: &[SecretGrant]) {
3039 let mut mounts = Vec::new();
3040 let mut environment = Map::new();
3041 for grant in grants {
3042 match grant {
3043 SecretGrant::Mount { .. } => {
3044 let mut value = Map::new();
3045 value.insert("Source".to_owned(), Value::String(grant.source().name().to_owned()));
3046 if let Some(target) = grant.mount_target() {
3047 value.insert("Target".to_owned(), Value::String(target.as_str().to_owned()));
3048 }
3049 if let Some(uid) = grant.mount_uid() {
3050 value.insert("UID".to_owned(), json!(uid.get()));
3051 }
3052 if let Some(gid) = grant.mount_gid() {
3053 value.insert("GID".to_owned(), json!(gid.get()));
3054 }
3055 value.insert(
3060 "Mode".to_owned(),
3061 json!(grant.mount_mode().map_or(0o444, SecretMode::get)),
3062 );
3063 mounts.push(Value::Object(value));
3064 }
3065 SecretGrant::Environment { target, .. } => {
3066 environment.insert(
3067 target.as_str().to_owned(),
3068 Value::String(grant.source().name().to_owned()),
3069 );
3070 }
3071 }
3072 }
3073 if !mounts.is_empty() {
3074 body.insert("secrets".to_owned(), Value::Array(mounts));
3075 }
3076 if !environment.is_empty() {
3077 body.insert("secret_env".to_owned(), Value::Object(environment));
3078 }
3079}
3080
3081fn volume_create_cli_arguments(volume: &crate::VolumeIntent) -> Vec<String> {
3082 let mut arguments = vec!["volume".to_owned(), "create".to_owned()];
3083 if let Some(uid) = volume.uid() {
3084 arguments.extend(["--uid".to_owned(), uid.get().to_string()]);
3085 }
3086 if let Some(gid) = volume.gid() {
3087 arguments.extend(["--gid".to_owned(), gid.get().to_string()]);
3088 }
3089 arguments.push(volume.identity().name().to_owned());
3090 arguments
3091}
3092
3093fn volume_create_json(volume: &crate::VolumeIntent) -> Value {
3094 let mut body = Map::new();
3095 body.insert("Name".to_owned(), Value::String(volume.identity().name().to_owned()));
3096 if let Some(uid) = volume.uid() {
3097 body.insert("UID".to_owned(), json!(uid.get()));
3098 }
3099 if let Some(gid) = volume.gid() {
3100 body.insert("GID".to_owned(), json!(gid.get()));
3101 }
3102 Value::Object(body)
3103}
3104
3105fn mount_has_unsupported_subpath(mount: &MountIntent) -> bool {
3106 matches!(mount, MountIntent::NamedVolume(volume) if volume.subpath().is_some() && volume.copy_mode() == crate::NamedVolumeCopyMode::NoCopy)
3107}
3108
3109fn secret_grants_renderable(grants: &[SecretGrant]) -> bool {
3110 let mut arguments = Vec::new();
3111 append_secret_grants_arguments(&mut arguments, grants)
3112}
3113
3114fn unsupported_runtime_fields(container: &crate::ContainerIntent, version: &str) -> Vec<&'static str> {
3115 let runtime = container.runtime();
3116 let mut fields = Vec::new();
3117 if runtime.startup_health().is_some() && !matches!(runtime.health(), Some(crate::HealthCheck::Command(_))) {
3118 fields.push("runtime.startup_health_requires_health");
3119 }
3120 for (health, field) in [
3121 (
3122 runtime.health().and_then(|health| match health {
3123 crate::HealthCheck::Command(configured) => Some(configured.command()),
3124 crate::HealthCheck::Disabled => None,
3125 }),
3126 "runtime.health.command",
3127 ),
3128 (
3129 runtime.startup_health().map(crate::StartupHealthCheck::command),
3130 "runtime.startup_health.command",
3131 ),
3132 ] {
3133 if health.is_some_and(health_command_is_sensitive) {
3134 fields.push(field);
3135 }
3136 }
3137 if !runtime.logging().journald_labels().is_empty() && !supports_journald_labels(version) {
3138 fields.push("runtime.logging.journald_labels.target_version");
3139 }
3140 if runtime.logging().driver().is_none()
3141 && (runtime.logging().max_size().is_some() || !runtime.logging().journald_labels().is_empty())
3142 {
3143 fields.push("runtime.logging.driver");
3144 }
3145 if !runtime.logging().journald_labels().is_empty() && runtime.logging().driver() != Some(crate::LogDriver::Journald)
3146 {
3147 fields.push("runtime.logging.journald_labels");
3148 }
3149 if runtime.logging().max_size().is_some() && runtime.logging().driver() != Some(crate::LogDriver::K8sFile) {
3150 fields.push("runtime.logging.max_size");
3151 }
3152 if runtime.resources().rlimits().iter().any(rlimit_is_unlimited) && !supports_unlimited_rlimit(version) {
3153 fields.push("runtime.resources.rlimits.unlimited.target_version");
3154 }
3155 if runtime.security().privileged() == Some(true)
3156 && (!runtime.security().cap_add().is_empty() || !runtime.security().cap_drop().is_empty())
3157 {
3158 fields.push("runtime.security.privileged_capabilities");
3159 }
3160 if runtime
3161 .security()
3162 .cap_add()
3163 .iter()
3164 .any(|capability| runtime.security().cap_drop().contains(capability))
3165 {
3166 fields.push("runtime.security.capability_overlap");
3167 }
3168 if runtime.security().read_write_tmpfs() == Some(true) && runtime.security().read_only_filesystem() != Some(true) {
3169 fields.push("runtime.security.read_write_tmpfs");
3170 }
3171 if container.pod().is_some() && !runtime.namespaces().is_empty() {
3172 fields.push("runtime.namespaces.pod_member");
3173 }
3174 fields
3175}
3176
3177fn health_command_is_sensitive(command: &crate::HealthCommand) -> bool {
3178 matches!(
3179 command,
3180 crate::HealthCommand::SensitiveInlineShell(_)
3181 | crate::HealthCommand::SensitiveInlineExec(_)
3182 | crate::HealthCommand::ExternalShell(_)
3183 | crate::HealthCommand::ExternalExec(_)
3184 )
3185}
3186
3187fn rlimit_is_unlimited(limit: &crate::Rlimit) -> bool {
3188 matches!(limit.soft(), crate::RlimitValue::Unlimited) || matches!(limit.hard(), crate::RlimitValue::Unlimited)
3189}
3190
3191fn supports_journald_labels(version: &str) -> bool {
3192 matches!(version, "6.0.0" | "6.1.0")
3193}
3194
3195fn supports_unlimited_rlimit(version: &str) -> bool {
3196 !matches!(version, "5.4.0" | "5.5.0")
3197}
3198
3199fn append_container_runtime_arguments(
3200 arguments: &mut Vec<String>,
3201 container: &crate::ContainerIntent,
3202 identity: &DeploymentResourceId,
3203 version: &str,
3204) -> Result<(), RenderingFinding> {
3205 let runtime = container.runtime();
3206 if let Some(health) = runtime.health() {
3207 match health {
3208 crate::HealthCheck::Disabled => arguments.push("--no-healthcheck".to_owned()),
3209 crate::HealthCheck::Command(configured) => {
3210 arguments.extend([
3211 "--health-cmd".to_owned(),
3212 health_command_cli(configured.command(), identity, "runtime.health.command")?,
3213 ]);
3214 append_health_arguments(arguments, configured);
3215 }
3216 }
3217 }
3218 if let Some(startup) = runtime.startup_health() {
3219 arguments.extend([
3220 "--health-startup-cmd".to_owned(),
3221 health_command_cli(startup.command(), identity, "runtime.startup_health.command")?,
3222 ]);
3223 if let Some(interval) = startup.interval() {
3224 arguments.extend(["--health-startup-interval".to_owned(), health_interval_cli(interval)]);
3225 }
3226 if let Some(timeout) = startup.timeout() {
3227 arguments.extend([
3228 "--health-startup-timeout".to_owned(),
3229 health_duration_cli(timeout.nanoseconds()),
3230 ]);
3231 }
3232 if let Some(retries) = startup.retries() {
3233 arguments.extend(["--health-startup-retries".to_owned(), retries.value().to_string()]);
3234 }
3235 if let Some(successes) = startup.successes() {
3236 arguments.extend(["--health-startup-success".to_owned(), successes.value().to_string()]);
3237 }
3238 }
3239 let logging = runtime.logging();
3240 if let Some(driver) = logging.driver() {
3241 arguments.extend(["--log-driver".to_owned(), log_driver_name(driver).to_owned()]);
3242 }
3243 if let Some(size) = logging.max_size() {
3244 arguments.extend(["--log-opt".to_owned(), format!("max-size={}", size.bytes())]);
3245 }
3246 if !logging.journald_labels().is_empty() && !supports_journald_labels(version) {
3247 return Err(rendering_runtime_finding(
3248 identity,
3249 "runtime.logging.journald_labels.target_version",
3250 ));
3251 }
3252 for label in logging.journald_labels() {
3253 arguments.extend([
3254 "--log-opt".to_owned(),
3255 format!("label={}={}", label.key().as_str(), label.value().as_str()),
3256 ]);
3257 }
3258 append_security_arguments(arguments, runtime.security());
3259 append_namespace_arguments(arguments, runtime.namespaces());
3260 append_resource_arguments(arguments, runtime.resources(), identity, version)?;
3261 Ok(())
3262}
3263
3264fn append_health_arguments(arguments: &mut Vec<String>, health: &crate::ConfiguredHealthCheck) {
3265 if let Some(interval) = health.interval() {
3266 arguments.extend(["--health-interval".to_owned(), health_interval_cli(interval)]);
3267 }
3268 if let Some(timeout) = health.timeout() {
3269 arguments.extend([
3270 "--health-timeout".to_owned(),
3271 health_duration_cli(timeout.nanoseconds()),
3272 ]);
3273 }
3274 if let Some(retries) = health.retries() {
3275 arguments.extend(["--health-retries".to_owned(), retries.value().to_string()]);
3276 }
3277 if let Some(period) = health.start_period() {
3278 arguments.extend([
3279 "--health-start-period".to_owned(),
3280 health_duration_cli(period.nanoseconds()),
3281 ]);
3282 }
3283 if let Some(on_failure) = health.on_failure() {
3284 arguments.extend([
3285 "--health-on-failure".to_owned(),
3286 health_on_failure_name(on_failure).to_owned(),
3287 ]);
3288 }
3289}
3290
3291fn append_security_arguments(arguments: &mut Vec<String>, security: &crate::SecuritySettings) {
3292 if let Some(value) = security.privileged() {
3293 arguments.push(format!("--privileged={value}"));
3294 }
3295 if let Some(value) = security.no_new_privileges() {
3296 arguments.extend(["--security-opt".to_owned(), format!("no-new-privileges={value}")]);
3297 }
3298 if let Some(value) = security.read_only_filesystem() {
3299 arguments.push(format!("--read-only={value}"));
3300 }
3301 if let Some(value) = security.read_write_tmpfs() {
3302 arguments.push(format!("--read-only-tmpfs={value}"));
3303 }
3304 for capability in security.cap_add() {
3305 arguments.extend(["--cap-add".to_owned(), capability.as_str().to_owned()]);
3306 }
3307 for capability in security.cap_drop() {
3308 arguments.extend(["--cap-drop".to_owned(), capability.as_str().to_owned()]);
3309 }
3310}
3311
3312fn append_namespace_arguments(arguments: &mut Vec<String>, namespaces: &crate::ContainerNamespaceSettings) {
3313 if let Some(pid) = namespaces.pid() {
3314 arguments.extend(["--pid".to_owned(), namespace_mode_name(pid).to_owned()]);
3315 }
3316 if let Some(ipc) = namespaces.ipc() {
3317 arguments.extend(["--ipc".to_owned(), ipc_namespace_mode_name(ipc).to_owned()]);
3318 }
3319 if let Some(uts) = namespaces.uts() {
3320 arguments.extend(["--uts".to_owned(), namespace_mode_name(uts).to_owned()]);
3321 }
3322 if let Some(cgroup) = namespaces.cgroup() {
3323 arguments.extend(["--cgroupns".to_owned(), namespace_mode_name(cgroup).to_owned()]);
3324 }
3325}
3326
3327fn append_resource_arguments(
3328 arguments: &mut Vec<String>,
3329 resources: &crate::ContainerResourceControls,
3330 identity: &DeploymentResourceId,
3331 version: &str,
3332) -> Result<(), RenderingFinding> {
3333 for (flag, value) in [
3334 ("--cpu-shares", resources.cpu_shares()),
3335 ("--cpu-period", resources.cpu_period()),
3336 ("--cpu-quota", resources.cpu_quota()),
3337 ("--memory", resources.memory_bytes()),
3338 ("--pids-limit", resources.pids()),
3339 ] {
3340 if let Some(value) = value {
3341 arguments.extend([flag.to_owned(), value.to_string()]);
3342 }
3343 }
3344 for rlimit in resources.rlimits() {
3345 if rlimit_is_unlimited(rlimit) && !supports_unlimited_rlimit(version) {
3346 return Err(rendering_runtime_finding(
3347 identity,
3348 "runtime.resources.rlimits.unlimited.target_version",
3349 ));
3350 }
3351 arguments.extend(["--ulimit".to_owned(), rlimit_cli_value(rlimit)]);
3352 }
3353 Ok(())
3354}
3355
3356fn append_container_runtime_json(
3357 body: &mut Map<String, Value>,
3358 container: &crate::ContainerIntent,
3359 identity: &DeploymentResourceId,
3360 version: &str,
3361) -> Result<(), RenderingFinding> {
3362 let runtime = container.runtime();
3363 if let Some(health) = runtime.health() {
3364 body.insert(
3365 "healthconfig".to_owned(),
3366 health_json(health, identity, "runtime.health.command")?,
3367 );
3368 if let crate::HealthCheck::Command(configured) = health {
3369 if let Some(on_failure) = configured.on_failure() {
3370 body.insert(
3371 "health_check_on_failure_action".to_owned(),
3372 json!(health_on_failure_code(on_failure)),
3373 );
3374 }
3375 }
3376 }
3377 if let Some(startup) = runtime.startup_health() {
3378 let mut configuration = Map::new();
3379 configuration.insert(
3380 "Test".to_owned(),
3381 health_command_json(startup.command(), identity, "runtime.startup_health.command")?,
3382 );
3383 append_startup_health_json(&mut configuration, startup);
3384 body.insert("startupHealthConfig".to_owned(), Value::Object(configuration));
3385 }
3386 append_logging_json(body, runtime.logging(), identity, version)?;
3387 append_security_json(body, runtime.security());
3388 append_namespace_json(body, runtime.namespaces());
3389 append_resource_json(body, runtime.resources(), identity, version)?;
3390 Ok(())
3391}
3392
3393fn health_command_cli(
3394 command: &crate::HealthCommand,
3395 identity: &DeploymentResourceId,
3396 field: &'static str,
3397) -> Result<String, RenderingFinding> {
3398 serde_json::to_string(&health_command_json(command, identity, field)?)
3399 .map_err(|_| rendering_runtime_finding(identity, field))
3400}
3401
3402fn health_command_json(
3403 command: &crate::HealthCommand,
3404 identity: &DeploymentResourceId,
3405 field: &'static str,
3406) -> Result<Value, RenderingFinding> {
3407 match command {
3408 crate::HealthCommand::Shell(command) => Ok(json!(["CMD-SHELL", command.as_str()])),
3409 crate::HealthCommand::Exec(arguments) => Ok(Value::Array(
3410 std::iter::once(Value::String("CMD".to_owned()))
3411 .chain(arguments.values().iter().cloned().map(Value::String))
3412 .collect(),
3413 )),
3414 crate::HealthCommand::SensitiveInlineShell(_)
3415 | crate::HealthCommand::SensitiveInlineExec(_)
3416 | crate::HealthCommand::ExternalShell(_)
3417 | crate::HealthCommand::ExternalExec(_) => Err(rendering_runtime_finding(identity, field)),
3418 }
3419}
3420
3421fn health_json(
3422 health: &crate::HealthCheck,
3423 identity: &DeploymentResourceId,
3424 field: &'static str,
3425) -> Result<Value, RenderingFinding> {
3426 match health {
3427 crate::HealthCheck::Disabled => Ok(json!({"Test": ["NONE"]})),
3428 crate::HealthCheck::Command(configured) => {
3429 let mut configuration = Map::new();
3430 configuration.insert(
3431 "Test".to_owned(),
3432 health_command_json(configured.command(), identity, field)?,
3433 );
3434 append_configured_health_json(&mut configuration, configured);
3435 Ok(Value::Object(configuration))
3436 }
3437 }
3438}
3439
3440fn append_configured_health_json(configuration: &mut Map<String, Value>, health: &crate::ConfiguredHealthCheck) {
3441 if let Some(interval) = health.interval() {
3442 configuration.insert("Interval".to_owned(), json!(health_interval_nanoseconds(interval)));
3443 }
3444 if let Some(timeout) = health.timeout() {
3445 configuration.insert("Timeout".to_owned(), json!(timeout.nanoseconds()));
3446 }
3447 if let Some(retries) = health.retries() {
3448 configuration.insert("Retries".to_owned(), json!(retries.value()));
3449 }
3450 if let Some(period) = health.start_period() {
3451 configuration.insert("StartPeriod".to_owned(), json!(period.nanoseconds()));
3452 }
3453}
3454
3455fn append_startup_health_json(configuration: &mut Map<String, Value>, health: &crate::StartupHealthCheck) {
3456 if let Some(interval) = health.interval() {
3457 configuration.insert("Interval".to_owned(), json!(health_interval_nanoseconds(interval)));
3458 }
3459 if let Some(timeout) = health.timeout() {
3460 configuration.insert("Timeout".to_owned(), json!(timeout.nanoseconds()));
3461 }
3462 if let Some(retries) = health.retries() {
3463 configuration.insert("Retries".to_owned(), json!(retries.value()));
3464 }
3465 if let Some(successes) = health.successes() {
3466 configuration.insert("Successes".to_owned(), json!(successes.value()));
3467 }
3468}
3469
3470fn health_interval_cli(interval: crate::HealthInterval) -> String {
3471 match interval {
3472 crate::HealthInterval::Disabled => "disable".to_owned(),
3473 crate::HealthInterval::Every(duration) => health_duration_cli(duration.nanoseconds()),
3474 }
3475}
3476
3477fn health_interval_nanoseconds(interval: crate::HealthInterval) -> i64 {
3478 match interval {
3479 crate::HealthInterval::Disabled => 0,
3480 crate::HealthInterval::Every(duration) => duration.nanoseconds(),
3481 }
3482}
3483
3484fn health_duration_cli(nanoseconds: i64) -> String {
3485 format!("{nanoseconds}ns")
3486}
3487
3488fn health_on_failure_name(value: crate::HealthOnFailure) -> &'static str {
3489 match value {
3490 crate::HealthOnFailure::None => "none",
3491 crate::HealthOnFailure::Kill => "kill",
3492 crate::HealthOnFailure::Restart => "restart",
3493 crate::HealthOnFailure::Stop => "stop",
3494 }
3495}
3496
3497const fn health_on_failure_code(value: crate::HealthOnFailure) -> u8 {
3498 match value {
3499 crate::HealthOnFailure::None => 0,
3500 crate::HealthOnFailure::Kill => 2,
3501 crate::HealthOnFailure::Restart => 3,
3502 crate::HealthOnFailure::Stop => 4,
3503 }
3504}
3505
3506fn log_driver_name(value: crate::LogDriver) -> &'static str {
3507 match value {
3508 crate::LogDriver::Journald => "journald",
3509 crate::LogDriver::K8sFile => "k8s-file",
3510 }
3511}
3512
3513fn append_logging_json(
3514 body: &mut Map<String, Value>,
3515 logging: &crate::LoggingSettings,
3516 identity: &DeploymentResourceId,
3517 version: &str,
3518) -> Result<(), RenderingFinding> {
3519 if logging.driver().is_none() && logging.max_size().is_none() && logging.journald_labels().is_empty() {
3520 return Ok(());
3521 }
3522 if !logging.journald_labels().is_empty() && !supports_journald_labels(version) {
3523 return Err(rendering_runtime_finding(
3524 identity,
3525 "runtime.logging.journald_labels.target_version",
3526 ));
3527 }
3528 let mut configuration = Map::new();
3529 if let Some(driver) = logging.driver() {
3530 configuration.insert("driver".to_owned(), Value::String(log_driver_name(driver).to_owned()));
3531 }
3532 if let Some(size) = logging.max_size() {
3533 configuration.insert("size".to_owned(), json!(size.bytes()));
3534 }
3535 if !logging.journald_labels().is_empty() {
3536 configuration.insert(
3537 "labels".to_owned(),
3538 Value::Object(
3539 logging
3540 .journald_labels()
3541 .iter()
3542 .map(|label| {
3543 (
3544 label.key().as_str().to_owned(),
3545 Value::String(label.value().as_str().to_owned()),
3546 )
3547 })
3548 .collect(),
3549 ),
3550 );
3551 }
3552 body.insert("log_configuration".to_owned(), Value::Object(configuration));
3553 Ok(())
3554}
3555
3556fn append_security_json(body: &mut Map<String, Value>, security: &crate::SecuritySettings) {
3557 if let Some(value) = security.privileged() {
3558 body.insert("privileged".to_owned(), json!(value));
3559 }
3560 if let Some(value) = security.no_new_privileges() {
3561 body.insert("no_new_privileges".to_owned(), json!(value));
3562 }
3563 if let Some(value) = security.read_only_filesystem() {
3564 body.insert("read_only_filesystem".to_owned(), json!(value));
3565 }
3566 if let Some(value) = security.read_write_tmpfs() {
3567 body.insert("read_write_tmpfs".to_owned(), json!(value));
3568 }
3569 if !security.cap_add().is_empty() {
3570 body.insert(
3571 "cap_add".to_owned(),
3572 Value::Array(
3573 security
3574 .cap_add()
3575 .iter()
3576 .map(|value| Value::String(value.as_str().to_owned()))
3577 .collect(),
3578 ),
3579 );
3580 }
3581 if !security.cap_drop().is_empty() {
3582 body.insert(
3583 "cap_drop".to_owned(),
3584 Value::Array(
3585 security
3586 .cap_drop()
3587 .iter()
3588 .map(|value| Value::String(value.as_str().to_owned()))
3589 .collect(),
3590 ),
3591 );
3592 }
3593}
3594
3595fn append_namespace_json(body: &mut Map<String, Value>, namespaces: &crate::ContainerNamespaceSettings) {
3596 for (member, mode) in [
3597 ("pidns", namespaces.pid().map(namespace_mode_name)),
3598 ("ipcns", namespaces.ipc().map(ipc_namespace_mode_name)),
3599 ("utsns", namespaces.uts().map(namespace_mode_name)),
3600 ("cgroupns", namespaces.cgroup().map(namespace_mode_name)),
3601 ] {
3602 if let Some(mode) = mode {
3603 body.insert(member.to_owned(), json!({"nsmode": mode}));
3604 }
3605 }
3606}
3607
3608fn namespace_mode_name(value: crate::NamespaceMode) -> &'static str {
3609 match value {
3610 crate::NamespaceMode::Private => "private",
3611 crate::NamespaceMode::Host => "host",
3612 }
3613}
3614
3615fn ipc_namespace_mode_name(value: crate::IpcNamespaceMode) -> &'static str {
3616 match value {
3617 crate::IpcNamespaceMode::Private => "private",
3618 crate::IpcNamespaceMode::Host => "host",
3619 crate::IpcNamespaceMode::Shareable => "shareable",
3620 crate::IpcNamespaceMode::None => "none",
3621 }
3622}
3623
3624fn append_resource_json(
3625 body: &mut Map<String, Value>,
3626 resources: &crate::ContainerResourceControls,
3627 identity: &DeploymentResourceId,
3628 version: &str,
3629) -> Result<(), RenderingFinding> {
3630 let mut limits = Map::new();
3631 let mut cpu = Map::new();
3632 if let Some(value) = resources.cpu_shares() {
3633 cpu.insert("shares".to_owned(), json!(value));
3634 }
3635 if let Some(value) = resources.cpu_period() {
3636 cpu.insert("period".to_owned(), json!(value));
3637 }
3638 if let Some(value) = resources.cpu_quota() {
3639 cpu.insert("quota".to_owned(), json!(value));
3640 }
3641 if !cpu.is_empty() {
3642 limits.insert("cpu".to_owned(), Value::Object(cpu));
3643 }
3644 if let Some(value) = resources.memory_bytes() {
3645 limits.insert("memory".to_owned(), json!({"limit": value}));
3646 }
3647 if let Some(value) = resources.pids() {
3648 limits.insert("pids".to_owned(), json!({"limit": value}));
3649 }
3650 if !limits.is_empty() {
3651 body.insert("resource_limits".to_owned(), Value::Object(limits));
3652 }
3653 if !resources.rlimits().is_empty() {
3654 let mut limits = Vec::with_capacity(resources.rlimits().len());
3655 for rlimit in resources.rlimits() {
3656 if rlimit_is_unlimited(rlimit) && !supports_unlimited_rlimit(version) {
3657 return Err(rendering_runtime_finding(
3658 identity,
3659 "runtime.resources.rlimits.unlimited.target_version",
3660 ));
3661 }
3662 limits.push(json!({
3663 "type": rlimit_kind_name(rlimit.kind()),
3664 "soft": rlimit_value_json(rlimit.soft()),
3665 "hard": rlimit_value_json(rlimit.hard()),
3666 }));
3667 }
3668 body.insert("r_limits".to_owned(), Value::Array(limits));
3669 }
3670 Ok(())
3671}
3672
3673fn rlimit_cli_value(rlimit: &crate::Rlimit) -> String {
3674 format!(
3675 "{}={}:{}",
3676 rlimit_kind_cli_name(rlimit.kind()),
3677 rlimit_value_cli(rlimit.soft()),
3678 rlimit_value_cli(rlimit.hard())
3679 )
3680}
3681
3682fn rlimit_kind_cli_name(kind: crate::RlimitKind) -> &'static str {
3683 match kind {
3684 crate::RlimitKind::NoFile => "nofile",
3685 crate::RlimitKind::NProc => "nproc",
3686 }
3687}
3688
3689fn rlimit_kind_name(kind: crate::RlimitKind) -> &'static str {
3690 match kind {
3691 crate::RlimitKind::NoFile => "RLIMIT_NOFILE",
3692 crate::RlimitKind::NProc => "RLIMIT_NPROC",
3693 }
3694}
3695
3696fn rlimit_value_cli(value: crate::RlimitValue) -> String {
3697 match value {
3698 crate::RlimitValue::Finite(value) => value.to_string(),
3699 crate::RlimitValue::Unlimited => "-1".to_owned(),
3700 }
3701}
3702
3703fn rlimit_value_json(value: crate::RlimitValue) -> Value {
3704 match value {
3705 crate::RlimitValue::Finite(value) => json!(value),
3706 crate::RlimitValue::Unlimited => json!(-1),
3707 }
3708}
3709
3710fn rendering_runtime_finding(identity: &DeploymentResourceId, field: &'static str) -> RenderingFinding {
3711 RenderingFinding::new(
3712 DiagnosticCode::RenderingUnsupported,
3713 Some(identity.clone()),
3714 Some(field),
3715 )
3716}
3717
3718fn append_container_setting_arguments(
3719 arguments: &mut Vec<String>,
3720 container: &crate::ContainerIntent,
3721 identity: &DeploymentResourceId,
3722) -> Result<(), RenderingFinding> {
3723 let settings = container.settings();
3724 if let Some(entrypoint) = settings.entrypoint() {
3725 let encoded = serde_json::to_string(entrypoint.values()).map_err(|_| {
3726 RenderingFinding::new(
3727 DiagnosticCode::RenderingUnsupported,
3728 Some(identity.clone()),
3729 Some("entrypoint"),
3730 )
3731 })?;
3732 arguments.push("--entrypoint".to_owned());
3733 arguments.push(encoded);
3734 }
3735 if let Some(user) = settings.user() {
3736 arguments.push("--user".to_owned());
3737 arguments.push(user.as_str().to_owned());
3738 }
3739 if let Some(workdir) = settings.workdir() {
3740 arguments.push("--workdir".to_owned());
3741 arguments.push(workdir.path().as_str().to_owned());
3742 }
3743 if let Some(hostname) = settings.hostname() {
3744 arguments.push("--hostname".to_owned());
3745 arguments.push(hostname.as_str().to_owned());
3746 }
3747 for label in settings.labels() {
3748 arguments.push("--label".to_owned());
3749 arguments.push(format!("{}={}", label.key().as_str(), label.value().as_str()));
3750 }
3751 for assignment in settings.environment() {
3752 let crate::DeploymentEnvironmentValue::Public(value) = assignment.value() else {
3753 return Err(RenderingFinding::new(
3754 DiagnosticCode::RenderingUnsupported,
3755 Some(identity.clone()),
3756 Some("environment"),
3757 ));
3758 };
3759 arguments.push("--env".to_owned());
3760 arguments.push(format!("{}={}", assignment.name().as_str(), value.as_str()));
3761 }
3762 if let Some(restart) = settings.restart_policy() {
3763 arguments.push("--restart".to_owned());
3764 arguments.push(restart_policy_name(restart).to_owned());
3765 }
3766 Ok(())
3767}
3768
3769fn append_container_setting_json(
3770 body: &mut Map<String, Value>,
3771 container: &crate::ContainerIntent,
3772 identity: &DeploymentResourceId,
3773) -> Result<(), RenderingFinding> {
3774 let settings = container.settings();
3775 if let Some(command) = settings.command() {
3776 body.insert("command".to_owned(), string_array(command.values()));
3777 }
3778 if let Some(entrypoint) = settings.entrypoint() {
3779 body.insert("entrypoint".to_owned(), string_array(entrypoint.values()));
3780 }
3781 if let Some(user) = settings.user() {
3782 body.insert("user".to_owned(), Value::String(user.as_str().to_owned()));
3783 }
3784 if let Some(workdir) = settings.workdir() {
3785 body.insert("work_dir".to_owned(), Value::String(workdir.path().as_str().to_owned()));
3786 }
3787 if let Some(hostname) = settings.hostname() {
3788 body.insert("hostname".to_owned(), Value::String(hostname.as_str().to_owned()));
3789 }
3790 if !settings.labels().is_empty() {
3791 body.insert(
3792 "labels".to_owned(),
3793 Value::Object(
3794 settings
3795 .labels()
3796 .iter()
3797 .map(|label| {
3798 (
3799 label.key().as_str().to_owned(),
3800 Value::String(label.value().as_str().to_owned()),
3801 )
3802 })
3803 .collect(),
3804 ),
3805 );
3806 }
3807 if !settings.environment().is_empty() {
3808 let mut environment = Map::new();
3809 for assignment in settings.environment() {
3810 let crate::DeploymentEnvironmentValue::Public(value) = assignment.value() else {
3811 return Err(RenderingFinding::new(
3812 DiagnosticCode::RenderingUnsupported,
3813 Some(identity.clone()),
3814 Some("environment"),
3815 ));
3816 };
3817 environment.insert(
3818 assignment.name().as_str().to_owned(),
3819 Value::String(value.as_str().to_owned()),
3820 );
3821 }
3822 body.insert("env".to_owned(), Value::Object(environment));
3823 }
3824 if let Some(restart) = settings.restart_policy() {
3825 body.insert(
3826 "restart_policy".to_owned(),
3827 Value::String(restart_policy_name(restart).to_owned()),
3828 );
3829 }
3830 if !container.mounts().is_empty() {
3831 let volumes = mount_json(container.mounts());
3832 if !volumes.as_array().is_some_and(Vec::is_empty) {
3833 body.insert("volumes".to_owned(), volumes);
3834 }
3835 let mounts = native_mount_json(container.mounts());
3836 if !mounts.as_array().is_some_and(Vec::is_empty) {
3837 body.insert("mounts".to_owned(), mounts);
3838 }
3839 }
3840 Ok(())
3841}
3842
3843fn string_array(values: &[String]) -> Value {
3844 Value::Array(values.iter().cloned().map(Value::String).collect())
3845}
3846
3847fn restart_policy_name(policy: RestartPolicy) -> &'static str {
3848 match policy {
3849 RestartPolicy::No => "no",
3850 RestartPolicy::OnFailure => "on-failure",
3851 RestartPolicy::Always => "always",
3852 RestartPolicy::UnlessStopped => "unless-stopped",
3853 }
3854}
3855
3856fn percent_encode(value: &str) -> String {
3857 let mut output = String::new();
3858 for byte in value.bytes() {
3859 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
3860 output.push(char::from(byte));
3861 } else {
3862 let _ = write!(output, "%{byte:02X}");
3863 }
3864 }
3865 output
3866}
3867
3868fn shell_quote(value: &str) -> String {
3869 format!("'{}'", value.replace('\'', "'\"'\"'"))
3870}
3871
3872fn resource_kind_name(kind: ResourceKind) -> &'static str {
3873 match kind {
3874 ResourceKind::Container => "container",
3875 ResourceKind::Pod => "pod",
3876 ResourceKind::Network => "network",
3877 ResourceKind::Volume => "volume",
3878 ResourceKind::Image => "image",
3879 ResourceKind::Secret => "secret",
3880 }
3881}
3882
3883#[cfg(test)]
3884mod tests {
3885 #![allow(clippy::expect_used)]
3886
3887 use super::{RENDERING_CATALOGUE_JSON, host_alias_value, parse_renderer_catalogue};
3888 use crate::HostAlias;
3889
3890 fn decoded_catalogue() -> serde_json::Value {
3891 serde_json::from_str(RENDERING_CATALOGUE_JSON).expect("embedded catalogue JSON")
3892 }
3893
3894 fn encoded_catalogue(value: &serde_json::Value) -> String {
3895 serde_json::to_string(value).expect("catalogue JSON")
3896 }
3897
3898 #[test]
3899 fn renderer_catalogue_rejects_invalid_operation_evidence_revisions_and_noncanonical_versions() {
3900 let missing_category = RENDERING_CATALOGUE_JSON.replacen(
3901 "\"category\": \"container-start\"",
3902 "\"category\": \"container-run\"",
3903 1,
3904 );
3905 assert!(parse_renderer_catalogue(&missing_category).is_err());
3906
3907 let invalid_source = RENDERING_CATALOGUE_JSON.replacen(
3908 "pkg/api/server/register_networks.go",
3909 "pkg/api/server/register_unknown.go",
3910 1,
3911 );
3912 assert!(parse_renderer_catalogue(&invalid_source).is_err());
3913
3914 let invalid_revision = RENDERING_CATALOGUE_JSON.replacen(
3915 "f9f7d48b24b1ca4403f189caaeab1cb8ff4a9aa2",
3916 "F9f7d48b24b1ca4403f189caaeab1cb8ff4a9aa2",
3917 1,
3918 );
3919 assert!(parse_renderer_catalogue(&invalid_revision).is_err());
3920
3921 let build_metadata =
3922 RENDERING_CATALOGUE_JSON.replacen("\"version\": \"5.4.0\"", "\"version\": \"5.4.0+build\"", 1);
3923 assert!(parse_renderer_catalogue(&build_metadata).is_err());
3924 }
3925
3926 #[test]
3927 fn renderer_catalogue_requires_the_complete_strict_field_matrix() {
3928 let mut missing = decoded_catalogue();
3929 missing["reviewed_lines"][0]["field_evidence"]
3930 .as_array_mut()
3931 .expect("field evidence")
3932 .pop();
3933 assert!(parse_renderer_catalogue(&encoded_catalogue(&missing)).is_err());
3934
3935 let mut duplicate = decoded_catalogue();
3936 let fields = duplicate["reviewed_lines"][0]["field_evidence"]
3937 .as_array_mut()
3938 .expect("field evidence");
3939 fields.push(fields[0].clone());
3940 assert!(parse_renderer_catalogue(&encoded_catalogue(&duplicate)).is_err());
3941
3942 let mut wrong_operation = decoded_catalogue();
3943 wrong_operation["reviewed_lines"][0]["field_evidence"][0]["operation"] = serde_json::json!("pod-create");
3944 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_operation)).is_err());
3945
3946 let mut wrong_claim = decoded_catalogue();
3947 wrong_claim["reviewed_lines"][0]["field_evidence"][1]["cli"]["flag"] = serde_json::json!("--label");
3948 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_claim)).is_err());
3949
3950 let mut wrong_shape = decoded_catalogue();
3951 wrong_shape["reviewed_lines"][0]["field_evidence"][1]["cli"]["value_shape"] = serde_json::json!("hostname");
3952 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_shape)).is_err());
3953
3954 let mut wrong_member = decoded_catalogue();
3955 wrong_member["reviewed_lines"][0]["field_evidence"][1]["libpod"]["json_member"] = serde_json::json!("hostname");
3956 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_member)).is_err());
3957 }
3958
3959 #[test]
3960 fn renderer_catalogue_rejects_nonimmutable_field_evidence_and_capability_substitutions() {
3961 let mut legacy_source_shape = decoded_catalogue();
3962 legacy_source_shape["reviewed_lines"][0]["field_evidence"][0]["cli_source"] =
3963 serde_json::json!("https://github.com/containers/podman/blob/main/cmd/podman/containers/create.go");
3964 assert!(parse_renderer_catalogue(&encoded_catalogue(&legacy_source_shape)).is_err());
3965 let mut wrong_module_version = decoded_catalogue();
3966 wrong_module_version["reviewed_lines"][0]["common_module"]["version"] = serde_json::json!("v0.0.0");
3967 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_module_version)).is_err());
3968
3969 let mut wrong_module_revision = decoded_catalogue();
3970 wrong_module_revision["reviewed_lines"][0]["common_module"]["revision"] =
3971 serde_json::json!("0000000000000000000000000000000000000000");
3972 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_module_revision)).is_err());
3973
3974 let mut wrong_module_repository = decoded_catalogue();
3975 wrong_module_repository["reviewed_lines"][0]["common_module"]["repository"] =
3976 serde_json::json!("podman-container-tools-container-libs");
3977 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_module_repository)).is_err());
3978
3979 let mut wrong_module_path = decoded_catalogue();
3980 wrong_module_path["reviewed_lines"][0]["common_module"]["path"] = serde_json::json!("go.podman.io/common");
3981 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_module_path)).is_err());
3982
3983 let mut mutable_field_source = decoded_catalogue();
3984 mutable_field_source["reviewed_lines"][0]["field_evidence"][0]["cli_source"]["revision"] =
3985 serde_json::json!("main");
3986 assert!(parse_renderer_catalogue(&encoded_catalogue(&mutable_field_source)).is_err());
3987
3988 let mut wrong_field_repository = decoded_catalogue();
3989 wrong_field_repository["reviewed_lines"][0]["field_evidence"][0]["cli_source"]["repository"] =
3990 serde_json::json!("containers-common");
3991 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_field_repository)).is_err());
3992
3993 let mut wrong_field_path = decoded_catalogue();
3994 wrong_field_path["reviewed_lines"][0]["field_evidence"][0]["cli_source"]["path"] =
3995 serde_json::json!("cmd/podman/unknown.go");
3996 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_field_path)).is_err());
3997
3998 let mut wrong_model = decoded_catalogue();
3999 wrong_model["reviewed_lines"][0]["field_evidence"][8]["model_sources"][1]["repository"] =
4000 serde_json::json!("containers-common");
4001 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_model)).is_err());
4002
4003 let mut mutable_model = decoded_catalogue();
4004 mutable_model["reviewed_lines"][0]["field_evidence"][8]["model_sources"][1]["revision"] =
4005 serde_json::json!("main");
4006 assert!(parse_renderer_catalogue(&encoded_catalogue(&mutable_model)).is_err());
4007
4008 let mut wrong_model_path = decoded_catalogue();
4009 wrong_model_path["reviewed_lines"][0]["field_evidence"][8]["model_sources"][1]["path"] =
4010 serde_json::json!("pkg/specgen/not-volumes.go");
4011 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_model_path)).is_err());
4012
4013 let mut wrong_handler = decoded_catalogue();
4014 wrong_handler["reviewed_lines"][0]["field_evidence"][9]["handler_source"]["repository"] =
4015 serde_json::json!("containers-common");
4016 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_handler)).is_err());
4017
4018 let mut mutable_handler = decoded_catalogue();
4019 mutable_handler["reviewed_lines"][0]["field_evidence"][9]["handler_source"]["revision"] =
4020 serde_json::json!("main");
4021 assert!(parse_renderer_catalogue(&encoded_catalogue(&mutable_handler)).is_err());
4022
4023 let mut wrong_handler_path = decoded_catalogue();
4024 wrong_handler_path["reviewed_lines"][0]["field_evidence"][9]["handler_source"]["path"] =
4025 serde_json::json!("pkg/api/handlers/libpod/not-containers.go");
4026 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_handler_path)).is_err());
4027
4028 let mut missing_line = decoded_catalogue();
4029 missing_line["reviewed_lines"]
4030 .as_array_mut()
4031 .expect("reviewed lines")
4032 .remove(0);
4033 assert!(parse_renderer_catalogue(&encoded_catalogue(&missing_line)).is_err());
4034
4035 let mut substituted_line = decoded_catalogue();
4036 let lines = substituted_line["reviewed_lines"]
4037 .as_array_mut()
4038 .expect("reviewed lines");
4039 lines[0] = lines[1].clone();
4040 assert!(parse_renderer_catalogue(&encoded_catalogue(&substituted_line)).is_err());
4041 }
4042
4043 #[test]
4044 fn renderer_catalogue_rejects_b2_field_mutations_and_fabricated_pre_six_support() {
4045 const FIRST_B2_FIELD: usize = 9;
4046 const NETWORK_ORDER_FIELD: usize = 29;
4047 const ROUTE_TYPE_BLACKHOLE_FIELD: usize = 37;
4048
4049 let mut missing = decoded_catalogue();
4050 missing["reviewed_lines"][0]["field_evidence"]
4051 .as_array_mut()
4052 .expect("field evidence")
4053 .remove(FIRST_B2_FIELD);
4054 assert!(parse_renderer_catalogue(&encoded_catalogue(&missing)).is_err());
4055
4056 let mut duplicate = decoded_catalogue();
4057 let fields = duplicate["reviewed_lines"][0]["field_evidence"]
4058 .as_array_mut()
4059 .expect("field evidence");
4060 fields.push(fields[FIRST_B2_FIELD].clone());
4061 assert!(parse_renderer_catalogue(&encoded_catalogue(&duplicate)).is_err());
4062
4063 for (path, value) in [
4064 (["operation", "", ""], serde_json::json!("pod-create")),
4065 (["cli", "flag", ""], serde_json::json!("--publish")),
4066 (["cli", "value_shape", ""], serde_json::json!("port-mapping")),
4067 (["libpod", "json_member", ""], serde_json::json!("portmappings")),
4068 ] {
4069 let mut mutated = decoded_catalogue();
4070 let field = &mut mutated["reviewed_lines"][0]["field_evidence"][FIRST_B2_FIELD];
4071 if path[1].is_empty() {
4072 field[path[0]] = value;
4073 } else if path[2].is_empty() {
4074 field[path[0]][path[1]] = value;
4075 }
4076 assert!(parse_renderer_catalogue(&encoded_catalogue(&mutated)).is_err());
4077 }
4078
4079 for (path, value) in [
4080 (["repository", ""], serde_json::json!("containers-podman")),
4081 (["revision", ""], serde_json::json!("main")),
4082 (["path", ""], serde_json::json!("libnetwork/types/fabricated.go")),
4083 (
4084 ["module", "revision"],
4085 serde_json::json!("0000000000000000000000000000000000000000"),
4086 ),
4087 ] {
4088 let mut mutated = decoded_catalogue();
4089 let source = &mut mutated["reviewed_lines"][0]["field_evidence"][FIRST_B2_FIELD]["model_sources"][1];
4090 if path[1].is_empty() {
4091 source[path[0]] = value;
4092 } else {
4093 source[path[0]][path[1]] = value;
4094 }
4095 assert!(parse_renderer_catalogue(&encoded_catalogue(&mutated)).is_err());
4096 }
4097
4098 let mut fabricated_pre_six_support = decoded_catalogue();
4099 fabricated_pre_six_support["reviewed_lines"][0]["field_evidence"][NETWORK_ORDER_FIELD]["availability"] =
4100 serde_json::json!("exact");
4101 assert!(parse_renderer_catalogue(&encoded_catalogue(&fabricated_pre_six_support)).is_err());
4102
4103 let mut fabricated_pre_six_route_type = decoded_catalogue();
4104 fabricated_pre_six_route_type["reviewed_lines"][0]["field_evidence"][ROUTE_TYPE_BLACKHOLE_FIELD]["availability"] =
4105 serde_json::json!("exact");
4106 assert!(parse_renderer_catalogue(&encoded_catalogue(&fabricated_pre_six_route_type)).is_err());
4107
4108 for line_index in [3, 4, 5] {
4109 let mut substituted_repository = decoded_catalogue();
4110 substituted_repository["reviewed_lines"][line_index]["field_evidence"][FIRST_B2_FIELD]["model_sources"]
4111 [1]["repository"] = serde_json::json!("containers-common");
4112 assert!(parse_renderer_catalogue(&encoded_catalogue(&substituted_repository)).is_err());
4113 }
4114
4115 for line_index in [3, 4, 5, 6] {
4116 let mut wrong_common_path = decoded_catalogue();
4117 wrong_common_path["reviewed_lines"][line_index]["field_evidence"][FIRST_B2_FIELD]["model_sources"][1]["path"] =
4118 serde_json::json!("libnetwork/types/network.go");
4119 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_common_path)).is_err());
4120 }
4121
4122 let mut missing_network_order_wire_model = decoded_catalogue();
4123 missing_network_order_wire_model["reviewed_lines"][5]["field_evidence"][NETWORK_ORDER_FIELD]["model_sources"]
4124 [0]["path"] = serde_json::json!("pkg/specgen/namespaces.go");
4125 assert!(parse_renderer_catalogue(&encoded_catalogue(&missing_network_order_wire_model)).is_err());
4126 }
4127
4128 #[test]
4129 fn renderer_catalogue_rejects_b3_claim_and_per_release_evidence_mutations() {
4130 let mut missing_claim = decoded_catalogue();
4131 missing_claim["runtime_field_claims"]
4132 .as_array_mut()
4133 .expect("runtime claims")
4134 .pop();
4135 assert!(parse_renderer_catalogue(&encoded_catalogue(&missing_claim)).is_err());
4136
4137 let mut duplicate_claim = decoded_catalogue();
4138 let claims = duplicate_claim["runtime_field_claims"]
4139 .as_array_mut()
4140 .expect("runtime claims");
4141 claims.push(claims[0].clone());
4142 assert!(parse_renderer_catalogue(&encoded_catalogue(&duplicate_claim)).is_err());
4143
4144 for (path, value) in [
4145 (["cli", "flag"], serde_json::json!("--health-timeout")),
4146 (["cli", "value_shape"], serde_json::json!("health-duration")),
4147 (["libpod", "json_member"], serde_json::json!("healthconfig_timeout")),
4148 ] {
4149 let mut substituted = decoded_catalogue();
4150 substituted["runtime_field_claims"][1][path[0]][path[1]] = value;
4151 assert!(parse_renderer_catalogue(&encoded_catalogue(&substituted)).is_err());
4152 }
4153
4154 for (claim_index, wrong_shape) in [
4155 (0, "boolean"),
4156 (6, "boolean"),
4157 (7, "boolean"),
4158 (12, "boolean"),
4159 (13, "boolean"),
4160 (14, "boolean"),
4161 (15, "string"),
4162 (16, "boolean"),
4163 (21, "boolean"),
4164 (25, "boolean"),
4165 (30, "boolean"),
4166 ] {
4167 let mut substituted = decoded_catalogue();
4168 substituted["runtime_field_claims"][claim_index]["libpod"]["value_shape"] = serde_json::json!(wrong_shape);
4169 assert!(parse_renderer_catalogue(&encoded_catalogue(&substituted)).is_err());
4170 }
4171
4172 let mut fabricated_journald = decoded_catalogue();
4173 let fields = fabricated_journald["reviewed_lines"][0]["runtime_evidence"]["exact_fields"]
4174 .as_array_mut()
4175 .expect("exact runtime fields");
4176 fields.push(serde_json::json!("container-log-journald-labels"));
4177 let gated = fabricated_journald["reviewed_lines"][0]["runtime_evidence"]["target_gated_fields"]
4178 .as_array_mut()
4179 .expect("target-gated runtime fields");
4180 gated.retain(|field| field != "container-log-journald-labels");
4181 assert!(parse_renderer_catalogue(&encoded_catalogue(&fabricated_journald)).is_err());
4182
4183 let mut fabricated_unlimited = decoded_catalogue();
4184 let fields = fabricated_unlimited["reviewed_lines"][1]["runtime_evidence"]["exact_fields"]
4185 .as_array_mut()
4186 .expect("exact runtime fields");
4187 fields.push(serde_json::json!("container-rlimit-unlimited"));
4188 let gated = fabricated_unlimited["reviewed_lines"][1]["runtime_evidence"]["target_gated_fields"]
4189 .as_array_mut()
4190 .expect("target-gated runtime fields");
4191 gated.retain(|field| field != "container-rlimit-unlimited");
4192 assert!(parse_renderer_catalogue(&encoded_catalogue(&fabricated_unlimited)).is_err());
4193
4194 for (member, source) in [
4195 ("cli_flag_source", "cmd/podman/common/unknown.go"),
4196 ("cli_transform_source", "pkg/specgenutil/unknown.go"),
4197 ("command_route_source", "cmd/podman/containers/unknown.go"),
4198 ("route_source", "pkg/api/server/register_unknown.go"),
4199 ("handler_source", "pkg/api/handlers/libpod/unknown.go"),
4200 ] {
4201 let mut substituted = decoded_catalogue();
4202 substituted["reviewed_lines"][0]["runtime_evidence"][member]["path"] = serde_json::json!(source);
4203 assert!(parse_renderer_catalogue(&encoded_catalogue(&substituted)).is_err());
4204 }
4205
4206 let mut wrong_model = decoded_catalogue();
4207 wrong_model["reviewed_lines"][2]["runtime_evidence"]["model_sources"][1]["path"] =
4208 serde_json::json!("pkg/specgen/specgen.go");
4209 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_model)).is_err());
4210
4211 let mut wrong_revision = decoded_catalogue();
4212 wrong_revision["reviewed_lines"][6]["runtime_evidence"]["handler_source"]["revision"] =
4213 serde_json::json!("0000000000000000000000000000000000000000");
4214 assert!(parse_renderer_catalogue(&encoded_catalogue(&wrong_revision)).is_err());
4215 }
4216
4217 #[test]
4218 fn renderer_catalogue_rejects_b4_boundary_and_evidence_mutations() {
4219 let mut missing_exact = decoded_catalogue();
4220 missing_exact["reviewed_lines"][0]["b4_evidence"]["exact_fields"]
4221 .as_array_mut()
4222 .expect("B4 exact fields")
4223 .pop();
4224 assert!(parse_renderer_catalogue(&encoded_catalogue(&missing_exact)).is_err());
4225
4226 let mut duplicate_blocked = decoded_catalogue();
4227 let blocked = duplicate_blocked["reviewed_lines"][0]["b4_evidence"]["blocked_fields"]
4228 .as_array_mut()
4229 .expect("B4 blocked fields");
4230 blocked.push(blocked[0].clone());
4231 assert!(parse_renderer_catalogue(&encoded_catalogue(&duplicate_blocked)).is_err());
4232
4233 let mut fabricated_pre_five_six = decoded_catalogue();
4234 let target_gated = fabricated_pre_five_six["reviewed_lines"][0]["b4_evidence"]["target_gated_fields"]
4235 .as_array_mut()
4236 .expect("B4 target-gated fields");
4237 let volume_uid = target_gated.remove(0);
4238 fabricated_pre_five_six["reviewed_lines"][0]["b4_evidence"]["exact_fields"]
4239 .as_array_mut()
4240 .expect("B4 exact fields")
4241 .push(volume_uid);
4242 assert!(parse_renderer_catalogue(&encoded_catalogue(&fabricated_pre_five_six)).is_err());
4243
4244 let mut mutable_source = decoded_catalogue();
4245 mutable_source["reviewed_lines"][0]["b4_evidence"]["cli_sources"][0]["revision"] = serde_json::json!("main");
4246 assert!(parse_renderer_catalogue(&encoded_catalogue(&mutable_source)).is_err());
4247
4248 let mut incorrect_model = decoded_catalogue();
4249 incorrect_model["reviewed_lines"][0]["b4_evidence"]["model_sources"][0]["path"] =
4250 serde_json::json!("pkg/specgenutil/specgen.go");
4251 assert!(parse_renderer_catalogue(&encoded_catalogue(&incorrect_model)).is_err());
4252
4253 let mut unsupported_pod_mount = decoded_catalogue();
4254 let blocked = unsupported_pod_mount["reviewed_lines"][0]["b4_evidence"]["blocked_fields"]
4255 .as_array_mut()
4256 .expect("B4 blocked fields");
4257 blocked.retain(|field| field != "pod-infra-mount");
4258 unsupported_pod_mount["reviewed_lines"][0]["b4_evidence"]["exact_fields"]
4259 .as_array_mut()
4260 .expect("B4 exact fields")
4261 .push(serde_json::json!("pod-infra-mount"));
4262 assert!(parse_renderer_catalogue(&encoded_catalogue(&unsupported_pod_mount)).is_err());
4263 }
4264
4265 #[test]
4266 fn renderer_catalogue_rejects_unknown_and_duplicate_json_keys() {
4267 let unknown = RENDERING_CATALOGUE_JSON.replacen(
4268 "\"schema_version\": 8,",
4269 "\"schema_version\": 8, \"unexpected\": true,",
4270 1,
4271 );
4272 assert!(parse_renderer_catalogue(&unknown).is_err());
4273
4274 let duplicate_root = RENDERING_CATALOGUE_JSON.replacen(
4275 "\"schema_version\": 8,",
4276 "\"schema_version\": 8, \"schema_version\": 8,",
4277 1,
4278 );
4279 assert!(parse_renderer_catalogue(&duplicate_root).is_err());
4280
4281 let duplicate_nested =
4282 RENDERING_CATALOGUE_JSON.replacen("\"flag\": null,", "\"flag\": null, \"flag\": null,", 1);
4283 assert!(parse_renderer_catalogue(&duplicate_nested).is_err());
4284 }
4285
4286 #[test]
4287 fn host_alias_rendering_uses_podmans_single_hostname_separator_for_both_ip_families() {
4288 let ipv4 = HostAlias::new("192.0.2.53".parse().expect("IPv4"), "resolver.example").expect("host alias");
4289 assert_eq!(host_alias_value(&ipv4), "resolver.example:192.0.2.53");
4290 let ipv6 = HostAlias::new("2001:db8::53".parse().expect("IPv6"), "resolver-v6.example").expect("host alias");
4291 assert_eq!(host_alias_value(&ipv6), "resolver-v6.example:2001:db8::53");
4292 }
4293}