use super::*;
#[derive(Clone, Debug)]
pub struct Event {
pub at: Option<DateTime<Utc>>,
pub env: String,
pub application: String,
pub message: String,
pub severity: String,
pub version_label: Option<String>,
}
#[derive(Clone, Debug)]
pub struct Instance {
pub id: String,
pub health: String, pub color: String, pub causes: Vec<String>,
pub instance_type: String,
pub availability_zone: String,
pub launched_at: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug)]
pub struct Application {
pub name: String,
pub description: String,
pub date_created: Option<DateTime<Utc>>,
pub date_updated: Option<DateTime<Utc>>,
pub version_count: usize,
pub templates: Vec<String>,
pub latest_version_label: Option<String>,
pub latest_version_created: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug)]
pub struct CustomPlatform {
pub arn: String,
pub branch: String,
pub version: String,
pub status: String,
pub lifecycle: String,
}
#[derive(Clone, Debug)]
pub struct AppVersion {
pub label: String,
pub description: String,
pub created: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug)]
pub struct Environment {
pub name: String,
pub application: String,
pub status: String,
pub health: String,
pub platform: String, pub solution_stack: String,
pub tier: String, pub cname: String,
pub version_label: String,
pub arn: Option<String>,
pub updated: Option<DateTime<Utc>>,
pub id: Option<String>,
pub region: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct EnvInstanceCounts {
pub healthy: i32,
pub total: i32,
}
#[derive(Clone, Debug, Default)]
pub struct EnvResources {
pub asgs: Vec<String>,
pub instances: Vec<String>,
pub launch_configs: Vec<String>,
pub launch_templates: Vec<String>,
pub load_balancers: Vec<String>,
pub triggers: Vec<String>,
pub queues: Vec<EnvResourceQueue>,
}
#[derive(Clone, Debug)]
pub struct EnvResourceQueue {
pub name: String,
pub url: String,
}
#[derive(Clone, Debug)]
pub struct ConfigOption {
pub namespace: String,
pub name: String,
pub value: Option<String>,
pub default_value: Option<String>,
pub value_type: String,
pub value_options: Vec<String>,
pub change_severity: Option<String>,
#[allow(dead_code)]
pub user_defined: Option<bool>,
pub min_value: Option<i32>,
pub max_value: Option<i32>,
pub max_length: Option<i32>,
}
const EVENT_TAIL_MAX_PAGES: usize = 5;
pub(super) fn map_platform(p: aws_sdk_elasticbeanstalk::types::PlatformSummary) -> CustomPlatform {
CustomPlatform {
arn: p.platform_arn.unwrap_or_default(),
branch: p.platform_branch_name.unwrap_or_default(),
version: p.platform_version.unwrap_or_default(),
status: p
.platform_status
.map(|s| s.as_str().to_string())
.unwrap_or_default(),
lifecycle: p.platform_lifecycle_state.unwrap_or_default(),
}
}
pub(super) fn map_env(e: aws_sdk_elasticbeanstalk::types::EnvironmentDescription) -> Environment {
let solution_stack = e.solution_stack_name.clone().unwrap_or_default();
let raw_platform = e
.solution_stack_name
.clone()
.or(e.platform_arn.clone())
.unwrap_or_default();
let tier = e
.tier
.as_ref()
.and_then(|t| t.name.as_deref())
.map(normalize_tier)
.unwrap_or_else(|| "?".into());
Environment {
name: e.environment_name.unwrap_or_default(),
application: e.application_name.unwrap_or_default(),
status: e
.status
.map(|s| s.as_str().to_string())
.unwrap_or_else(|| "-".into()),
health: e
.health
.map(|h| h.as_str().to_string())
.unwrap_or_else(|| "-".into()),
platform: platform_family(&raw_platform),
solution_stack,
tier,
cname: e.cname.unwrap_or_default(),
version_label: e.version_label.unwrap_or_default(),
arn: e.environment_arn,
updated: e
.date_updated
.and_then(|d| DateTime::from_timestamp(d.secs(), d.subsec_nanos())),
id: e.environment_id,
region: None,
}
}
pub(crate) fn platform_branch_from(stack_or_arn: &str) -> String {
if stack_or_arn.starts_with("arn:") {
let parts: Vec<&str> = stack_or_arn.split('/').collect();
if parts.len() >= 2 {
return parts[parts.len() - 2].to_string();
}
return String::new();
}
if let Some(rest) = stack_or_arn.split(" running ").nth(1) {
return rest.trim().to_string();
}
String::new()
}
pub fn summarise_instance_health(
summary: Option<&aws_sdk_elasticbeanstalk::types::InstanceHealthSummary>,
) -> EnvInstanceCounts {
let Some(s) = summary else {
return EnvInstanceCounts::default();
};
let g = |v: Option<i32>| v.unwrap_or(0);
let ok = g(s.ok);
let info = g(s.info);
let healthy = ok + info;
let total = g(s.no_data)
+ g(s.unknown)
+ g(s.pending)
+ ok
+ info
+ g(s.warning)
+ g(s.degraded)
+ g(s.severe);
EnvInstanceCounts { healthy, total }
}
pub fn stack_family_version(stack: &str) -> Option<(String, String)> {
let version_token = stack.split_whitespace().find(|tok| {
tok.strip_prefix('v')
.map(|rest| {
!rest.is_empty()
&& rest
.split('.')
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
})
.unwrap_or(false)
})?;
let version = version_token.trim_start_matches('v').to_string();
let key = stack
.split_whitespace()
.filter(|tok| *tok != version_token)
.collect::<Vec<_>>()
.join(" ");
Some((key, version))
}
pub fn latest_stack_versions(stacks: &[String]) -> std::collections::HashMap<String, String> {
let mut out: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for s in stacks {
if let Some((key, ver)) = stack_family_version(s) {
match out.get(&key) {
Some(cur)
if crate::util::compare_versions(&ver, cur) != std::cmp::Ordering::Greater => {}
_ => {
out.insert(key, ver);
}
}
}
}
out
}
pub fn newer_stack_version(
env_stack: &str,
latest: &std::collections::HashMap<String, String>,
) -> Option<String> {
let (key, ver) = stack_family_version(env_stack)?;
let newest = latest.get(&key)?;
if crate::util::compare_versions(newest, &ver) == std::cmp::Ordering::Greater {
Some(newest.clone())
} else {
None
}
}
pub async fn list_environments_in_region(
profile: Option<String>,
region: String,
) -> Result<Vec<Environment>> {
let client = super::cached_client(profile, region.clone())
.await
.wrap_err_with(|| format!("region {region}"))?;
let resolved_region = client.context.region.clone();
let mut envs = client
.list_environments()
.await
.wrap_err_with(|| format!("region {region}"))?;
stamp_region(&mut envs, &resolved_region);
Ok(envs)
}
pub(super) fn stamp_region(envs: &mut [Environment], resolved_region: &str) {
for e in envs {
e.region = Some(resolved_region.to_string());
}
}
pub async fn list_environments_for_account(
name: &str,
spec: &crate::config::AccountSpec,
region: Option<String>,
) -> Result<Vec<Environment>> {
let mut spec = spec.clone();
if region.is_some() {
spec.region = region.clone();
}
let client = super::cached_role_client(name, &spec).await?;
let resolved_region = client.context.region.clone();
let mut envs = client.list_environments().await?;
stamp_region(&mut envs, &resolved_region);
Ok(envs)
}
pub(crate) fn platform_family(raw: &str) -> String {
if raw.is_empty() {
return String::new();
}
if raw.contains(" running on ") {
for seg in raw.split('/') {
if let Some((family, _)) = seg.split_once(" running on ") {
return family.trim().to_string();
}
}
}
if let Some((_, after)) = raw.rsplit_once(" running ") {
return after.trim().to_string();
}
raw.to_string()
}
pub(crate) fn normalize_tier(name: &str) -> String {
match name {
"WebServer" => "Web".into(),
"Worker" => "Worker".into(),
other => other.to_string(),
}
}
#[derive(Clone, Debug, Default)]
pub struct WorkerQueues {
pub main_url: Option<String>,
pub dlq_url: Option<String>,
pub main_stats: Option<QueueStats>,
pub dlq_stats: Option<QueueStats>,
}
#[derive(Clone, Debug, Default)]
pub struct EnvVpcContext {
pub vpc_id: Option<String>,
pub subnets: Vec<String>,
pub elb_subnets: Vec<String>,
pub security_groups: Vec<String>,
}
impl AwsClient {
pub async fn list_events(&self, max: i32) -> Result<Vec<Event>> {
Ok(self.list_events_inner(None, None, max, 1).await?.0)
}
pub async fn list_events_for_env(&self, env_name: &str, max: i32) -> Result<Vec<Event>> {
Ok(self
.list_events_inner(Some(env_name.to_string()), None, max, 1)
.await?
.0)
}
pub async fn list_events_since(&self, since_ms: i64, max: i32) -> Result<(Vec<Event>, bool)> {
self.list_events_inner(None, Some(since_ms), max, EVENT_TAIL_MAX_PAGES)
.await
}
async fn list_events_inner(
&self,
env_name: Option<String>,
since_ms: Option<i64>,
max: i32,
max_pages: usize,
) -> Result<(Vec<Event>, bool)> {
let mut raw = Vec::new();
let mut next_token: Option<String> = None;
let mut pages = 0usize;
let mut truncated = false;
loop {
let mut req = self.client.describe_events().max_records(max);
if let Some(n) = env_name.clone() {
req = req.environment_name(n);
}
if let Some(ms) = since_ms {
req = req.start_time(aws_sdk_elasticbeanstalk::primitives::DateTime::from_millis(
ms,
));
}
if let Some(t) = next_token.take() {
req = req.next_token(t);
}
let resp = req.send().await?;
raw.extend(resp.events.unwrap_or_default());
pages += 1;
match resp.next_token {
Some(t) if !t.is_empty() => {
if pages < max_pages {
next_token = Some(t);
continue;
}
truncated = true;
if max_pages > 1 {
tracing::warn!(
target: "ebman::aws",
pages,
collected = raw.len(),
"DescribeEvents page cap reached with more pages available — \
older events in this window were not fetched"
);
}
break;
}
_ => break,
}
}
let events = raw
.into_iter()
.map(|e| Event {
at: e
.event_date
.and_then(|d| DateTime::from_timestamp(d.secs(), d.subsec_nanos())),
env: e.environment_name.unwrap_or_default(),
application: e.application_name.unwrap_or_default(),
message: e.message.unwrap_or_default(),
severity: e
.severity
.map(|s| s.as_str().to_string())
.unwrap_or_else(|| "INFO".to_string()),
version_label: e.version_label.filter(|v| !v.is_empty()),
})
.collect();
Ok((events, truncated))
}
pub async fn describe_env_resources(&self, env_name: &str) -> Result<EnvResources> {
let resp = self
.client
.describe_environment_resources()
.environment_name(env_name)
.send()
.await
.wrap_err("DescribeEnvironmentResources failed")?;
let res = resp
.environment_resources
.ok_or_else(|| eyre!("no environment_resources in response"))?;
Ok(EnvResources {
asgs: res
.auto_scaling_groups
.unwrap_or_default()
.into_iter()
.filter_map(|a| a.name)
.collect(),
instances: res
.instances
.unwrap_or_default()
.into_iter()
.filter_map(|i| i.id)
.collect(),
launch_configs: res
.launch_configurations
.unwrap_or_default()
.into_iter()
.filter_map(|l| l.name)
.collect(),
launch_templates: res
.launch_templates
.unwrap_or_default()
.into_iter()
.filter_map(|l| l.id)
.collect(),
load_balancers: res
.load_balancers
.unwrap_or_default()
.into_iter()
.filter_map(|l| l.name)
.collect(),
triggers: res
.triggers
.unwrap_or_default()
.into_iter()
.filter_map(|t| t.name)
.collect(),
queues: res
.queues
.unwrap_or_default()
.into_iter()
.filter_map(|q| {
let name = q.name?;
Some(EnvResourceQueue {
name,
url: q.url.unwrap_or_default(),
})
})
.collect(),
})
}
pub async fn describe_worker_queues(
&self,
application_name: &str,
env_name: &str,
) -> Result<WorkerQueues> {
let mut main_url: Option<String> = None;
let mut dlq_url: Option<String> = None;
let mut discovery_err: Option<String> = None;
match self
.client
.describe_environment_resources()
.environment_name(env_name)
.send()
.await
{
Ok(resp) => {
if let Some(res) = resp.environment_resources {
for q in res.queues.unwrap_or_default() {
let name = q.name.unwrap_or_default();
let url = q.url.unwrap_or_default();
if url.is_empty() {
continue;
}
match name.as_str() {
"WorkerQueue" => main_url = Some(url),
"WorkerDeadLetterQueue" => dlq_url = Some(url),
_ => {}
}
}
}
}
Err(e) => discovery_err = Some(format!("DescribeEnvironmentResources: {e}")),
}
if main_url.is_none() || dlq_url.is_none() {
match self
.client
.describe_configuration_settings()
.application_name(application_name)
.environment_name(env_name)
.send()
.await
{
Err(e) => {
let msg = format!("DescribeConfigurationSettings: {e}");
discovery_err = Some(match discovery_err.take() {
Some(prior) => format!("{prior} + {msg}"),
None => msg,
});
}
Ok(resp) => {
for setting in resp.configuration_settings.unwrap_or_default() {
for opt in setting.option_settings.unwrap_or_default() {
let ns = opt.namespace.unwrap_or_default();
let name = opt.option_name.unwrap_or_default();
if ns != "aws:elasticbeanstalk:sqsd" {
continue;
}
match name.as_str() {
"WorkerQueueURL" => {
let v = opt.value.unwrap_or_default();
if !v.is_empty() && main_url.is_none() {
main_url = Some(v);
}
}
"DeadLetterQueueURL" => {
let v = opt.value.unwrap_or_default();
if !v.is_empty() && dlq_url.is_none() {
dlq_url = Some(v);
}
}
_ => {}
}
}
}
}
}
}
if main_url.is_none() {
if let Some(err) = discovery_err {
return Err(eyre!(err));
}
}
if let (Some(main), None) = (&main_url, &dlq_url) {
dlq_url = derive_dlq_url(main);
}
let main_stats = match &main_url {
Some(u) => match self.queue_stats(u).await {
Ok(st) => Some(st),
Err(e) => {
let text = format!("{e:#}");
if text.contains("NonExistentQueue") {
None
} else {
return Err(eyre!("main queue stats: {text}"));
}
}
},
None => None,
};
let dlq_stats = match &dlq_url {
Some(u) => match self.queue_stats(u).await {
Ok(st) => Some(st),
Err(e) => {
let text = format!("{e:#}");
if text.contains("NonExistentQueue") {
None
} else {
return Err(eyre!("dlq stats: {text}"));
}
}
},
None => None,
};
Ok(WorkerQueues {
main_url,
dlq_url,
main_stats,
dlq_stats,
})
}
pub async fn fetch_env_option_settings(
&self,
application_name: &str,
env_name: &str,
) -> Result<Vec<(String, String, String)>> {
let resp = self
.client
.describe_configuration_settings()
.application_name(application_name)
.environment_name(env_name)
.send()
.await
.wrap_err("DescribeConfigurationSettings(env) failed")?;
let out = resp
.configuration_settings
.unwrap_or_default()
.into_iter()
.flat_map(|c| c.option_settings.unwrap_or_default())
.map(|o| {
(
o.namespace.unwrap_or_default(),
o.option_name.unwrap_or_default(),
o.value.unwrap_or_default(),
)
})
.collect();
Ok(out)
}
pub async fn fetch_env_vpc_context(
&self,
application_name: &str,
env_name: &str,
) -> Result<EnvVpcContext> {
let resp = self
.client
.describe_configuration_settings()
.application_name(application_name)
.environment_name(env_name)
.send()
.await
.wrap_err("DescribeConfigurationSettings(env) failed")?;
let mut ctx = EnvVpcContext::default();
for setting in resp.configuration_settings.unwrap_or_default() {
for opt in setting.option_settings.unwrap_or_default() {
let ns = opt.namespace.unwrap_or_default();
let name = opt.option_name.unwrap_or_default();
let value = opt.value.unwrap_or_default();
match (ns.as_str(), name.as_str()) {
("aws:ec2:vpc", "VPCId") if !value.is_empty() => {
ctx.vpc_id = Some(value);
}
("aws:ec2:vpc", "Subnets") if !value.is_empty() => {
ctx.subnets = crate::util::split_csv(&value);
}
("aws:ec2:vpc", "ELBSubnets") if !value.is_empty() => {
ctx.elb_subnets = crate::util::split_csv(&value);
}
("aws:autoscaling:launchconfiguration", "SecurityGroups")
if !value.is_empty() =>
{
ctx.security_groups = crate::util::split_csv(&value);
}
_ => {}
}
}
}
Ok(ctx)
}
pub async fn fetch_env_rds_config(
&self,
application_name: &str,
env_name: &str,
) -> Result<Vec<(String, String)>> {
let resp = self
.client
.describe_configuration_settings()
.application_name(application_name)
.environment_name(env_name)
.send()
.await
.wrap_err("DescribeConfigurationSettings(rds) failed")?;
let mut out: Vec<(String, String)> = resp
.configuration_settings
.unwrap_or_default()
.into_iter()
.flat_map(|c| c.option_settings.unwrap_or_default())
.filter_map(|o| {
let ns = o.namespace?;
if ns != "aws:rds:dbinstance" {
return None;
}
let opt = o.option_name?;
let value = o.value.unwrap_or_default();
if value.is_empty() {
return None;
}
Some((opt, value))
})
.collect();
out.sort();
Ok(out)
}
pub async fn fetch_env_configuration_options(
&self,
application_name: &str,
env_name: &str,
) -> Result<Vec<ConfigOption>> {
let vocab_fut = self
.client
.describe_configuration_options()
.environment_name(env_name)
.send();
let settings_fut = self
.client
.describe_configuration_settings()
.application_name(application_name)
.environment_name(env_name)
.send();
let (vocab_resp, settings_resp) = tokio::try_join!(
async {
vocab_fut
.await
.wrap_err("DescribeConfigurationOptions failed")
},
async {
settings_fut
.await
.wrap_err("DescribeConfigurationSettings(options) failed")
},
)?;
let mut current: std::collections::HashMap<(String, String), String> =
std::collections::HashMap::new();
for c in settings_resp.configuration_settings.unwrap_or_default() {
for o in c.option_settings.unwrap_or_default() {
if let (Some(ns), Some(name)) = (o.namespace, o.option_name) {
if let Some(v) = o.value {
if !v.is_empty() {
current.insert((ns, name), v);
}
}
}
}
}
let mut out: Vec<ConfigOption> = vocab_resp
.options
.unwrap_or_default()
.into_iter()
.filter_map(|o| {
let namespace = o.namespace?;
let name = o.name?;
let value = current.get(&(namespace.clone(), name.clone())).cloned();
Some(ConfigOption {
namespace,
name,
value,
default_value: o.default_value,
value_type: o
.value_type
.map(|v| v.as_str().to_string())
.unwrap_or_default(),
value_options: o.value_options.unwrap_or_default(),
change_severity: o.change_severity,
user_defined: o.user_defined,
min_value: o.min_value,
max_value: o.max_value,
max_length: o.max_length,
})
})
.collect();
out.sort_by(|a, b| {
let a_set = a.value.is_some();
let b_set = b.value.is_some();
a.namespace
.cmp(&b.namespace)
.then_with(|| b_set.cmp(&a_set))
.then_with(|| a.name.cmp(&b.name))
});
Ok(out)
}
pub async fn fetch_env_listeners(
&self,
application_name: &str,
env_name: &str,
) -> Result<Vec<(String, String, String)>> {
let resp = self
.client
.describe_configuration_settings()
.application_name(application_name)
.environment_name(env_name)
.send()
.await
.wrap_err("DescribeConfigurationSettings(listeners) failed")?;
let mut out: Vec<(String, String, String)> = resp
.configuration_settings
.unwrap_or_default()
.into_iter()
.flat_map(|c| c.option_settings.unwrap_or_default())
.filter_map(|o| {
let ns = o.namespace?;
let port = ns.strip_prefix("aws:elbv2:listener:")?.to_string();
let opt = o.option_name?;
let value = o.value.unwrap_or_default();
if value.is_empty() {
return None;
}
Some((port, opt, value))
})
.collect();
out.sort_by(|a, b| {
let rank_a = u8::from(a.0 != "default");
let rank_b = u8::from(b.0 != "default");
let port_a = a.0.parse::<u32>().unwrap_or(0);
let port_b = b.0.parse::<u32>().unwrap_or(0);
(rank_a, port_a, &a.1).cmp(&(rank_b, port_b, &b.1))
});
Ok(out)
}
pub async fn fetch_env_vars(
&self,
application_name: &str,
env_name: &str,
) -> Result<Vec<(String, String)>> {
let resp = self
.client
.describe_configuration_settings()
.application_name(application_name)
.environment_name(env_name)
.send()
.await
.wrap_err("DescribeConfigurationSettings(env) failed")?;
let mut out: Vec<(String, String)> = resp
.configuration_settings
.unwrap_or_default()
.into_iter()
.flat_map(|c| c.option_settings.unwrap_or_default())
.filter(|o| {
o.namespace.as_deref() == Some("aws:elasticbeanstalk:application:environment")
})
.map(|o| {
(
o.option_name.unwrap_or_default(),
o.value.unwrap_or_default(),
)
})
.collect();
out.sort();
Ok(out)
}
pub async fn update_env_option_settings(
&self,
env_name: &str,
to_set: &[(String, String, String)],
to_remove: &[(String, String)],
) -> Result<()> {
use aws_sdk_elasticbeanstalk::types::{ConfigurationOptionSetting, OptionSpecification};
if to_set.is_empty() && to_remove.is_empty() {
return Err(eyre!("update_env_option_settings: nothing to do"));
}
let mut req = self.client.update_environment().environment_name(env_name);
for (ns, name, value) in to_set {
req = req.option_settings(
ConfigurationOptionSetting::builder()
.namespace(ns)
.option_name(name)
.value(value)
.build(),
);
}
for (ns, name) in to_remove {
req = req.options_to_remove(
OptionSpecification::builder()
.namespace(ns)
.option_name(name)
.build(),
);
}
req.send()
.await
.wrap_err("UpdateEnvironment(option_settings) failed")?;
Ok(())
}
pub async fn list_tags(&self, resource_arn: &str) -> Result<Vec<(String, String)>> {
let resp = self
.client
.list_tags_for_resource()
.resource_arn(resource_arn)
.send()
.await?;
let tags = resp
.resource_tags
.unwrap_or_default()
.into_iter()
.filter_map(|t| match (t.key, t.value) {
(Some(k), Some(v)) => Some((k, v)),
_ => None,
})
.collect();
Ok(tags)
}
pub async fn update_tags(
&self,
resource_arn: &str,
to_add: &[(String, String)],
to_remove: &[String],
) -> Result<()> {
use aws_sdk_elasticbeanstalk::types::Tag;
let mut req = self
.client
.update_tags_for_resource()
.resource_arn(resource_arn);
for (k, v) in to_add {
req = req.tags_to_add(Tag::builder().key(k).value(v).build());
}
for k in to_remove {
req = req.tags_to_remove(k);
}
req.send().await?;
Ok(())
}
pub async fn rebuild_env(&self, env_name: &str) -> Result<()> {
self.client
.rebuild_environment()
.environment_name(env_name)
.send()
.await?;
Ok(())
}
pub async fn restart_app_server(&self, env_name: &str) -> Result<()> {
self.client
.restart_app_server()
.environment_name(env_name)
.send()
.await?;
Ok(())
}
pub async fn swap_cnames(&self, source: &str, dest: &str) -> Result<()> {
self.client
.swap_environment_cnames()
.source_environment_name(source)
.destination_environment_name(dest)
.send()
.await?;
Ok(())
}
pub async fn create_config_template(
&self,
application_name: &str,
template_name: &str,
source_env_name: &str,
) -> Result<()> {
self.client
.create_configuration_template()
.application_name(application_name)
.template_name(template_name)
.environment_id(source_env_name)
.send()
.await
.wrap_err("CreateConfigurationTemplate failed")?;
Ok(())
}
pub async fn delete_config_template(
&self,
application_name: &str,
template_name: &str,
) -> Result<()> {
self.client
.delete_configuration_template()
.application_name(application_name)
.template_name(template_name)
.send()
.await
.wrap_err("DeleteConfigurationTemplate failed")?;
Ok(())
}
pub async fn list_compatible_platforms(&self, env_name: &str) -> Result<Vec<CustomPlatform>> {
use aws_sdk_elasticbeanstalk::types::{PlatformFilter, PlatformStatus};
let desc = self
.client
.describe_environments()
.environment_names(env_name)
.send()
.await
.wrap_err("DescribeEnvironments failed")?;
let env = desc
.environments
.unwrap_or_default()
.into_iter()
.next()
.ok_or_else(|| eyre!("env '{env_name}' not found"))?;
let current_arn = env.platform_arn.clone().unwrap_or_default();
let stack_or_arn = env
.solution_stack_name
.clone()
.unwrap_or_else(|| current_arn.clone());
let branch = platform_branch_from(&stack_or_arn);
let owner_filter = PlatformFilter::builder()
.r#type("PlatformStatus")
.operator("=")
.values(PlatformStatus::Ready.as_str())
.build();
let mut filters = vec![owner_filter];
if !branch.is_empty() {
filters.push(
PlatformFilter::builder()
.r#type("PlatformBranchName")
.operator("begins_with")
.values(branch.clone())
.build(),
);
}
let (this, fs) = (self, &filters);
let raw = super::paginate("ListPlatformVersions", move |token| async move {
let mut req = this.client.list_platform_versions();
for f in fs {
req = req.filters(f.clone());
}
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req.send().await.wrap_err("ListPlatformVersions failed")?;
Ok((
resp.platform_summary_list.unwrap_or_default(),
resp.next_token,
))
})
.await?
.complete("ListPlatformVersions")?;
let mut out: Vec<CustomPlatform> = raw.into_iter().map(map_platform).collect();
out.sort_by(|a, b| crate::util::compare_versions(&b.version, &a.version));
Ok(out)
}
pub async fn upgrade_platform(&self, env_name: &str, platform_arn: &str) -> Result<()> {
self.client
.update_environment()
.environment_name(env_name)
.platform_arn(platform_arn)
.send()
.await
.wrap_err("UpdateEnvironment(platform_arn) failed")?;
Ok(())
}
pub async fn clone_env(&self, source_env_name: &str, target_env_name: &str) -> Result<()> {
let desc = self
.client
.describe_environments()
.environment_names(source_env_name)
.send()
.await
.wrap_err("DescribeEnvironments failed")?;
let env = desc
.environments
.unwrap_or_default()
.into_iter()
.next()
.ok_or_else(|| eyre!("source env '{source_env_name}' not found"))?;
let application = env
.application_name
.ok_or_else(|| eyre!("source env has no application_name"))?;
let env_id = env
.environment_id
.ok_or_else(|| eyre!("source env has no environment_id"))?;
let template = format!(
"__ebman-clone-{}-{}",
target_env_name,
chrono::Utc::now().timestamp()
);
self.client
.create_configuration_template()
.application_name(&application)
.template_name(&template)
.environment_id(&env_id)
.send()
.await
.wrap_err("CreateConfigurationTemplate failed")?;
let create_result = self
.client
.create_environment()
.application_name(&application)
.environment_name(target_env_name)
.template_name(&template)
.send()
.await;
let _ = self
.client
.delete_configuration_template()
.application_name(&application)
.template_name(&template)
.send()
.await;
create_result.wrap_err("CreateEnvironment failed")?;
Ok(())
}
pub async fn scale_env(&self, env_name: &str, min: i32, max: i32) -> Result<()> {
use aws_sdk_elasticbeanstalk::types::ConfigurationOptionSetting;
let opts = vec![
ConfigurationOptionSetting::builder()
.namespace("aws:autoscaling:asg")
.option_name("MinSize")
.value(min.to_string())
.build(),
ConfigurationOptionSetting::builder()
.namespace("aws:autoscaling:asg")
.option_name("MaxSize")
.value(max.to_string())
.build(),
];
self.client
.update_environment()
.environment_name(env_name)
.set_option_settings(Some(opts))
.send()
.await
.wrap_err("UpdateEnvironment(asg) failed")?;
Ok(())
}
pub async fn abort_environment_update(&self, env_name: &str) -> Result<()> {
self.client
.abort_environment_update()
.environment_name(env_name)
.send()
.await
.wrap_err("AbortEnvironmentUpdate failed")?;
Ok(())
}
pub async fn list_custom_platforms(&self) -> Result<Vec<CustomPlatform>> {
use aws_sdk_elasticbeanstalk::types::PlatformFilter;
let filter = PlatformFilter::builder()
.r#type("PlatformOwner")
.operator("=")
.values("self")
.build();
let (this, f) = (self, &filter);
let raw = super::paginate("ListPlatformVersions", move |token| async move {
let mut req = this.client.list_platform_versions().filters(f.clone());
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req.send().await.wrap_err("ListPlatformVersions failed")?;
Ok((
resp.platform_summary_list.unwrap_or_default(),
resp.next_token,
))
})
.await?
.complete("ListPlatformVersions")?;
let out: Vec<CustomPlatform> = raw.into_iter().map(map_platform).collect();
Ok(out)
}
pub async fn latest_platform_version_date(
&self,
version_arns: &[String],
) -> Result<Option<DateTime<Utc>>> {
let mut latest: Option<DateTime<Utc>> = None;
for arn in version_arns {
let resp = self
.client
.describe_platform_version()
.platform_arn(arn)
.send()
.await
.wrap_err("DescribePlatformVersion failed")?;
let date = resp
.platform_description
.and_then(|d| d.date_created)
.and_then(|t| DateTime::<Utc>::from_timestamp(t.secs(), t.subsec_nanos()));
if let Some(d) = date {
if latest.is_none_or(|l| d > l) {
latest = Some(d);
}
}
}
Ok(latest)
}
pub async fn delete_custom_platform(&self, platform_arn: &str) -> Result<()> {
self.client
.delete_platform_version()
.platform_arn(platform_arn)
.send()
.await
.wrap_err("DeletePlatformVersion failed")?;
Ok(())
}
pub async fn list_application_versions(
&self,
application_name: &str,
) -> Result<Vec<AppVersion>> {
let (this, app) = (self, application_name);
let raw = super::paginate("DescribeApplicationVersions", move |token| async move {
let mut req = this
.client
.describe_application_versions()
.application_name(app);
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req
.send()
.await
.wrap_err("DescribeApplicationVersions failed")?;
Ok((
resp.application_versions.unwrap_or_default(),
resp.next_token,
))
})
.await?
.complete("DescribeApplicationVersions")?;
let mut out: Vec<AppVersion> = Vec::new();
for v in raw {
out.push(AppVersion {
label: v.version_label.unwrap_or_default(),
description: v.description.unwrap_or_default(),
created: v
.date_created
.and_then(|d| DateTime::from_timestamp(d.secs(), d.subsec_nanos())),
});
}
out.sort_by_key(|v| std::cmp::Reverse(v.created));
Ok(out)
}
pub async fn delete_application_version(
&self,
application_name: &str,
version_label: &str,
delete_source_bundle: bool,
) -> Result<()> {
self.client
.delete_application_version()
.application_name(application_name)
.version_label(version_label)
.delete_source_bundle(delete_source_bundle)
.send()
.await
.wrap_err("DeleteApplicationVersion failed")?;
Ok(())
}
pub async fn create_storage_location(&self) -> Result<String> {
let resp = self
.client
.create_storage_location()
.send()
.await
.wrap_err("CreateStorageLocation failed")?;
resp.s3_bucket
.ok_or_else(|| eyre!("CreateStorageLocation returned no S3Bucket"))
}
pub async fn create_app_version(
&self,
application_name: &str,
version_label: &str,
description: Option<&str>,
s3_bucket: &str,
s3_key: &str,
) -> Result<()> {
use aws_sdk_elasticbeanstalk::types::S3Location;
let source = S3Location::builder()
.s3_bucket(s3_bucket)
.s3_key(s3_key)
.build();
let mut req = self
.client
.create_application_version()
.application_name(application_name)
.version_label(version_label)
.source_bundle(source)
.auto_create_application(false);
if let Some(d) = description {
req = req.description(d);
}
req.send()
.await
.wrap_err("CreateApplicationVersion failed")?;
Ok(())
}
pub async fn deploy_version(&self, env_name: &str, version_label: &str) -> Result<()> {
self.client
.update_environment()
.environment_name(env_name)
.version_label(version_label)
.send()
.await
.wrap_err("UpdateEnvironment(version_label) failed")?;
Ok(())
}
pub async fn describe_template_settings(
&self,
application_name: &str,
template_name: &str,
) -> Result<Vec<(String, String, String)>> {
let resp = self
.client
.describe_configuration_settings()
.application_name(application_name)
.template_name(template_name)
.send()
.await
.wrap_err("DescribeConfigurationSettings(template) failed")?;
let mut out: Vec<(String, String, String)> = resp
.configuration_settings
.unwrap_or_default()
.into_iter()
.flat_map(|c| c.option_settings.unwrap_or_default())
.map(|o| {
(
o.namespace.unwrap_or_default(),
o.option_name.unwrap_or_default(),
o.value.unwrap_or_default(),
)
})
.collect();
out.sort();
Ok(out)
}
pub async fn apply_config_template(&self, env_name: &str, template_name: &str) -> Result<()> {
self.client
.update_environment()
.environment_name(env_name)
.template_name(template_name)
.send()
.await
.wrap_err("UpdateEnvironment(template_name) failed")?;
Ok(())
}
pub async fn terminate_env(&self, env_name: &str) -> Result<()> {
self.client
.terminate_environment()
.environment_name(env_name)
.send()
.await?;
Ok(())
}
pub async fn request_env_info_tail(&self, env_name: &str) -> Result<()> {
use aws_sdk_elasticbeanstalk::types::EnvironmentInfoType;
self.client
.request_environment_info()
.environment_name(env_name)
.info_type(EnvironmentInfoType::Tail)
.send()
.await
.wrap_err("RequestEnvironmentInfo failed")?;
Ok(())
}
pub async fn retrieve_env_info_tail(&self, env_name: &str) -> Result<Vec<(String, String)>> {
use aws_sdk_elasticbeanstalk::types::EnvironmentInfoType;
let resp = self
.client
.retrieve_environment_info()
.environment_name(env_name)
.info_type(EnvironmentInfoType::Tail)
.send()
.await
.wrap_err("RetrieveEnvironmentInfo failed")?;
let mut out = Vec::new();
for info in resp.environment_info.unwrap_or_default() {
if let (Some(id), Some(url)) = (info.ec2_instance_id, info.message) {
out.push((id, url));
}
}
Ok(out)
}
pub async fn fetch_env_instance_counts(&self, env_name: &str) -> Result<EnvInstanceCounts> {
let resp = self
.client
.describe_environment_health()
.environment_name(env_name)
.attribute_names(
aws_sdk_elasticbeanstalk::types::EnvironmentHealthAttribute::InstancesHealth,
)
.send()
.await
.wrap_err("DescribeEnvironmentHealth failed")?;
Ok(summarise_instance_health(resp.instances_health.as_ref()))
}
pub async fn list_instances(&self, env_name: &str) -> Result<Vec<Instance>> {
let (this, env) = (self, env_name);
let raw = super::paginate("DescribeInstancesHealth", move |token| async move {
let mut req = this
.client
.describe_instances_health()
.environment_name(env)
.attribute_names(aws_sdk_elasticbeanstalk::types::InstancesHealthAttribute::All);
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req
.send()
.await
.wrap_err("DescribeInstancesHealth failed")?;
Ok((
resp.instance_health_list.unwrap_or_default(),
resp.next_token,
))
})
.await?
.complete("DescribeInstancesHealth")?;
let instances = raw
.into_iter()
.map(|i| Instance {
id: i.instance_id.unwrap_or_default(),
health: i.health_status.unwrap_or_default(),
color: i.color.unwrap_or_default(),
causes: i.causes.unwrap_or_default(),
instance_type: i.instance_type.unwrap_or_default(),
availability_zone: i.availability_zone.unwrap_or_default(),
launched_at: i
.launched_at
.and_then(|d| DateTime::from_timestamp(d.secs(), d.subsec_nanos())),
})
.collect();
Ok(instances)
}
pub async fn list_applications(&self) -> Result<Vec<Application>> {
let resp = self.client.describe_applications().send().await?;
let apps = resp
.applications
.unwrap_or_default()
.into_iter()
.map(|a| Application {
name: a.application_name.unwrap_or_default(),
description: a.description.unwrap_or_default(),
date_created: a
.date_created
.and_then(|d| DateTime::from_timestamp(d.secs(), d.subsec_nanos())),
date_updated: a
.date_updated
.and_then(|d| DateTime::from_timestamp(d.secs(), d.subsec_nanos())),
version_count: a.versions.map(|v| v.len()).unwrap_or(0),
templates: a.configuration_templates.unwrap_or_default(),
latest_version_label: None,
latest_version_created: None,
})
.collect();
Ok(apps)
}
pub async fn list_environments(&self) -> Result<Vec<Environment>> {
let this = self;
let raw = super::paginate("DescribeEnvironments", move |token| async move {
let mut req = this.client.describe_environments().include_deleted(false);
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req.send().await.wrap_err("DescribeEnvironments failed")?;
Ok((resp.environments.unwrap_or_default(), resp.next_token))
})
.await?
.complete("DescribeEnvironments")?;
Ok(raw.into_iter().map(map_env).collect())
}
pub async fn list_solution_stacks(&self) -> Result<Vec<String>> {
let resp = self
.client
.list_available_solution_stacks()
.send()
.await
.wrap_err("ListAvailableSolutionStacks failed")?;
Ok(resp.solution_stacks.unwrap_or_default())
}
}