n5i-apps 0.12.0-dev.1

Utils for working with n5i apps
// SPDX-FileCopyrightText: 2024-2026 The n5i Project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use std::collections::{BTreeMap, HashSet};

#[cfg(feature = "graphql")]
use async_graphql::scalar;
use n5i::PodSecurityStandard;
pub use n5i::utils::MultiLanguageItem;
use serde::{Deserialize, Serialize};

pub use n5i::settings::Setting;

use crate::utils::true_default;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum Dependency {
    OneDependency(String),
    AlternativeDependency(HashSet<String>),
}

#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct SvcPorts {
    pub udp: Vec<u16>,
    pub tcp: Vec<u16>,
}

#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct Permission {
    pub id: String,
    pub name: MultiLanguageItem,
    pub description: MultiLanguageItem,
    /// Other permissions this permission implies
    /// May also contain permissions of other apps
    pub includes: Vec<String>,
    /// Secrets (+ keys) accessible with this permission
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub secrets: BTreeMap<String, Vec<String>>,
    /// Makes this permission "invisible" (Hidden from the UI) if requested by the apps listed in this field
    /// The * wildcard can be used to hide from all apps
    pub hidden: Vec<String>,
    /// Includes access to certain services and ports on these services
    pub services: BTreeMap<String, SvcPorts>,
    /// For system apps, set this to true to allow all users to access this permission
    /// If false, only other system apps can access this permission
    pub open_to_all_users: bool,
    /// The volumes this permission grants access to
    #[serde(default)]
    pub volumes: Vec<VolumePermission>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct VolumePermission {
    pub volume: String,
    pub sub_paths: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "graphql", derive(async_graphql::Enum))]
pub enum VolumeAccessPolicy {
    ReadWriteOnce,
    #[default]
    ReadWriteMany,
    ReadWriteOncePod,
}

#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
#[cfg_attr(feature = "graphql", graphql(name = "VolumeDefinition"))]
pub struct Volume {
    pub minimum_size: u64,
    pub recommended_size: u64,
    pub name: MultiLanguageItem,
    pub description: MultiLanguageItem,
    #[serde(default)]
    pub access_policy: VolumeAccessPolicy,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum Runtime {
    #[serde(rename = "AppYml")]
    Kubernetes,
    Plugin(String),
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct InputDeclaration {
    pub label: MultiLanguageItem,
    pub description: MultiLanguageItem,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct StorePlugin {
    pub name: String,
    pub icon: String,
    pub description: String,
    pub id: String,
    pub inputs: BTreeMap<String, InputDeclaration>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct UiMenuEntry {
    pub name: MultiLanguageItem,
    // Icon name, must be either a raw SVG or a name of a heroicons icon
    pub icon: String,
    pub path: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct UiModule {
    pub menu_entries: Vec<UiMenuEntry>,
}

#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub struct Settings(pub BTreeMap<String, Setting>);

#[cfg(feature = "graphql")]
scalar!(Settings);

impl Settings {
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "graphql", derive(async_graphql::Enum))]
pub enum AppScope {
    #[default]
    User,
    System,
}

impl AppScope {
    #[must_use]
    pub fn get_ns(&self, user: &str, app_id: &str) -> String {
        match self {
            Self::User => format!("{user}-{app_id}"),
            Self::System => app_id.to_string(),
        }
    }
}

impl TryFrom<i32> for AppScope {
    type Error = String;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Self::User),
            1 => Ok(Self::System),
            _ => Err(format!("Invalid AppScope value: {value}")),
        }
    }
}

impl From<AppScope> for i32 {
    fn from(value: AppScope) -> Self {
        match value {
            AppScope::User => 0,
            AppScope::System => 1,
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AppComponent {
    /// The component id
    pub id: String,
    /// The name of the component
    pub name: MultiLanguageItem,
    /// A description of the component
    pub description: MultiLanguageItem,
    pub icon: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct IngressOption {
    /// The id of the plugin that provides this ingress option
    pub plugin_id: String,
    /// Unique identifier for the ingress option, used later when adding domains
    pub id: String,
    /// The name, in a map language -> name
    pub name: MultiLanguageItem,
    pub can_choose_domain: bool,
    /// If `can_choose_domain` is true, this is the placeholder for the domain input, in a map language -> placeholder
    pub domain_placeholder: MultiLanguageItem,
    /// The component this option is for
    pub component: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct PackagerDetails {
    pub name: String,
    pub website: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
#[allow(clippy::struct_excessive_bools)]
pub struct Metadata {
    /// The app id
    pub id: String,
    /// If this app is a clone of another app, this is the base app id
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub base_app: Option<String>,
    /// The name of the app
    pub name: MultiLanguageItem,
    /// The version of the app
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub version: semver::Version,
    /// The version of the app to display
    #[cfg_attr(feature = "graphql", graphql(name = "version"))]
    pub display_version: String,
    /// The category for the app
    pub category: MultiLanguageItem,
    /// A short tagline for the app
    pub tagline: MultiLanguageItem,
    /// A list of URLs to get screenshots from that will be shown in the app store
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub screenshots: Vec<String>,
    /// Developer name -> their website
    pub developers: BTreeMap<String, String>,
    /// The app packager
    pub packager: Option<PackagerDetails>,
    /// A description of the app
    pub description: MultiLanguageItem,
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    #[cfg_attr(feature = "graphql", graphql(skip))]
    /// Dependencies the app requires
    pub dependencies: Vec<Dependency>,
    /// Permissions this app has, without including permissions from individual containers
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub base_permissions: HashSet<String>,
    /// Permissions the app has
    /// If a permission is from an app that is not listed in the dependencies, it is considered optional
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub has_permissions: HashSet<String>,
    /// Permissions this app exposes
    pub exposes_permissions: Vec<Permission>,
    /// App repository name -> repo URL
    pub repos: BTreeMap<String, String>,
    /// A support link for the app
    pub support: String,
    /// A list of promo images for the apps
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub gallery: Vec<String>,
    /// The URL to the app icon
    pub icon: Option<String>,
    /// The path the "Open" link on the dashboard should lead to
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    /// The app's default username
    pub default_username: Option<String>,
    /// The app's default password.
    pub default_password: Option<String>,
    /// For "virtual" apps, the service the app implements
    #[serde(skip_serializing_if = "Option::is_none")]
    pub implements: Option<String>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub release_notes: BTreeMap<String, MultiLanguageItem>,
    /// The SPDX identifier of the app license
    pub license: String,
    /// Available settings for this app
    #[serde(default, skip_serializing_if = "Settings::is_empty")]
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub settings: Settings,
    /// Volumes this app exposes
    pub volumes: BTreeMap<String, Volume>,
    /// Ports this app uses
    /// Before installing, agent clients need to check if any of these ports are already in use by other apps
    /// If so, the user needs to be notified
    pub ports: Vec<u16>,
    /// Exported data shared with plugins (Map plugin -> data)
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub exported_data: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
    /// The runtime this app uses
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub runtime: Runtime,
    /// The scopes this app can be installed to
    pub allowed_scopes: Vec<AppScope>,
    /// The default scopes
    pub default_scope: AppScope,
    #[serde(default)]
    pub store_plugins: Vec<StorePlugin>,
    #[serde(default)]
    pub ui_module: Option<UiModule>,
    /// Whether the app supports ingress
    /// This is declared by agent based on whether an app's ingress vec is empty,
    /// setting it during generation has no effect
    #[serde(default = "true_default")]
    pub supports_ingress: bool,
    /// Whether this app can be protected
    /// Please note: If the app type is not "App", this field is ignored and will be treated as if it were false
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub can_be_protected: bool,
    /// True if the app receives ingress as TCP (without TLS being handled by Traefik) instead of HTTP
    pub raw_ingress: bool,
    /// URL to redirect to post-install
    pub post_install_redirect: Option<String>,
    #[serde(default)]
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub components: Vec<AppComponent>,
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub preload_type: Option<PreloadType>,
    /// True if the app requires special configuration of the cluster and can not run as-is
    pub requires_cluster_config: Option<bool>,
    /// If this is true, the app is not shown in the app store, but can still be updated
    pub deprecated: Option<bool>,
    #[serde(default)]
    pub ingress_options: Vec<IngressOption>,
    /// Whether the app supports multiple instances of it being installed
    /// If true, no other apps can depend on this app, and it can not contain plugins
    #[serde(default)]
    pub supports_multi_instance: bool,
    #[serde(default)]
    pub pod_security_standard: PodSecurityStandard,
    /// Whether this app supports snapshots by just keeping Volumes + Helm Chart
    pub is_snapshot_compatible: Option<bool>,
    /// If this app can be downgraded to an older version, without deleting data/restoring a
    /// snapshot, shot put the oldest possible downgrade version from this version here
    #[cfg_attr(feature = "graphql", graphql(skip))]
    pub downgrade_compatibility: Option<semver::Version>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum PreloadType {
    Nirvati,
    MainVolumeFromSubdir(String),
    MainVolumeFromRoot,
}