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}
148
149impl PlanContext {
150    /// Returns a compact summary string for callers that want a human-readable overview.
151    pub fn summary(&self) -> String {
152        format!(
153            "Plan for {} @ {} (target {}): {} runners, {} channels, {} components",
154            self.plan.tenant,
155            self.plan.environment,
156            self.target.as_str(),
157            self.plan.runners.len(),
158            self.plan.channels.len(),
159            self.components.len()
160        )
161    }
162}
163
164/// Derived NATS/messaging hints.
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct MessagingContext {
167    pub logical_cluster: String,
168    pub replicas: u16,
169    pub admin_url: String,
170}
171
172/// Derived telemetry hints.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TelemetryContext {
175    pub otlp_endpoint: String,
176    pub resource_attributes: BTreeMap<String, String>,
177}
178
179/// Channel ingress hints for IaC rendering.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ChannelContext {
182    pub name: String,
183    pub kind: String,
184    pub ingress: Vec<String>,
185    pub oauth_required: bool,
186}
187
188/// Deployment hints used to resolve provider/strategy dispatch.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct DeploymentHints {
191    pub target: Target,
192    pub provider: String,
193    pub strategy: String,
194}
195
196/// Builds carrier resource attributes used by telemetry-aware deployments.
197pub fn build_telemetry_context(plan: &DeploymentPlan, config: &DeployerConfig) -> TelemetryContext {
198    let endpoint = plan
199        .telemetry
200        .as_ref()
201        .and_then(|t| t.suggested_endpoint.clone())
202        .or_else(|| config.telemetry_config().endpoint.clone())
203        .unwrap_or_else(|| "https://otel.greentic.ai".to_string());
204
205    let mut resource_attributes = BTreeMap::new();
206    resource_attributes.insert(
207        "service.name".to_string(),
208        format!("greentic-deployer-{}", config.provider.as_str()),
209    );
210    resource_attributes.insert(
211        "deployment.environment".to_string(),
212        config.environment.clone(),
213    );
214    resource_attributes.insert("greentic.tenant".to_string(), config.tenant.clone());
215
216    TelemetryContext {
217        otlp_endpoint: endpoint,
218        resource_attributes,
219    }
220}
221
222/// Builds channel ingress hints based on the plan data and resolved deployer config.
223pub fn build_channel_context(
224    plan: &DeploymentPlan,
225    config: &DeployerConfig,
226) -> Vec<ChannelContext> {
227    const DEFAULT_BASE_DOMAIN: &str = "deploy.greentic.ai";
228    let base_domain = config
229        .greentic
230        .deployer
231        .as_ref()
232        .and_then(|d| d.base_domain.as_deref())
233        .unwrap_or(DEFAULT_BASE_DOMAIN);
234    plan.channels
235        .iter()
236        .map(|channel| {
237            let ingress = format!(
238                "https://{}/ingress/{}/{}/{}",
239                base_domain, config.environment, config.tenant, channel.kind
240            );
241            ChannelContext {
242                name: channel.name.clone(),
243                kind: channel.kind.clone(),
244                ingress: vec![ingress],
245                oauth_required: matches!(
246                    channel.kind.as_str(),
247                    "slack" | "teams" | "webex" | "telegram" | "whatsapp"
248                ),
249            }
250        })
251        .collect()
252}
253
254/// Resolves the scope for a requirement, defaulting to the plan's tenant/environment when absent.
255pub fn requirement_scope(
256    requirement: &SecretRequirement,
257    plan_env: &str,
258    plan_tenant: &str,
259) -> SecretScope {
260    requirement.scope.clone().unwrap_or_else(|| SecretScope {
261        env: plan_env.to_string(),
262        tenant: plan_tenant.to_string(),
263        team: None,
264    })
265}
266
267/// Builds messaging hints using tenant/environment heuristics.
268pub fn build_messaging_context(plan: &DeploymentPlan) -> MessagingContext {
269    let logical_cluster = plan
270        .messaging
271        .as_ref()
272        .map(|m| m.logical_cluster.clone())
273        .unwrap_or_else(|| format!("nats-{}-{}", plan.environment, plan.tenant));
274    let replicas = if plan.environment.contains("prod") {
275        3
276    } else {
277        1
278    };
279    let admin_url = format!("https://nats.{}.{}.svc", plan.environment, plan.tenant);
280
281    MessagingContext {
282        logical_cluster,
283        replicas,
284        admin_url,
285    }
286}
287
288/// Creates a [`PlanContext`] bundle from the base deployment plan.
289pub fn assemble_plan(
290    plan: DeploymentPlan,
291    config: &DeployerConfig,
292    deployment: DeploymentHints,
293    external_components: Vec<String>,
294    components: Vec<PlannedComponent>,
295) -> PlanContext {
296    let telemetry = build_telemetry_context(&plan, config);
297    let messaging = build_messaging_context(&plan);
298    let channels = build_channel_context(&plan, config);
299    let secrets = plan.secrets.clone();
300    PlanContext {
301        plan,
302        target: deployment.target.clone(),
303        external_components,
304        components,
305        messaging,
306        telemetry,
307        channels,
308        secrets,
309        deployment,
310    }
311}