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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct InfraPlan {
106 pub target: Target,
107 pub profile: DeploymentProfile,
108 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PlanContext {
127 pub plan: DeploymentPlan,
129 pub target: Target,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub external_components: Vec<String>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub components: Vec<PlannedComponent>,
137 pub messaging: MessagingContext,
139 pub telemetry: TelemetryContext,
141 pub channels: Vec<ChannelContext>,
143 pub secrets: Vec<SecretRequirement>,
145 pub deployment: DeploymentHints,
147 #[serde(default)]
150 pub serviceless: bool,
151}
152
153impl PlanContext {
154 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct TelemetryContext {
179 pub otlp_endpoint: String,
180 pub resource_attributes: BTreeMap<String, String>,
181}
182
183#[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#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct DeploymentHints {
195 pub target: Target,
196 pub provider: String,
197 pub strategy: String,
198}
199
200pub 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
226pub 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
258pub 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
271pub 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
292pub 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}