use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum DebugSessionError {
#[error("alien debug ({platform}): {message}")]
UnsupportedCredential { platform: String, message: String },
#[error("alien debug: {message}")]
UnsupportedPlatform { message: String },
#[error("alien debug ({platform}): {message}")]
IoError { platform: String, message: String },
#[error("alien debug ({platform}): {message}")]
TokenMintFailed { platform: String, message: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum DebugSessionResponse {
Push(PushDebugSession),
Pull(PullDebugSession),
Pending(PendingDebugSession),
PushTunnel(PushTunnelDebugSession),
RuntimeTunnel(RuntimeTunnelDebugSession),
}
impl DebugSessionResponse {
pub fn expires_at(&self) -> Option<&str> {
match self {
Self::Push(p) => p.expires_at.as_deref(),
Self::Pull(p) => p.expires_at.as_deref(),
Self::Pending(_) => None,
Self::PushTunnel(p) => p.expires_at.as_deref(),
Self::RuntimeTunnel(p) => p.expires_at.as_deref(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum DebugSessionKind {
Context,
RuntimeShell,
RuntimeExec,
}
impl Default for DebugSessionKind {
fn default() -> Self {
Self::Context
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeTunnelDebugSession {
pub session_id: String,
pub platform: String,
pub tunnel_url: String,
pub client_token: String,
pub kind: DebugSessionKind,
pub protocol_version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum RuntimeClientFrame {
StartShell {
cols: u16,
rows: u16,
},
StartExec {
command: Vec<String>,
timeout_ms: Option<u64>,
},
Stdin {
data_b64: String,
},
Resize {
cols: u16,
rows: u16,
},
CloseStdin,
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum RuntimeAgentFrame {
Started {
pid: Option<u32>,
detail: Option<String>,
},
Stdout {
data_b64: String,
},
Stderr {
data_b64: String,
},
Exit {
code: Option<i32>,
timed_out: bool,
output_truncated: bool,
},
Error {
message: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PushTunnelDebugSession {
pub session_id: String,
pub provider: String,
pub tunnel_url: String,
pub client_token: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PushDebugSession {
pub provider: String,
pub env: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files: Vec<DebugCredFile>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub setup_script: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugCredFile {
pub file_name: String,
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub env_var: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingDebugSession {
pub session_id: String,
pub poll_url: String,
#[serde(default)]
pub poll_interval_ms: u32,
pub deadline: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PullDebugSession {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub kubeconfig: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub env: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files: Vec<DebugCredFile>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aws_endpoint_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gcp_endpoint_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub azure_endpoint_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cloud_proxy_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
pub fn extract_aws_service_and_region(host: &str, fallback_region: &str) -> (&'static str, String) {
let labels: Vec<&str> = host.split('.').collect();
let Some(amz_idx) = labels.iter().rposition(|l| *l == "amazonaws") else {
return ("execute-api", fallback_region.to_string());
};
let (service, region) = match &labels[..amz_idx] {
[_bucket_or_subdomain @ .., service, region]
if region.contains('-') && service.len() <= 8 =>
{
(*service, region.to_string())
}
[_subdomain @ .., service] => (*service, fallback_region.to_string()),
_ => ("execute-api", fallback_region.to_string()),
};
let static_service: &'static str = match service {
"sts" => "sts",
"iam" => "iam",
"ec2" => "ec2",
"lambda" => "lambda",
"s3" => "s3",
"dynamodb" => "dynamodb",
"sqs" => "sqs",
"sns" => "sns",
"ecr" => "ecr",
"eks" => "eks",
"ecs" => "ecs",
"cloudformation" => "cloudformation",
"cloudwatch" => "monitoring",
"logs" => "logs",
"ssm" => "ssm",
"secretsmanager" => "secretsmanager",
"kms" => "kms",
"events" | "eventbridge" => "events",
"apigateway" => "apigateway",
"execute-api" => "execute-api",
_ => "execute-api",
};
(static_service, region)
}
#[cfg(test)]
mod aws_endpoint_parsing_tests {
use super::extract_aws_service_and_region;
#[test]
fn regional_service() {
assert_eq!(
extract_aws_service_and_region("ec2.us-east-1.amazonaws.com", "us-west-2"),
("ec2", "us-east-1".to_string())
);
}
#[test]
fn global_service() {
assert_eq!(
extract_aws_service_and_region("iam.amazonaws.com", "us-east-1"),
("iam", "us-east-1".to_string())
);
}
#[test]
fn s3_bucket_regional() {
assert_eq!(
extract_aws_service_and_region("mybucket.s3.us-east-1.amazonaws.com", "us-west-2"),
("s3", "us-east-1".to_string())
);
}
#[test]
fn unknown_host() {
assert_eq!(
extract_aws_service_and_region("internal.example.com", "us-east-1"),
("execute-api", "us-east-1".to_string())
);
}
}