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}
148
149impl PlanContext {
150 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TelemetryContext {
175 pub otlp_endpoint: String,
176 pub resource_attributes: BTreeMap<String, String>,
177}
178
179#[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#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct DeploymentHints {
191 pub target: Target,
192 pub provider: String,
193 pub strategy: String,
194}
195
196pub 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
222pub 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
254pub 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
267pub 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
288pub 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}