Skip to main content

palimpsest_paas_core/
lib.rs

1// Copyright 2026 Thousand Birds Inc.
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Shared models for the Palimpsest managed PaaS.
5//!
6//! This crate intentionally has no runtime dependencies on the existing
7//! Palimpsest server. It captures platform intent and agent command contracts
8//! so the PaaS can be built additively under `paas/`.
9
10use std::{collections::BTreeMap, fmt, str::FromStr};
11
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15pub const MIN_SUPPORTED_POSTGRES_MAJOR: u16 = 18;
16
17#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(try_from = "String", into = "String")]
19pub struct PostgresVersion {
20    major: u16,
21    original: String,
22}
23
24impl PostgresVersion {
25    pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
26        let original = value.into();
27        let major = parse_major(&original)?;
28        if major < MIN_SUPPORTED_POSTGRES_MAJOR {
29            return Err(ModelError::UnsupportedPostgresVersion {
30                version: original,
31                minimum_major: MIN_SUPPORTED_POSTGRES_MAJOR,
32            });
33        }
34        Ok(Self { major, original })
35    }
36
37    pub const fn major(&self) -> u16 {
38        self.major
39    }
40
41    pub fn as_str(&self) -> &str {
42        &self.original
43    }
44}
45
46impl fmt::Display for PostgresVersion {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(&self.original)
49    }
50}
51
52impl From<PostgresVersion> for String {
53    fn from(value: PostgresVersion) -> Self {
54        value.original
55    }
56}
57
58impl FromStr for PostgresVersion {
59    type Err = ModelError;
60
61    fn from_str(value: &str) -> Result<Self, Self::Err> {
62        Self::new(value)
63    }
64}
65
66impl TryFrom<String> for PostgresVersion {
67    type Error = ModelError;
68
69    fn try_from(value: String) -> Result<Self, Self::Error> {
70        Self::new(value)
71    }
72}
73
74fn parse_major(value: &str) -> Result<u16, ModelError> {
75    let trimmed = value.trim();
76    if trimmed.is_empty() {
77        return Err(ModelError::InvalidPostgresVersion(value.to_owned()));
78    }
79    let major = trimmed
80        .split(['.', '-'])
81        .next()
82        .ok_or_else(|| ModelError::InvalidPostgresVersion(value.to_owned()))?;
83    major
84        .parse()
85        .map_err(|_| ModelError::InvalidPostgresVersion(value.to_owned()))
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum DatabaseMode {
91    Managed,
92    Local,
93    External,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct EnvironmentSpec {
98    pub organization_id: String,
99    pub project_id: String,
100    pub environment_id: String,
101    pub region: String,
102    pub database: DatabaseSpec,
103    pub sync: SyncDeploymentSpec,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct Organization {
108    pub organization_id: String,
109    pub name: String,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct Project {
114    pub project_id: String,
115    pub organization_id: String,
116    pub name: String,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct Environment {
121    pub environment_id: String,
122    pub organization_id: String,
123    pub project_id: String,
124    pub name: String,
125    pub region: String,
126}
127
128impl EnvironmentSpec {
129    pub fn validate(&self) -> Result<(), ModelError> {
130        self.database.validate()
131    }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct DatabaseSpec {
136    pub mode: DatabaseMode,
137    pub postgres_version: Option<PostgresVersion>,
138    pub managed: Option<ManagedPostgresSpec>,
139}
140
141impl DatabaseSpec {
142    pub fn validate(&self) -> Result<(), ModelError> {
143        match self.mode {
144            DatabaseMode::Managed | DatabaseMode::Local => {
145                if self.postgres_version.is_none() {
146                    return Err(ModelError::MissingPostgresVersion {
147                        mode: self.mode.clone(),
148                    });
149                }
150            }
151            DatabaseMode::External => {}
152        }
153
154        if matches!(self.mode, DatabaseMode::Managed) && self.managed.is_none() {
155            return Err(ModelError::MissingManagedPostgresSpec);
156        }
157
158        Ok(())
159    }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct ManagedPostgresSpec {
164    pub cluster_id: String,
165    pub tier: String,
166    pub storage_gib: u32,
167    pub backup_policy: BackupPolicy,
168    pub maintenance_policy: MaintenancePolicy,
169    #[serde(default)]
170    pub roles: Vec<DatabaseRoleSpec>,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174pub struct BackupPolicy {
175    pub pitr_window_hours: u16,
176    pub base_backup_interval_hours: u16,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct MaintenancePolicy {
181    pub window: String,
182    pub auto_minor_upgrades: bool,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct SyncDeploymentSpec {
187    pub deployment_id: String,
188    pub config_version: String,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct ConfigVersion {
193    pub config_version: String,
194    pub environment_id: String,
195    pub rendered_hash: String,
196    pub status: ConfigVersionStatus,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub struct SyncDeployment {
201    pub deployment_id: String,
202    pub organization_id: String,
203    pub project_id: String,
204    pub environment_id: String,
205    pub managed_postgres_cluster_id: String,
206    pub config_version: String,
207    pub lifecycle_state: SyncDeploymentLifecycleState,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "snake_case")]
212pub enum SyncDeploymentLifecycleState {
213    Requested,
214    Starting,
215    Running,
216    Draining,
217    Stopped,
218    Failed,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct GatewayRoute {
223    pub host: String,
224    pub organization_id: String,
225    pub project_id: String,
226    pub environment_id: String,
227    pub sync_endpoint: String,
228    pub tls_policy: TlsPolicy,
229    pub rate_limit: RateLimitPolicy,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct Domain {
234    pub domain_id: String,
235    pub hostname: String,
236    pub organization_id: String,
237    pub project_id: String,
238    pub environment_id: String,
239    pub route_host: String,
240    pub verification_status: DomainVerificationStatus,
241    pub verification_token: String,
242    pub tls_status: DomainTlsStatus,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct IpAllowlistRule {
247    pub rule_id: String,
248    pub organization_id: String,
249    pub project_id: String,
250    pub environment_id: String,
251    pub name: String,
252    pub cidr: String,
253    pub purpose: IpAllowlistPurpose,
254    pub status: IpAllowlistStatus,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct StaticEgressIp {
259    pub egress_ip_id: String,
260    pub organization_id: String,
261    pub project_id: String,
262    pub environment_id: String,
263    pub region: String,
264    pub ip_address: String,
265    pub provider_ref: String,
266    pub status: StaticEgressIpStatus,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270pub struct MaintenanceWindow {
271    pub window_id: String,
272    pub organization_id: String,
273    pub project_id: String,
274    pub environment_id: String,
275    pub name: String,
276    pub day_of_week: MaintenanceDayOfWeek,
277    pub start_time: String,
278    pub duration_minutes: u32,
279    pub auto_minor_upgrades: bool,
280    pub status: MaintenanceWindowStatus,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284#[serde(rename_all = "snake_case")]
285pub enum TlsPolicy {
286    TerminateAtGateway,
287    MutualTlsToSync {
288        ca_secret_ref: String,
289        #[serde(default, skip_serializing_if = "Option::is_none")]
290        client_certificate_secret_ref: Option<String>,
291        #[serde(default, skip_serializing_if = "Option::is_none")]
292        client_private_key_secret_ref: Option<String>,
293        #[serde(default, skip_serializing_if = "Option::is_none")]
294        server_name: Option<String>,
295    },
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299pub struct RateLimitPolicy {
300    pub max_connections: u32,
301    pub max_requests_per_minute: u32,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(rename_all = "snake_case")]
306pub enum DomainVerificationStatus {
307    Pending,
308    Verified,
309    Failed,
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(rename_all = "snake_case")]
314pub enum DomainTlsStatus {
315    Pending,
316    Active,
317    Failed,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "snake_case")]
322pub enum IpAllowlistPurpose {
323    App,
324    Migration,
325    Support,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum IpAllowlistStatus {
331    Active,
332    Disabled,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "snake_case")]
337pub enum StaticEgressIpStatus {
338    Provisioning,
339    Active,
340    Retired,
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
344#[serde(rename_all = "snake_case")]
345pub enum MaintenanceDayOfWeek {
346    Monday,
347    Tuesday,
348    Wednesday,
349    Thursday,
350    Friday,
351    Saturday,
352    Sunday,
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(rename_all = "snake_case")]
357pub enum MaintenanceWindowStatus {
358    Active,
359    Disabled,
360}
361
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct DatabaseProxyRoute {
364    pub listen_addr: String,
365    pub upstream_addr: String,
366    pub organization_id: String,
367    pub project_id: String,
368    pub environment_id: String,
369    pub cluster_id: String,
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub policy: Option<DatabaseProxyPolicy>,
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub tls: Option<DatabaseProxyTlsConfig>,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
377pub struct DatabaseProxyPolicy {
378    #[serde(default)]
379    pub allowed_users: Vec<String>,
380    #[serde(default)]
381    pub allowed_databases: Vec<String>,
382    #[serde(default)]
383    pub forbidden_startup_parameters: Vec<String>,
384    #[serde(default)]
385    pub required_startup_parameters: BTreeMap<String, String>,
386    #[serde(default)]
387    pub forbidden_simple_query_verbs: Vec<String>,
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub max_simple_query_bytes: Option<u32>,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
393pub struct DatabaseProxyTlsConfig {
394    pub mode: DatabaseProxyTlsMode,
395    pub certificate_id: String,
396    pub common_name: String,
397    pub certificate_secret_ref: SecretRef,
398    pub private_key_secret_ref: SecretRef,
399    pub fingerprint_sha256: String,
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
403#[serde(rename_all = "snake_case")]
404pub enum DatabaseProxyTlsMode {
405    TerminateAtProxy,
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
409#[serde(rename_all = "snake_case")]
410pub enum ConfigVersionStatus {
411    Uploaded,
412    Validated,
413    Deployed,
414    Failed,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
418pub struct DatabaseRoleSpec {
419    pub name: String,
420    pub kind: DatabaseRoleKind,
421    pub secret_ref: Option<SecretRef>,
422    #[serde(default)]
423    pub privileges: Vec<String>,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct DatabaseRoleCredential {
428    pub name: String,
429    pub kind: DatabaseRoleKind,
430    pub password: String,
431    #[serde(default)]
432    pub privileges: Vec<String>,
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
436#[serde(rename_all = "snake_case")]
437pub enum DatabaseRoleKind {
438    App,
439    Migration,
440    Replication,
441    Support,
442}
443
444#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
445pub struct ManagedPostgresCluster {
446    pub cluster_id: String,
447    pub organization_id: String,
448    pub project_id: String,
449    pub environment_id: String,
450    pub region: String,
451    pub postgres_version: PostgresVersion,
452    pub tier: String,
453    pub storage_gib: u32,
454    pub lifecycle_state: ClusterLifecycleState,
455    pub host_assignment: Option<HostAssignment>,
456}
457
458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
459pub struct ManagedPostgresEndpoint {
460    pub environment_id: String,
461    pub active_cluster_id: String,
462    pub updated_by_failover_id: Option<String>,
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub database_proxy_listen_addr: Option<String>,
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub active_certificate_id: Option<String>,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
470pub struct ManagedPostgresEndpointCertificate {
471    pub certificate_id: String,
472    pub environment_id: String,
473    pub listen_addr: String,
474    pub common_name: String,
475    pub status: CertificateLifecycleState,
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    pub certificate_secret_ref: Option<SecretRef>,
478    pub private_key_secret_ref: SecretRef,
479    pub not_before: String,
480    pub not_after: String,
481    pub fingerprint_sha256: String,
482    pub issued_by: String,
483    pub error_message: Option<String>,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487pub struct ManagedPostgresEndpointCertificateBundle {
488    pub certificate: ManagedPostgresEndpointCertificate,
489    pub certificate_pem: String,
490    pub private_key_pem: String,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
494pub struct GatewayRouteMtlsBundle {
495    pub host: String,
496    pub environment_id: String,
497    pub ca_secret_ref: String,
498    pub ca_pem: String,
499    pub client_certificate_secret_ref: Option<String>,
500    pub client_certificate_pem: Option<String>,
501    pub client_private_key_secret_ref: Option<String>,
502    pub client_private_key_pem: Option<String>,
503    pub server_name: Option<String>,
504}
505
506#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
507pub struct ManagedPostgresCertificateAuthorityProvider {
508    pub ca_provider_id: String,
509    pub name: String,
510    pub kind: CertificateAuthorityProviderKind,
511    pub issuer_ref: String,
512    pub status: CertificateAuthorityProviderStatus,
513    pub default_for_managed_postgres: bool,
514    pub updated_at: String,
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
518#[serde(rename_all = "snake_case")]
519pub enum CertificateAuthorityProviderKind {
520    LocalDev,
521    Acme,
522    ExternalPki,
523}
524
525#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
526#[serde(rename_all = "snake_case")]
527pub enum CertificateAuthorityProviderStatus {
528    Active,
529    Disabled,
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
533#[serde(rename_all = "snake_case")]
534pub enum CertificateLifecycleState {
535    Provisioning,
536    Active,
537    Rotating,
538    Revoked,
539    Failed,
540}
541
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
543pub struct ManagedPostgresAcmeOrder {
544    pub order_id: String,
545    pub certificate_id: String,
546    pub environment_id: String,
547    pub ca_provider_id: String,
548    pub common_name: String,
549    pub challenge_type: AcmeChallengeType,
550    pub challenge_token: String,
551    pub key_authorization_secret_ref: SecretRef,
552    pub csr_secret_ref: SecretRef,
553    pub directory_url: String,
554    pub account_ref: String,
555    pub status: AcmeOrderStatus,
556    pub error_message: Option<String>,
557    pub created_at: String,
558    pub updated_at: String,
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
562pub enum AcmeChallengeType {
563    #[serde(rename = "http_01")]
564    Http01,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
568#[serde(rename_all = "snake_case")]
569pub enum AcmeOrderStatus {
570    PendingChallenge,
571    ReadyToFinalize,
572    Succeeded,
573    Failed,
574}
575
576#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
577pub struct ManagedPostgresBackup {
578    pub backup_id: String,
579    pub cluster_id: String,
580    pub status: BackupLifecycleState,
581    pub backup_dir: String,
582    pub error_message: Option<String>,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
586pub struct ManagedPostgresBackupArtifact {
587    pub artifact_id: String,
588    pub backup_id: String,
589    pub cluster_id: String,
590    pub provider: String,
591    pub object_uri: String,
592    pub manifest_path: String,
593    pub manifest_sha256: Option<String>,
594    pub size_bytes: Option<u64>,
595    pub status: BackupArtifactStatus,
596    pub created_at: String,
597    pub updated_at: String,
598    pub error_message: Option<String>,
599}
600
601#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
602#[serde(rename_all = "snake_case")]
603pub enum BackupArtifactStatus {
604    Pending,
605    Available,
606    Expired,
607    Deleted,
608    Failed,
609}
610
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612pub struct ManagedPostgresBackupRetentionPolicy {
613    pub cluster_id: String,
614    pub retention_days: u32,
615    pub keep_min_successful_backups: u32,
616    pub enabled: bool,
617    pub updated_at: String,
618}
619
620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621pub struct ManagedPostgresCloneRedactionPolicy {
622    pub policy_id: String,
623    pub organization_id: String,
624    pub project_id: String,
625    pub environment_id: String,
626    pub name: String,
627    pub status: CloneRedactionPolicyStatus,
628    pub rules: Vec<CloneRedactionRule>,
629    pub created_at: String,
630    pub updated_at: String,
631}
632
633#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
634#[serde(rename_all = "snake_case")]
635pub enum CloneRedactionPolicyStatus {
636    Active,
637    Disabled,
638}
639
640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
641pub struct CloneRedactionRule {
642    pub table_schema: String,
643    pub table_name: String,
644    pub column_name: String,
645    pub method: CloneRedactionMethod,
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub static_value: Option<String>,
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
651#[serde(rename_all = "snake_case")]
652pub enum CloneRedactionMethod {
653    Null,
654    StaticValue,
655    HashSha256,
656}
657
658#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
659pub struct ManagedPostgresSupportAccessSession {
660    pub session_id: String,
661    pub cluster_id: String,
662    pub organization_id: String,
663    pub project_id: String,
664    pub environment_id: String,
665    pub requested_by: String,
666    pub approved_by: Option<String>,
667    pub revoked_by: Option<String>,
668    pub reason: String,
669    pub ticket_ref: Option<String>,
670    pub status: SupportAccessStatus,
671    pub requested_at: String,
672    pub approved_at: Option<String>,
673    pub expires_at: String,
674    pub revoked_at: Option<String>,
675}
676
677#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
678#[serde(rename_all = "snake_case")]
679pub enum SupportAccessStatus {
680    Requested,
681    Active,
682    Revoked,
683    Expired,
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
687pub struct ManagedPostgresDeletionTombstone {
688    pub cluster_id: String,
689    pub organization_id: String,
690    pub project_id: String,
691    pub environment_id: String,
692    pub region: String,
693    pub postgres_version: PostgresVersion,
694    pub tier: String,
695    pub retained_backup_id: Option<String>,
696    pub deleted_at: String,
697    pub retention_expires_at: Option<String>,
698    pub unrecoverable_at: Option<String>,
699    pub expired_at: Option<String>,
700}
701
702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
703pub struct ManagedPostgresRestore {
704    pub restore_id: String,
705    pub source_cluster_id: String,
706    pub target_cluster_id: String,
707    pub target_environment_id: String,
708    pub backup_id: String,
709    pub status: RestoreLifecycleState,
710    pub redaction_policy_id: Option<String>,
711    pub recovery_target_lsn: Option<String>,
712    pub error_message: Option<String>,
713}
714
715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
716pub struct ManagedPostgresRestoreDrill {
717    pub drill_id: String,
718    pub source_cluster_id: String,
719    pub restore_id: String,
720    pub backup_id: String,
721    pub target_cluster_id: String,
722    pub status: RestoreLifecycleState,
723    pub error_message: Option<String>,
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
727pub struct ManagedPostgresWalArchiveSegment {
728    pub cluster_id: String,
729    pub segment_name: String,
730    pub status: WalArchiveSegmentStatus,
731    pub archive_dir: String,
732    pub error_message: Option<String>,
733}
734
735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
736pub struct ManagedPostgresPitrCheck {
737    pub check_id: String,
738    pub cluster_id: String,
739    pub backup_id: Option<String>,
740    pub status: PitrCheckStatus,
741    pub segment_count: u32,
742    pub first_segment: Option<String>,
743    pub latest_segment: Option<String>,
744    pub error_message: Option<String>,
745}
746
747#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
748pub struct ManagedPostgresFailover {
749    pub failover_id: String,
750    pub source_cluster_id: String,
751    pub target_cluster_id: String,
752    pub status: FailoverLifecycleState,
753    pub error_message: Option<String>,
754}
755
756#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
757pub struct ManagedPostgresStandby {
758    pub standby_id: String,
759    pub source_cluster_id: String,
760    pub target_cluster_id: String,
761    pub backup_id: String,
762    pub status: StandbyLifecycleState,
763    pub error_message: Option<String>,
764}
765
766#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
767pub struct ManagedPostgresStandbyCheck {
768    pub check_id: String,
769    pub standby_id: String,
770    pub source_cluster_id: String,
771    pub target_cluster_id: String,
772    pub slot_name: String,
773    pub max_lag_bytes: u64,
774    pub status: StandbyCheckStatus,
775    pub error_message: Option<String>,
776}
777
778#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
779pub struct ManagedPostgresRuntimeCheck {
780    pub check_id: String,
781    pub cluster_id: String,
782    pub status: RuntimeCheckStatus,
783    pub connection_count: u32,
784    pub max_connections: u32,
785    pub replication_slot_lag_bytes: Option<u64>,
786    pub long_running_query_count: u32,
787    pub blocked_lock_count: u32,
788    pub oldest_transaction_age_seconds: Option<u64>,
789    pub autovacuum_running: bool,
790    pub checked_at: String,
791    pub error_message: Option<String>,
792}
793
794#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
795pub struct ManagedPostgresMajorUpgrade {
796    pub upgrade_id: String,
797    pub cluster_id: String,
798    pub source_postgres_version: PostgresVersion,
799    pub target_postgres_version: PostgresVersion,
800    pub strategy: ManagedPostgresMajorUpgradeStrategy,
801    pub status: ManagedPostgresMajorUpgradeStatus,
802    pub command_id: Option<String>,
803    pub operation_id: Option<String>,
804    pub error_message: Option<String>,
805    pub created_at: String,
806    pub completed_at: Option<String>,
807}
808
809#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
810#[serde(rename_all = "snake_case")]
811pub enum ManagedPostgresMajorUpgradeStrategy {
812    LogicalReplicationCopy,
813    PgUpgradeCopy,
814}
815
816#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
817#[serde(rename_all = "snake_case")]
818pub enum ManagedPostgresMajorUpgradeStatus {
819    Running,
820    Succeeded,
821    Failed,
822    Cancelled,
823}
824
825#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
826#[serde(rename_all = "snake_case")]
827pub enum BackupLifecycleState {
828    Requested,
829    Running,
830    Succeeded,
831    Failed,
832    Deleted,
833}
834
835#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
836#[serde(rename_all = "snake_case")]
837pub enum RestoreLifecycleState {
838    Requested,
839    Running,
840    Succeeded,
841    Failed,
842}
843
844#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
845#[serde(rename_all = "snake_case")]
846pub enum WalArchiveSegmentStatus {
847    Running,
848    Succeeded,
849    Failed,
850}
851
852#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
853#[serde(rename_all = "snake_case")]
854pub enum PitrCheckStatus {
855    Succeeded,
856    Failed,
857}
858
859#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
860#[serde(rename_all = "snake_case")]
861pub enum FailoverLifecycleState {
862    Running,
863    Succeeded,
864    Failed,
865}
866
867#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
868#[serde(rename_all = "snake_case")]
869pub enum StandbyLifecycleState {
870    Running,
871    Succeeded,
872    Failed,
873}
874
875#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
876#[serde(rename_all = "snake_case")]
877pub enum StandbyCheckStatus {
878    Running,
879    Succeeded,
880    Failed,
881}
882
883#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
884#[serde(rename_all = "snake_case")]
885pub enum RuntimeCheckStatus {
886    Healthy,
887    Degraded,
888    Failed,
889}
890
891#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
892pub struct HostAssignment {
893    pub host_id: String,
894    pub data_dir: String,
895    pub port: u16,
896}
897
898#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
899#[serde(rename_all = "snake_case")]
900pub enum ClusterLifecycleState {
901    Requested,
902    Placing,
903    AllocatingStorage,
904    InitializingPostgres,
905    ConfiguringRoles,
906    ConfiguringReplication,
907    Restoring,
908    Resizing,
909    UpdatingPostgres,
910    Starting,
911    Verifying,
912    Ready,
913    Stopping,
914    Stopped,
915    Deleting,
916    Deleted,
917    Failed,
918}
919
920#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
921pub struct NodeAgentCommand {
922    pub command_id: String,
923    pub cluster_id: String,
924    pub action: NodeAgentAction,
925}
926
927#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
928pub struct QueuedNodeAgentCommand {
929    pub host_id: String,
930    pub operation_id: Option<String>,
931    pub command: NodeAgentCommand,
932    pub status: AgentCommandStatus,
933    pub attempts: u32,
934    pub last_error: Option<String>,
935    #[serde(skip_serializing_if = "Option::is_none")]
936    pub operation_token: Option<String>,
937}
938
939#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
940pub struct NodeAgentCommandResult {
941    pub command_id: String,
942    pub host_id: String,
943    pub status: AgentCommandStatus,
944    pub detail: Option<String>,
945    #[serde(skip_serializing_if = "Option::is_none")]
946    pub operation_token: Option<String>,
947    #[serde(default, skip_serializing_if = "Vec::is_empty")]
948    pub backup_artifacts: Vec<NodeAgentBackupArtifact>,
949}
950
951#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
952pub struct NodeAgentBackupArtifact {
953    pub backup_id: String,
954    pub provider: String,
955    pub object_uri: String,
956    pub manifest_path: String,
957    pub manifest_sha256: Option<String>,
958    pub size_bytes: Option<u64>,
959}
960
961#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
962#[serde(rename_all = "snake_case")]
963pub enum AgentCommandStatus {
964    Pending,
965    Running,
966    Succeeded,
967    Failed,
968    Cancelled,
969}
970
971#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
972#[serde(tag = "kind", rename_all = "snake_case")]
973pub enum NodeAgentAction {
974    PreparePostgres {
975        postgres_version: PostgresVersion,
976        data_dir: String,
977        port: u16,
978    },
979    StartPostgres,
980    StopPostgres,
981    DeletePostgresData {
982        data_dir: String,
983        tombstone_retention_days: Option<u32>,
984    },
985    ConfigurePostgresAccess {
986        data_dir: String,
987        port: u16,
988        database: String,
989        roles: Vec<DatabaseRoleCredential>,
990        publication: String,
991        replication_slot: String,
992    },
993    CreateCopyOnWriteDatabaseClone {
994        data_dir: String,
995        port: u16,
996        source_database: String,
997        target_database: String,
998        #[serde(default)]
999        terminate_source_connections: bool,
1000    },
1001    ReportStatus,
1002    CheckPostgresStandbyLag {
1003        source_data_dir: String,
1004        source_port: u16,
1005        database: String,
1006        slot_name: String,
1007        max_lag_bytes: u64,
1008    },
1009    RunBaseBackup {
1010        backup_id: String,
1011        data_dir: String,
1012        postgres_url: String,
1013        backup_dir: String,
1014    },
1015    DeleteBackupData {
1016        backup_id: String,
1017        backup_dir: String,
1018    },
1019    PrepareRestore {
1020        backup_id: String,
1021        backup_dir: String,
1022        data_dir: String,
1023        #[serde(default, skip_serializing_if = "Option::is_none")]
1024        target_port: Option<u16>,
1025        #[serde(default, skip_serializing_if = "Option::is_none")]
1026        database: Option<String>,
1027        restore_command: String,
1028        recovery_target_lsn: Option<String>,
1029        #[serde(skip_serializing_if = "Option::is_none")]
1030        redaction_policy: Option<ManagedPostgresCloneRedactionPolicy>,
1031    },
1032    PreparePostgresStandby {
1033        backup_id: String,
1034        backup_dir: String,
1035        data_dir: String,
1036        #[serde(default, skip_serializing_if = "Option::is_none")]
1037        target_port: Option<u16>,
1038        primary_conninfo: String,
1039        primary_slot_name: String,
1040        source_data_dir: String,
1041        source_port: u16,
1042        database: String,
1043        restore_command: String,
1044    },
1045    ArchiveWalSegment {
1046        source_path: String,
1047        archive_dir: String,
1048        segment_name: String,
1049    },
1050    PromotePostgresStandby {
1051        data_dir: String,
1052    },
1053    FencePostgresPrimary {
1054        data_dir: String,
1055    },
1056    ResizePostgresStorage {
1057        data_dir: String,
1058        storage_gib: u32,
1059    },
1060    UpdatePostgresMinor {
1061        data_dir: String,
1062        target_postgres_version: PostgresVersion,
1063    },
1064    UpgradePostgresMajor {
1065        data_dir: String,
1066        #[serde(default, skip_serializing_if = "Option::is_none")]
1067        source_port: Option<u16>,
1068        #[serde(default, skip_serializing_if = "Option::is_none")]
1069        database: Option<String>,
1070        source_postgres_version: PostgresVersion,
1071        target_postgres_version: PostgresVersion,
1072        strategy: ManagedPostgresMajorUpgradeStrategy,
1073    },
1074    StartSyncDeployment {
1075        deployment_id: String,
1076        config_version: String,
1077    },
1078    StopSyncDeployment {
1079        deployment_id: String,
1080    },
1081    ReportSyncDeployment {
1082        deployment_id: String,
1083    },
1084}
1085
1086#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1087pub struct NodeAgentStatus {
1088    pub host_id: String,
1089    pub cluster_id: String,
1090    pub observed_state: ClusterLifecycleState,
1091    pub postgres_running: bool,
1092    pub postgres_version: Option<PostgresVersion>,
1093    pub backup_status: Option<BackupStatus>,
1094    pub wal_archive_status: Option<WalArchiveStatus>,
1095    pub detail: Option<String>,
1096}
1097
1098#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1099pub struct NodeHost {
1100    pub host_id: String,
1101    pub region: String,
1102    pub failure_domain: String,
1103    pub data_root: String,
1104    pub first_port: u16,
1105    pub state: NodeHostState,
1106    pub capacity: NodeHostCapacity,
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1110pub struct NodeHostHardeningCheck {
1111    pub check_id: String,
1112    pub host_id: String,
1113    pub status: NodeHostHardeningStatus,
1114    pub image_ref: String,
1115    pub os_release: String,
1116    pub kernel_version: String,
1117    pub postgres_major_min: u16,
1118    pub container_runtime: String,
1119    pub disk_encryption: bool,
1120    pub firewall_enabled: bool,
1121    pub unattended_upgrades: bool,
1122    pub last_patched_at: Option<String>,
1123    pub checked_at: String,
1124    pub error_message: Option<String>,
1125}
1126
1127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1128#[serde(rename_all = "snake_case")]
1129pub enum NodeHostHardeningStatus {
1130    Passing,
1131    Warning,
1132    Failing,
1133}
1134
1135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1136pub struct NodeHostAgentCredential {
1137    pub host_id: String,
1138    pub key_id: String,
1139    pub state: NodeHostAgentCredentialState,
1140    pub created_at: String,
1141    pub rotated_at: Option<String>,
1142    pub revoked_at: Option<String>,
1143    pub last_used_at: Option<String>,
1144    pub last_used_operation: Option<String>,
1145}
1146
1147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1148#[serde(rename_all = "snake_case")]
1149pub enum NodeHostAgentCredentialState {
1150    Active,
1151    Rotated,
1152    Revoked,
1153}
1154
1155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1156pub struct NodeHostHeartbeat {
1157    pub state: NodeHostState,
1158    pub capacity: NodeHostCapacity,
1159    #[serde(default)]
1160    pub observed_clusters: Vec<NodeHostClusterObservation>,
1161    #[serde(default)]
1162    pub observed_sync_deployments: Vec<NodeHostSyncDeploymentObservation>,
1163}
1164
1165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1166pub struct NodeHostClusterObservation {
1167    pub cluster_id: String,
1168    pub data_dir: String,
1169    pub postgres_running: bool,
1170}
1171
1172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1173pub struct NodeHostSyncDeploymentObservation {
1174    pub deployment_id: String,
1175    pub running: bool,
1176}
1177
1178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1179#[serde(rename_all = "snake_case")]
1180pub enum NodeHostState {
1181    Registering,
1182    Active,
1183    Draining,
1184    Maintenance,
1185    Offline,
1186}
1187
1188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1189pub struct NodeHostCapacity {
1190    pub max_clusters: u32,
1191    pub assigned_clusters: u32,
1192    pub storage_gib: u32,
1193    pub used_storage_gib: u32,
1194}
1195
1196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1197pub struct OperationRecord {
1198    pub operation_id: String,
1199    pub idempotency_key: String,
1200    pub target_resource_id: String,
1201    pub kind: OperationKind,
1202    pub status: OperationStatus,
1203    pub current_step: String,
1204    pub lease_owner: Option<String>,
1205}
1206
1207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1208#[serde(rename_all = "snake_case")]
1209pub enum OperationKind {
1210    CreateCluster,
1211    StartCluster,
1212    StopCluster,
1213    ResizeCluster,
1214    UpdateCluster,
1215    RotateCredentials,
1216    BackupCluster,
1217    DeleteBackup,
1218    ArchiveWalSegment,
1219    RestoreCluster,
1220    CreateDatabaseClone,
1221    PrepareStandby,
1222    CheckStandby,
1223    FencePrimary,
1224    FailoverCluster,
1225    DeleteCluster,
1226    StartSyncDeployment,
1227    StopSyncDeployment,
1228}
1229
1230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1231#[serde(rename_all = "snake_case")]
1232pub enum OperationStatus {
1233    Pending,
1234    Running,
1235    Succeeded,
1236    Failed,
1237    Cancelled,
1238}
1239
1240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1241pub struct AuditEvent {
1242    pub event_id: String,
1243    pub actor_id: String,
1244    pub action: String,
1245    pub resource_id: String,
1246    pub occurred_at: String,
1247}
1248
1249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1250pub struct ApiKey {
1251    pub key_id: String,
1252    pub token_prefix: String,
1253    pub name: String,
1254    pub organization_id: Option<String>,
1255    pub project_id: Option<String>,
1256    pub environment_id: Option<String>,
1257    pub role: TeamRole,
1258    pub created_by: String,
1259    pub revoked: bool,
1260}
1261
1262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1263pub struct TeamMembership {
1264    pub organization_id: String,
1265    pub actor_id: String,
1266    pub role: TeamRole,
1267}
1268
1269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1270pub struct SecretEncryptionKey {
1271    pub key_ref: String,
1272    pub provider: String,
1273    pub purpose: String,
1274    pub status: SecretEncryptionKeyStatus,
1275    pub created_at: String,
1276    pub activated_at: Option<String>,
1277    pub retired_at: Option<String>,
1278}
1279
1280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1281#[serde(rename_all = "snake_case")]
1282pub enum SecretEncryptionKeyStatus {
1283    Active,
1284    Retiring,
1285    Retired,
1286}
1287
1288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1289pub struct SecretRewrapPlan {
1290    pub plan_id: String,
1291    pub source_key_ref: String,
1292    pub target_key_ref: String,
1293    pub status: SecretRewrapPlanStatus,
1294    pub matched_secret_count: u32,
1295    pub rewrapped_secret_count: u32,
1296    pub error_message: Option<String>,
1297    pub created_at: String,
1298    pub completed_at: Option<String>,
1299}
1300
1301#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1302#[serde(rename_all = "snake_case")]
1303pub enum SecretRewrapPlanStatus {
1304    Planned,
1305    Running,
1306    Succeeded,
1307    Failed,
1308}
1309
1310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1311pub struct JwtIssuer {
1312    pub issuer_id: String,
1313    pub organization_id: String,
1314    pub project_id: Option<String>,
1315    pub environment_id: Option<String>,
1316    pub name: String,
1317    pub issuer: String,
1318    pub audience: String,
1319    pub jwks_url: String,
1320    pub claim_to_field: Vec<JwtClaimMapping>,
1321    pub status: JwtIssuerStatus,
1322}
1323
1324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1325pub struct WebhookEndpoint {
1326    pub endpoint_id: String,
1327    pub organization_id: String,
1328    pub project_id: Option<String>,
1329    pub environment_id: Option<String>,
1330    pub name: String,
1331    pub url: String,
1332    pub event_types: Vec<String>,
1333    pub signing_secret_ref: Option<SecretRef>,
1334    pub status: WebhookEndpointStatus,
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1338pub struct SsoIdentityProvider {
1339    pub provider_id: String,
1340    pub organization_id: String,
1341    pub name: String,
1342    pub kind: SsoProviderKind,
1343    pub issuer: String,
1344    pub sso_url: String,
1345    pub certificate_secret_ref: Option<SecretRef>,
1346    pub claim_mappings: Vec<SsoClaimMapping>,
1347    pub status: SsoProviderStatus,
1348}
1349
1350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1351pub struct Incident {
1352    pub incident_id: String,
1353    pub organization_id: String,
1354    pub project_id: Option<String>,
1355    pub environment_id: Option<String>,
1356    pub title: String,
1357    pub summary: String,
1358    pub severity: IncidentSeverity,
1359    pub status: IncidentStatus,
1360    #[serde(default)]
1361    pub impacted_services: Vec<String>,
1362    pub started_at: String,
1363    pub resolved_at: Option<String>,
1364}
1365
1366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1367pub struct SsoClaimMapping {
1368    pub claim: String,
1369    pub field: String,
1370}
1371
1372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1373pub struct JwtClaimMapping {
1374    pub claim: String,
1375    pub field: String,
1376}
1377
1378#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1379#[serde(rename_all = "snake_case")]
1380pub enum TeamRole {
1381    Owner,
1382    Admin,
1383    Developer,
1384    Viewer,
1385    Ci,
1386}
1387
1388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1389#[serde(rename_all = "snake_case")]
1390pub enum JwtIssuerStatus {
1391    Active,
1392    Disabled,
1393}
1394
1395#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1396#[serde(rename_all = "snake_case")]
1397pub enum WebhookEndpointStatus {
1398    Active,
1399    Disabled,
1400}
1401
1402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1403#[serde(rename_all = "snake_case")]
1404pub enum SsoProviderKind {
1405    Saml,
1406    Oidc,
1407}
1408
1409#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1410#[serde(rename_all = "snake_case")]
1411pub enum SsoProviderStatus {
1412    Active,
1413    Disabled,
1414}
1415
1416#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1417#[serde(rename_all = "snake_case")]
1418pub enum IncidentSeverity {
1419    Info,
1420    Warning,
1421    Critical,
1422}
1423
1424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1425#[serde(rename_all = "snake_case")]
1426pub enum IncidentStatus {
1427    Investigating,
1428    Identified,
1429    Monitoring,
1430    Resolved,
1431}
1432
1433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1434pub struct UsageEvent {
1435    pub event_id: String,
1436    pub idempotency_key: String,
1437    pub organization_id: String,
1438    pub project_id: String,
1439    pub environment_id: String,
1440    pub metric: String,
1441    pub quantity: u64,
1442    pub occurred_at: String,
1443    #[serde(default, skip_serializing_if = "Option::is_none")]
1444    pub signature: Option<UsageEventSignature>,
1445}
1446
1447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1448pub struct UsageEventSignature {
1449    pub key_id: String,
1450    pub algorithm: String,
1451    pub signature: String,
1452}
1453
1454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1455pub struct QuotaPolicy {
1456    pub policy_id: String,
1457    pub organization_id: String,
1458    pub project_id: String,
1459    pub environment_id: String,
1460    pub metric: String,
1461    pub limit_quantity: u64,
1462    pub window_seconds: u64,
1463    pub enforcement: QuotaEnforcement,
1464}
1465
1466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1467pub struct QuotaAlert {
1468    pub alert_id: String,
1469    pub policy_id: String,
1470    pub organization_id: String,
1471    pub project_id: String,
1472    pub environment_id: String,
1473    pub metric: String,
1474    pub threshold_basis_points: u32,
1475    pub current_quantity: u64,
1476    pub limit_quantity: u64,
1477    pub window_seconds: u64,
1478    pub state: QuotaAlertState,
1479    pub last_evaluated_at: Option<String>,
1480    pub fired_at: Option<String>,
1481    pub resolved_at: Option<String>,
1482}
1483
1484#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1485pub struct QueryPermissionPolicy {
1486    pub policy_id: String,
1487    pub organization_id: String,
1488    pub project_id: String,
1489    pub environment_id: String,
1490    pub name: String,
1491    pub table_schema: String,
1492    pub table_name: String,
1493    pub operation: QueryPermissionOperation,
1494    pub principal_claim: String,
1495    pub predicate_sql: String,
1496    pub sample_context: serde_json::Value,
1497    pub status: QueryPermissionPolicyStatus,
1498}
1499
1500/// Per-environment permission-rule DSL document authored in the PaaS UI.
1501///
1502/// The `dsl` field holds the raw `palimpsest-permissions` TOML config; it is
1503/// stored verbatim so operators can keep drafts, and is compiled by the
1504/// verifier on demand (see the `/v1/permissions/verify` endpoint).
1505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1506pub struct PermissionRuleDocument {
1507    pub environment_id: String,
1508    pub organization_id: String,
1509    pub project_id: String,
1510    pub dsl: String,
1511    pub updated_at: Option<String>,
1512}
1513
1514#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1515#[serde(rename_all = "snake_case")]
1516pub enum QuotaEnforcement {
1517    Reject,
1518    Observe,
1519}
1520
1521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1522#[serde(rename_all = "snake_case")]
1523pub enum QuotaAlertState {
1524    Ok,
1525    Firing,
1526}
1527
1528#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1529#[serde(rename_all = "snake_case")]
1530pub enum QueryPermissionOperation {
1531    Read,
1532    Subscribe,
1533}
1534
1535#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1536#[serde(rename_all = "snake_case")]
1537pub enum QueryPermissionPolicyStatus {
1538    Draft,
1539    Active,
1540}
1541
1542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1543pub struct BillingExport {
1544    pub export_id: String,
1545    pub destination: String,
1546    pub delivery_ref: Option<String>,
1547    pub organization_id: Option<String>,
1548    pub project_id: Option<String>,
1549    pub environment_id: Option<String>,
1550    pub metric: Option<String>,
1551    pub occurred_at_from: Option<String>,
1552    pub occurred_at_to: Option<String>,
1553    pub event_count: u64,
1554    pub quantity_total: u64,
1555    pub status: BillingExportStatus,
1556    pub error_message: Option<String>,
1557    pub events: Vec<UsageEvent>,
1558}
1559
1560#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1561#[serde(rename_all = "snake_case")]
1562pub enum BillingExportStatus {
1563    Succeeded,
1564    Failed,
1565}
1566
1567#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1568pub struct SecretRef {
1569    pub secret_id: String,
1570    pub provider: String,
1571    pub external_ref: String,
1572}
1573
1574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1575pub struct BackupStatus {
1576    pub last_base_backup_id: Option<String>,
1577    pub last_base_backup_at: Option<String>,
1578    pub last_successful_restore_check_at: Option<String>,
1579    pub pitr_window_hours: u16,
1580    pub state: BackupState,
1581}
1582
1583#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1584#[serde(rename_all = "snake_case")]
1585pub enum BackupState {
1586    Unknown,
1587    Healthy,
1588    Degraded,
1589    Failed,
1590}
1591
1592#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1593pub struct WalArchiveStatus {
1594    pub latest_archived_lsn: Option<String>,
1595    pub latest_archived_at: Option<String>,
1596    pub archive_lag_bytes: Option<u64>,
1597    pub state: WalArchiveState,
1598}
1599
1600#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1601#[serde(rename_all = "snake_case")]
1602pub enum WalArchiveState {
1603    Unknown,
1604    Streaming,
1605    Lagging,
1606    Failed,
1607}
1608
1609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1610pub struct CustomerEnvironmentHealth {
1611    pub environment_id: String,
1612    pub state: CustomerEnvironmentHealthState,
1613    pub summary: String,
1614    pub components: Vec<CustomerEnvironmentHealthComponent>,
1615}
1616
1617#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1618pub struct CustomerEnvironmentHealthComponent {
1619    pub name: String,
1620    pub state: CustomerEnvironmentHealthState,
1621    pub detail: String,
1622}
1623
1624#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1625#[serde(rename_all = "snake_case")]
1626pub enum CustomerEnvironmentHealthState {
1627    Healthy,
1628    Degraded,
1629    AtRisk,
1630    Maintenance,
1631    Unavailable,
1632}
1633
1634#[derive(Debug, Error, PartialEq, Eq)]
1635pub enum ModelError {
1636    #[error("invalid postgres version '{0}'")]
1637    InvalidPostgresVersion(String),
1638    #[error("postgres version '{version}' is unsupported; managed and local PaaS require PostgreSQL {minimum_major}+")]
1639    UnsupportedPostgresVersion { version: String, minimum_major: u16 },
1640    #[error("database mode {mode:?} requires postgres_version")]
1641    MissingPostgresVersion { mode: DatabaseMode },
1642    #[error("managed database mode requires a managed postgres spec")]
1643    MissingManagedPostgresSpec,
1644}
1645
1646#[cfg(test)]
1647mod tests {
1648    use super::*;
1649
1650    #[test]
1651    fn postgres_version_accepts_eighteen_and_newer() {
1652        let version = PostgresVersion::new("18.1").expect("version should parse");
1653        assert_eq!(version.major(), 18);
1654
1655        let version = PostgresVersion::new("19-beta1").expect("version should parse");
1656        assert_eq!(version.major(), 19);
1657    }
1658
1659    #[test]
1660    fn postgres_version_rejects_pre_eighteen() {
1661        let err = PostgresVersion::new("17.5").expect_err("version should be rejected");
1662        assert_eq!(
1663            err,
1664            ModelError::UnsupportedPostgresVersion {
1665                version: "17.5".to_owned(),
1666                minimum_major: 18
1667            }
1668        );
1669    }
1670
1671    #[test]
1672    fn managed_database_requires_version_and_spec() {
1673        let spec = DatabaseSpec {
1674            mode: DatabaseMode::Managed,
1675            postgres_version: Some(PostgresVersion::new("18").expect("valid version")),
1676            managed: None,
1677        };
1678
1679        assert_eq!(spec.validate(), Err(ModelError::MissingManagedPostgresSpec));
1680    }
1681
1682    #[test]
1683    fn managed_postgres_spec_roles_default_when_omitted() {
1684        let raw = r#"{
1685            "cluster_id": "cluster_123",
1686            "tier": "dev",
1687            "storage_gib": 20,
1688            "backup_policy": {
1689                "pitr_window_hours": 72,
1690                "base_backup_interval_hours": 24
1691            },
1692            "maintenance_policy": {
1693                "window": "sun:04:00-05:00Z",
1694                "auto_minor_upgrades": true
1695            }
1696        }"#;
1697
1698        let spec: ManagedPostgresSpec = serde_json::from_str(raw).expect("spec parses");
1699
1700        assert!(spec.roles.is_empty());
1701    }
1702}