n5i-apps 0.7.0

Utils for working with n5i apps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
// 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};

use crate::utils::true_default;

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

#[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, Debug, PartialEq)]
#[serde(tag = "type")]
pub enum Setting {
    Enum {
        name: MultiLanguageItem,
        description: MultiLanguageItem,
        values: Vec<String>,
        default: Option<String>,
        #[serde(default)]
        immutable: bool,
    },
    String {
        name: MultiLanguageItem,
        description: MultiLanguageItem,
        default: Option<String>,
        max_len: Option<usize>,
        string_type: Option<StringType>,
        #[serde(default = "true_default")]
        required: bool,
        placeholder: Option<MultiLanguageItem>,
        #[serde(default)]
        immutable: bool,
    },
    Bool {
        name: MultiLanguageItem,
        description: MultiLanguageItem,
        default: bool,
        #[serde(default)]
        immutable: bool,
    },
    Int {
        name: MultiLanguageItem,
        description: MultiLanguageItem,
        default: Option<i64>,
        min: Option<i64>,
        max: Option<i64>,
        step_size: Option<i64>,
        placeholder: Option<MultiLanguageItem>,
        #[serde(default)]
        immutable: bool,
    },
    Float {
        name: MultiLanguageItem,
        description: MultiLanguageItem,
        default: Option<f64>,
        min: Option<f64>,
        max: Option<f64>,
        step_size: Option<f64>,
        placeholder: Option<MultiLanguageItem>,
        #[serde(default)]
        immutable: bool,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum StringType {
    #[serde(rename = "password")]
    Password,
    #[serde(rename = "email")]
    Email,
    #[serde(rename = "url")]
    Url,
    #[serde(rename = "ip")]
    Ip,
}

impl Setting {
    pub fn default_value(&self) -> serde_json::Value {
        match self {
            Setting::Enum { default, .. } => match default {
                Some(default) => serde_json::Value::String(default.clone()),
                None => serde_json::Value::Null,
            },
            Setting::String { default, .. } => match default {
                Some(default) => serde_json::Value::String(default.clone()),
                None => serde_json::Value::Null,
            },
            Setting::Bool { default, .. } => serde_json::Value::Bool(*default),
            Setting::Int { default, .. } => match default {
                Some(default) => serde_json::Value::Number(serde_json::Number::from(*default)),
                None => serde_json::Value::Null,
            },
            Setting::Float { default, .. } => match default {
                Some(default) => {
                    serde_json::Value::Number(serde_json::Number::from_f64(*default).unwrap())
                }
                None => serde_json::Value::Null,
            },
        }
    }
}

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

#[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 {
    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 {
    pub fn get_ns(&self, user: &str, app_id: &str) -> String {
        match self {
            AppScope::User => format!("{user}-{app_id}"),
            AppScope::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(AppScope::User),
            1 => Ok(AppScope::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)]
#[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, 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)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
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,
    /// Developer name -> their website
    pub developers: BTreeMap<String, String>,
    /// 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")]
    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,
    #[serde(default)]
    pub saas_compatibility: SaasCompatibility,
    /// 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>,
}

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