//! The aws module implements the `PlatformDataProvider` trait for gathering userdata on AWS.
use super::{PlatformDataProvider, SettingsJson};
use crate::compression::expand_slice_maybe;
use async_trait::async_trait;
use imdsclient::ImdsClient;
use serde_json::json;
use snafu::{OptionExt, ResultExt};
use std::fs;
use std::path::Path;
use crate::provider::local_file::{local_file_user_data, USER_DATA_FILE};
/// Unit struct for AWS so we can implement the PlatformDataProvider trait.
pub(crate) struct AwsDataProvider;
impl AwsDataProvider {
const IDENTITY_DOCUMENT_FILE: &'static str = "/etc/early-boot-config/identity-document";
const FALLBACK_REGION: &'static str = "us-east-1";
/// Fetches user data, which is expected to be in TOML form and contain a `[settings]` section,
/// returning a SettingsJson representing the inside of that section.
async fn user_data(client: &mut ImdsClient) -> Result> {
let user_data_raw = match client
.fetch_userdata()
.await
.context(error::ImdsRequestSnafu)?
{
Some(user_data_raw) => user_data_raw,
None => return Ok(None),
};
let user_data_str = expand_slice_maybe(&user_data_raw)
.context(error::DecompressionSnafu { what: "user data" })?;
trace!("Received user data: {}", user_data_str);
let json = SettingsJson::from_toml_str(&user_data_str, "user data").context(
error::SettingsToJSONSnafu {
from: "instance user data",
},
)?;
Ok(Some(json))
}
/// Fetches the instance identity, returning a SettingsJson representing the values from the
/// document which we'd like to send to the API - currently just region.
async fn identity_document(client: &mut ImdsClient) -> Result > {
let desc = "instance identity document";
let file = Self::IDENTITY_DOCUMENT_FILE;
let region = if Path::new(file).exists() {
info!("{} found at {}, using it", desc, file);
let data =
fs::read_to_string(file).context(error::InputFileReadSnafu { path: file })?;
let iid: serde_json::Value =
serde_json::from_str(&data).context(error::DeserializeJsonSnafu)?;
iid.get("region")
.context(error::IdentityDocMissingDataSnafu { missing: "region" })?
.as_str()
.context(error::WrongTypeSnafu {
field_name: "region",
expected_type: "string",
})?
.to_owned()
} else {
client
.fetch_region()
.await
.context(error::ImdsRequestSnafu)?
.unwrap_or_else(|| Self::FALLBACK_REGION.to_owned())
};
trace!(
"Retrieved region from instance identity document: {}",
region
);
let val = json!({ "aws": {"region": region} });
let json = SettingsJson::from_val(&val, desc).context(error::SettingsToJSONSnafu {
from: "instance identity document",
})?;
Ok(Some(json))
}
}
#[async_trait]
impl PlatformDataProvider for AwsDataProvider {
/// Return settings changes from the instance identity document and user data.
async fn platform_data(
&self,
) -> std::result::Result, Box> {
let mut output = Vec::new();
let mut client = ImdsClient::new();
// Attempt to read from local file first
match local_file_user_data()? {
Some(s) => output.push(s),
None => warn!("No user data found via local file: {}", USER_DATA_FILE),
}
// Instance identity doc next, so the user has a chance to override
match Self::identity_document(&mut client).await? {
Some(s) => output.push(s),
None => warn!("No instance identity document found."),
}
// Optional user-specified configuration / overrides
match Self::user_data(&mut client).await? {
Some(s) => output.push(s),
None => warn!("No user data found."),
}
Ok(output)
}
}
mod error {
use snafu::Snafu;
use std::io;
use std::path::PathBuf;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(super)))]
pub(crate) enum Error {
#[snafu(display("Failed to decompress {}: {}", what, source))]
Decompression { what: String, source: io::Error },
#[snafu(display("Error deserializing from JSON: {}", source))]
DeserializeJson { source: serde_json::error::Error },
#[snafu(display("Instance identity document missing {}", missing))]
IdentityDocMissingData { missing: String },
#[snafu(display("IMDS client failed: {}", source))]
ImdsClient { source: imdsclient::Error },
#[snafu(display("Unable to read input file '{}': {}", path.display(), source))]
InputFileRead { path: PathBuf, source: io::Error },
#[snafu(display("IMDS request failed: {}", source))]
ImdsRequest { source: imdsclient::Error },
#[snafu(display("Unable to serialize settings from {}: {}", from, source))]
SettingsToJSON {
from: String,
source: crate::settings::Error,
},
#[snafu(display(
"Wrong type while deserializing, expected '{}' to be type '{}'",
field_name,
expected_type
))]
WrongType {
field_name: &'static str,
expected_type: &'static str,
},
}
}
type Result = std::result::Result;