pub mod networkpolicy;
use std::collections::{BTreeMap, HashSet};
use crate::internal::NetworkPolicyIngress;
use crate::internal::networkpolicy::NetworkPolicy;
use crate::metadata::{Dependency, Metadata, Runtime, SvcPorts};
use crate::utils::{StringLike, StringOrNumber};
use itertools::Itertools;
use k8s_openapi::api::rbac::v1::PolicyRule;
use serde::{Deserialize, Serialize};
use slugify::slugify;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Runnable {
Deployment(Box<Deployment>),
StatefulSet(Box<StatefulSet>),
Once(Box<Job>),
OnAppUpdate(Box<Job>),
Cron(Box<CronJob>),
}
impl Runnable {
#[must_use]
pub fn into_container(self) -> Container {
match self {
Self::Deployment(dep) => dep.container,
Self::StatefulSet(set) => set.container,
Self::Once(job) | Self::OnAppUpdate(job) => job.container,
Self::Cron(job) => job.container,
}
}
#[must_use]
pub fn get_container(&self) -> &Container {
match self {
Self::Deployment(dep) => &dep.container,
Self::StatefulSet(set) => &set.container,
Self::Once(job) | Self::OnAppUpdate(job) => &job.container,
Self::Cron(job) => &job.container,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Volume {
Host(HostVolume),
Pvc(PvcVolume),
Secret(SecretMount),
EmptyDir(EmptyDirVolume),
}
impl Volume {
#[must_use]
pub fn get_name(&self) -> &str {
match self {
Self::Host(vol) => &vol.name,
Self::Pvc(vol) => &vol.name,
Self::Secret(vol) => &vol.name,
Self::EmptyDir(vol) => &vol.name,
}
}
#[must_use]
pub fn get_slug(&self) -> String {
match self {
Self::Host(vol) => {
format!("host-{}", slugify!(&vol.name))
}
Self::Pvc(vol) => {
let prefix = vol
.from_app
.as_ref()
.map(|a| slugify!(a))
.or_else(|| vol.from_ns.as_ref().map(|ns| slugify!(ns)))
.unwrap_or_else(|| "own".to_string());
format!("pvc-{}-{}", prefix, slugify!(&vol.name))
}
Self::Secret(vol) => {
format!("secret-{}", slugify!(&vol.name))
}
Self::EmptyDir(vol) => {
format!("emptydir-{}", slugify!(&vol.name))
}
}
}
#[must_use]
pub fn get_target_slug(&self) -> String {
match self {
Self::Host(vol) => {
format!("host-{}", slugify!(&vol.name))
}
Self::Pvc(vol) => {
format!("pvc-own-{}", slugify!(&vol.name))
}
Self::Secret(vol) => {
format!("secret-{}", slugify!(&vol.name))
}
Self::EmptyDir(vol) => {
format!("emptydir-{}", slugify!(&vol.name))
}
}
}
#[must_use]
pub const fn get_sub_path(&self) -> Option<&String> {
match self {
Self::Pvc(vol) => vol.sub_path.as_ref(),
Self::Host(_) | Self::Secret(_) | Self::EmptyDir(_) => None,
}
}
#[must_use]
pub fn get_mount_path(&self) -> &str {
match self {
Self::Host(vol) => &vol.mount_path,
Self::Pvc(vol) => &vol.mount_path,
Self::Secret(vol) => &vol.mount_path,
Self::EmptyDir(vol) => &vol.mount_path,
}
}
#[must_use]
pub const fn is_readonly(&self) -> bool {
match self {
Self::Host(vol) => vol.is_readonly,
Self::Pvc(vol) => vol.is_readonly,
Self::Secret(_) => true,
Self::EmptyDir(vol) => vol.is_readonly,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct HostVolume {
pub name: String,
pub host_path: String,
pub mount_path: String,
pub is_readonly: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct EmptyDirVolume {
pub name: String,
pub mount_path: String,
pub is_readonly: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct PvcVolume {
pub name: String,
pub mount_path: String,
pub sub_path: Option<String>,
pub is_readonly: bool,
pub from_app: Option<String>,
pub from_ns: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct SecretMount {
pub name: String,
pub mount_path: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct SecretRef {
pub secret: String,
pub key: String,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub enum EnvVar {
StringLike(StringLike),
SecretRef(SecretRef),
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct Ports {
pub udp: BTreeMap<u16, u16>,
pub tcp: BTreeMap<u16, u16>,
}
impl Ports {
pub fn extend(&mut self, other: Self) {
self.udp.extend(other.udp);
self.tcp.extend(other.tcp);
}
}
#[must_use]
pub fn port_config_for_pod_or_ingress(ports: Ports) -> Vec<networkpolicy::PortConfig> {
let mut port_configs = Vec::new();
for (_public_port, target_port) in ports.tcp {
port_configs.push(networkpolicy::PortConfig {
port: target_port,
protocol: networkpolicy::Protocol::Tcp,
});
}
for (_public_port, target_port) in ports.udp {
port_configs.push(networkpolicy::PortConfig {
port: target_port,
protocol: networkpolicy::Protocol::Udp,
});
}
port_configs
}
#[must_use]
pub fn port_config_for_service_egress_only(ports: Ports) -> Vec<networkpolicy::PortConfig> {
let mut port_configs = Vec::new();
for (public_port, _target_port) in ports.tcp {
port_configs.push(networkpolicy::PortConfig {
port: public_port,
protocol: networkpolicy::Protocol::Tcp,
});
}
for (public_port, _target_port) in ports.udp {
port_configs.push(networkpolicy::PortConfig {
port: public_port,
protocol: networkpolicy::Protocol::Udp,
});
}
port_configs
}
impl Ports {
#[must_use]
pub fn keys(&self) -> Vec<u16> {
self.tcp
.keys()
.copied()
.chain(self.udp.clone().keys().copied())
.sorted()
.dedup()
.collect()
}
#[must_use]
pub fn values(&self) -> Vec<u16> {
self.tcp
.values()
.copied()
.chain(self.udp.clone().values().copied())
.sorted()
.dedup()
.collect()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tcp.is_empty() && self.udp.is_empty()
}
pub fn append(&mut self, other: &Self) {
self.tcp.extend(other.tcp.clone());
self.udp.extend(other.udp.clone());
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct Service {
pub target_container: String,
pub r#type: ServiceType,
pub ports: Ports,
pub cluster_ip: Option<String>,
pub network_policy: Option<NetworkPolicyIngress>,
#[serde(default)]
pub traefik_h2c: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum ServiceType {
LoadBalancer,
ClusterIp,
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Container {
pub cap_add: Vec<String>,
pub cap_drop: Vec<String>,
pub command: Option<Vec<String>>,
pub entrypoint: Option<Vec<String>>,
pub environment: BTreeMap<String, EnvVar>,
pub hostname: Option<String>,
pub image: String,
pub privileged: bool,
pub restart: Option<String>,
pub stop_grace_period: Option<String>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub volumes: Vec<Volume>,
pub working_dir: Option<String>,
pub shm_size: Option<StringOrNumber>,
pub ulimits: Option<serde_json::Value>,
pub host_network: bool,
pub exposes: SvcPorts,
pub fs_group: Option<u32>,
pub service_account: Option<ServiceAccount>,
pub network_policy: Option<NetworkPolicy>,
pub has_permissions: HashSet<String>,
pub additional_labels: BTreeMap<String, String>,
#[serde(default)]
pub sysctls: BTreeMap<String, String>,
pub read_only_rootfs: Option<bool>,
pub tty: Option<bool>,
pub stdin_open: Option<bool>,
pub host_pid: Option<bool>,
pub dns_policy: Option<String>,
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct CronJob {
pub container: Container,
pub schedule: String,
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Job {
pub container: Container,
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Deployment {
pub container: Container,
#[serde(default)]
pub middlewares: BTreeMap<String, k8s_crds_traefik::MiddlewareSpec>,
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct StatefulSet {
pub container: Container,
#[serde(default)]
pub middlewares: BTreeMap<String, k8s_crds_traefik::MiddlewareSpec>,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
pub struct ServiceAccountInner {
pub rules: Vec<PolicyRule>,
pub builtin_roles: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
pub struct ServiceAccount {
pub cluster_rules: Vec<PolicyRule>,
pub builtin_cluster_roles: Vec<String>,
pub inner: ServiceAccountInner,
pub other_ns: BTreeMap<String, ServiceAccountInner>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct Secret {
pub name: String,
pub data: BTreeMap<String, Box<[u8]>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginType {
Context,
Source,
Runtime,
CustomResource,
Ingress,
DnsProvider,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginInfo {
pub display_name: String,
pub r#type: PluginType,
pub endpoint: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct CustomResource {
pub app: String,
pub plugin_id: String,
pub definition: serde_yaml::Value,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct InternalAppRepresentation {
pub metadata: Metadata,
pub containers: BTreeMap<String, Runnable>,
pub services: BTreeMap<String, Service>,
pub ingress: Vec<Ingress>,
pub plugins: BTreeMap<String, PluginInfo>,
pub secrets: Vec<Secret>,
pub custom_resources: Vec<CustomResource>,
pub app_network_policy: Option<NetworkPolicy>,
pub other: Vec<serde_yaml::Value>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Hash)]
pub enum IngressType {
HttpFallback,
#[default]
Https,
TlsTcp,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Hash)]
#[allow(clippy::struct_excessive_bools)]
pub struct Ingress {
pub target_service: Option<String>,
pub target_ns: Option<String>,
pub target_app: Option<String>,
pub path_prefix: Option<String>,
pub target_port: Option<u16>,
pub r#type: IngressType,
pub enable_compression: bool,
pub strip_prefix: bool,
pub auth_exclude: bool,
pub component: Option<String>,
#[serde(default)]
#[deprecated]
pub use_https: bool,
pub use_https_with_hostname: Option<String>,
#[serde(default)]
pub tls_insecure: bool,
}
impl InternalAppRepresentation {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new(
metadata: Metadata,
containers: BTreeMap<String, Runnable>,
services: BTreeMap<String, Service>,
ingress: Vec<Ingress>,
secrets: Vec<Secret>,
plugins: BTreeMap<String, PluginInfo>,
custom_resources: Vec<CustomResource>,
app_network_policy: Option<NetworkPolicy>,
other: Vec<serde_yaml::Value>,
) -> Self {
let mut tmp = Self {
metadata,
containers,
services,
ingress,
plugins,
secrets,
custom_resources,
app_network_policy,
other,
};
tmp.finalize();
tmp
}
pub fn finalize(&mut self) {
self.metadata.supports_ingress = !self.ingress.is_empty();
self.metadata.can_be_protected =
!self.ingress.is_empty() && self.ingress.iter().any(|i| !i.auth_exclude);
self.metadata.dependencies.append(
&mut self
.custom_resources
.iter()
.map(|cr| Dependency::OneDependency(cr.app.clone()))
.collect(),
);
if !self
.metadata
.allowed_scopes
.contains(&self.metadata.default_scope)
{
self.metadata
.allowed_scopes
.push(self.metadata.default_scope);
}
self.metadata.is_snapshot_compatible = Some(self.supports_snapshots());
}
#[must_use]
pub fn get_public_ports(&self) -> Vec<u16> {
let mut ports = Vec::new();
for service in self.services.values() {
if service.r#type == ServiceType::LoadBalancer {
let mut new_ports = service.ports.keys();
ports.append(&mut new_ports);
}
}
ports.sort_unstable();
ports.dedup();
ports
}
#[must_use]
pub const fn get_ingress(&self) -> &Vec<Ingress> {
&self.ingress
}
#[must_use]
pub fn into_ingress(self) -> Vec<Ingress> {
self.ingress
}
#[must_use]
pub const fn get_metadata(&self) -> &Metadata {
&self.metadata
}
pub const fn get_metadata_mut(&mut self) -> &mut Metadata {
&mut self.metadata
}
#[must_use]
pub fn into_metadata(self) -> Metadata {
self.metadata
}
#[must_use]
pub const fn get_plugins(&self) -> &BTreeMap<String, PluginInfo> {
&self.plugins
}
#[must_use]
pub fn into_plugins(self) -> BTreeMap<String, PluginInfo> {
self.plugins
}
pub fn set_runtime(&mut self, runtime: Runtime) {
self.metadata.runtime = runtime;
}
#[must_use]
pub fn supports_snapshots(&self) -> bool {
if let Some(supported) = self.metadata.is_snapshot_compatible {
return supported;
}
if !self.other.is_empty() || !self.custom_resources.is_empty() {
return false;
}
for container in self.containers.values() {
for volume in &container.get_container().volumes {
match volume {
Volume::Host(host_vol) => {
if !(host_vol.host_path.starts_with("/dev")
|| host_vol.host_path.starts_with("/sys")
|| host_vol.host_path.starts_with("/proc")
|| host_vol.host_path.starts_with("/run"))
{
return false;
}
}
Volume::Pvc(pvc_vol) => {
if pvc_vol.from_app.is_some() || pvc_vol.from_ns.is_some() {
return false;
}
}
Volume::Secret(_) => return false,
Volume::EmptyDir(_) => {}
}
}
}
true
}
}