Skip to main content

greentic_deployer/
plan.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4use greentic_types::deployment::DeploymentPlan;
5use greentic_types::secrets::{SecretRequirement, SecretScope};
6
7use crate::config::{DeployerConfig, Provider};
8
9/// Generic component role derived from pack metadata and WIT worlds.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ComponentRole {
13    EventProvider,
14    EventBridge,
15    MessagingAdapter,
16    Worker,
17    Other,
18}
19
20impl ComponentRole {
21    pub fn as_str(&self) -> &'static str {
22        match self {
23            ComponentRole::EventProvider => "event_provider",
24            ComponentRole::EventBridge => "event_bridge",
25            ComponentRole::MessagingAdapter => "messaging_adapter",
26            ComponentRole::Worker => "worker",
27            ComponentRole::Other => "other",
28        }
29    }
30}
31
32/// Abstract deployment profile used to map onto target infrastructure.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum DeploymentProfile {
36    LongLivedService,
37    HttpEndpoint,
38    QueueConsumer,
39    ScheduledSource,
40    OneShotJob,
41}
42
43impl DeploymentProfile {
44    pub fn as_str(&self) -> &'static str {
45        match self {
46            DeploymentProfile::LongLivedService => "long_lived_service",
47            DeploymentProfile::HttpEndpoint => "http_endpoint",
48            DeploymentProfile::QueueConsumer => "queue_consumer",
49            DeploymentProfile::ScheduledSource => "scheduled_source",
50            DeploymentProfile::OneShotJob => "one_shot_job",
51        }
52    }
53}
54
55/// Supported deployment targets.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum Target {
59    Local,
60    Aws,
61    Azure,
62    Gcp,
63    K8s,
64}
65
66impl Target {
67    pub fn as_str(&self) -> &'static str {
68        match self {
69            Target::Local => "local",
70            Target::Aws => "aws",
71            Target::Azure => "azure",
72            Target::Gcp => "gcp",
73            Target::K8s => "k8s",
74        }
75    }
76}
77
78impl From<Provider> for Target {
79    fn from(value: Provider) -> Self {
80        match value {
81            Provider::Local => Target::Local,
82            Provider::Aws => Target::Aws,
83            Provider::Azure => Target::Azure,
84            Provider::Gcp => Target::Gcp,
85            Provider::K8s => Target::K8s,
86            Provider::Generic => Target::Local,
87        }
88    }
89}
90
91/// Per-component planning entry with inferred role/profile and infra summary.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct PlannedComponent {
94    pub id: String,
95    pub role: ComponentRole,
96    pub profile: DeploymentProfile,
97    pub target: Target,
98    pub infra: InfraPlan,
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub inference: Option<InferenceNotes>,
101}
102
103/// Target-specific infrastructure mapping for a component.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct InfraPlan {
106    pub target: Target,
107    pub profile: DeploymentProfile,
108    /// Short human-readable mapping summary (e.g. "api-gateway + lambda").
109    pub summary: String,
110    #[serde(default, skip_serializing_if = "Vec::is_empty")]
111    pub resources: Vec<String>,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub notes: Option<String>,
114}
115
116/// Inference details attached to a component entry.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct InferenceNotes {
119    pub source: String,
120    #[serde(default, skip_serializing_if = "Vec::is_empty")]
121    pub warnings: Vec<String>,
122}
123
124/// Provider-agnostic deployment plan bundle enriched with deployer hints.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PlanContext {
127    /// Canonical plan produced by `greentic-types`.
128    pub plan: DeploymentPlan,
129    /// Target selected for planning/rendering.
130    pub target: Target,
131    /// Components that expose inbound surfaces (messaging/http/events).
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub external_components: Vec<String>,
134    /// Per-component deployment mapping (role + profile).
135    #[serde(default, skip_serializing_if = "Vec::is_empty")]
136    pub components: Vec<PlannedComponent>,
137    /// Messaging hints inferred from tenant/environment.
138    pub messaging: MessagingContext,
139    /// Telemetry hints applied to generated artifacts.
140    pub telemetry: TelemetryContext,
141    /// Channel ingress and OAuth hints.
142    pub channels: Vec<ChannelContext>,
143    /// Logical secrets referenced by the deployment.
144    pub secrets: Vec<SecretRequirement>,
145    /// Deployment target hints (provider/strategy strings).
146    pub deployment: DeploymentHints,
147    /// True when no app bundle was provided; the deployment pack owns all
148    /// infrastructure decisions and the operator-module rewrite must be skipped.
149    #[serde(default)]
150    pub serviceless: bool,
151}
152
153impl PlanContext {
154    /// Returns a compact summary string for callers that want a human-readable overview.
155    pub fn summary(&self) -> String {
156        format!(
157            "Plan for {} @ {} (target {}): {} runners, {} channels, {} components",
158            self.plan.tenant,
159            self.plan.environment,
160            self.target.as_str(),
161            self.plan.runners.len(),
162            self.plan.channels.len(),
163            self.components.len()
164        )
165    }
166}
167
168/// Derived NATS/messaging hints.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct MessagingContext {
171    pub logical_cluster: String,
172    pub replicas: u16,
173    pub admin_url: String,
174}
175
176/// Derived telemetry hints.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct TelemetryContext {
179    pub otlp_endpoint: String,
180    pub resource_attributes: BTreeMap<String, String>,
181}
182
183/// Channel ingress hints for IaC rendering.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ChannelContext {
186    pub name: String,
187    pub kind: String,
188    pub ingress: Vec<String>,
189    pub oauth_required: bool,
190}
191
192/// Deployment hints used to resolve provider/strategy dispatch.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct DeploymentHints {
195    pub target: Target,
196    pub provider: String,
197    pub strategy: String,
198}
199
200/// Builds carrier resource attributes used by telemetry-aware deployments.
201pub fn build_telemetry_context(plan: &DeploymentPlan, config: &DeployerConfig) -> TelemetryContext {
202    let endpoint = plan
203        .telemetry
204        .as_ref()
205        .and_then(|t| t.suggested_endpoint.clone())
206        .or_else(|| config.telemetry_config().endpoint.clone())
207        .unwrap_or_else(|| "https://otel.greentic.ai".to_string());
208
209    let mut resource_attributes = BTreeMap::new();
210    resource_attributes.insert(
211        "service.name".to_string(),
212        format!("greentic-deployer-{}", config.provider.as_str()),
213    );
214    resource_attributes.insert(
215        "deployment.environment".to_string(),
216        config.environment.clone(),
217    );
218    resource_attributes.insert("greentic.tenant".to_string(), config.tenant.clone());
219
220    TelemetryContext {
221        otlp_endpoint: endpoint,
222        resource_attributes,
223    }
224}
225
226/// Builds channel ingress hints based on the plan data and resolved deployer config.
227pub fn build_channel_context(
228    plan: &DeploymentPlan,
229    config: &DeployerConfig,
230) -> Vec<ChannelContext> {
231    const DEFAULT_BASE_DOMAIN: &str = "deploy.greentic.ai";
232    let base_domain = config
233        .greentic
234        .deployer
235        .as_ref()
236        .and_then(|d| d.base_domain.as_deref())
237        .unwrap_or(DEFAULT_BASE_DOMAIN);
238    plan.channels
239        .iter()
240        .map(|channel| {
241            let ingress = format!(
242                "https://{}/ingress/{}/{}/{}",
243                base_domain, config.environment, config.tenant, channel.kind
244            );
245            ChannelContext {
246                name: channel.name.clone(),
247                kind: channel.kind.clone(),
248                ingress: vec![ingress],
249                oauth_required: matches!(
250                    channel.kind.as_str(),
251                    "slack" | "teams" | "webex" | "telegram" | "whatsapp"
252                ),
253            }
254        })
255        .collect()
256}
257
258/// Resolves the scope for a requirement, defaulting to the plan's tenant/environment when absent.
259pub fn requirement_scope(
260    requirement: &SecretRequirement,
261    plan_env: &str,
262    plan_tenant: &str,
263) -> SecretScope {
264    requirement.scope.clone().unwrap_or_else(|| SecretScope {
265        env: plan_env.to_string(),
266        tenant: plan_tenant.to_string(),
267        team: None,
268    })
269}
270
271/// Builds messaging hints using tenant/environment heuristics.
272pub fn build_messaging_context(plan: &DeploymentPlan) -> MessagingContext {
273    let logical_cluster = plan
274        .messaging
275        .as_ref()
276        .map(|m| m.logical_cluster.clone())
277        .unwrap_or_else(|| format!("nats-{}-{}", plan.environment, plan.tenant));
278    let replicas = if plan.environment.contains("prod") {
279        3
280    } else {
281        1
282    };
283    let admin_url = format!("https://nats.{}.{}.svc", plan.environment, plan.tenant);
284
285    MessagingContext {
286        logical_cluster,
287        replicas,
288        admin_url,
289    }
290}
291
292/// Creates a [`PlanContext`] bundle from the base deployment plan.
293pub fn assemble_plan(
294    plan: DeploymentPlan,
295    config: &DeployerConfig,
296    deployment: DeploymentHints,
297    external_components: Vec<String>,
298    components: Vec<PlannedComponent>,
299) -> PlanContext {
300    let telemetry = build_telemetry_context(&plan, config);
301    let messaging = build_messaging_context(&plan);
302    let channels = build_channel_context(&plan, config);
303    let secrets = plan.secrets.clone();
304    PlanContext {
305        plan,
306        target: deployment.target.clone(),
307        external_components,
308        components,
309        messaging,
310        telemetry,
311        channels,
312        secrets,
313        deployment,
314        serviceless: false,
315    }
316}