Skip to content

Commit

Permalink
fixup! providers: Add "akamai" provider
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick Saika committed Apr 19, 2024
1 parent 21961f5 commit dd04fe0
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 90 deletions.
14 changes: 6 additions & 8 deletions src/providers/akamai/mock_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{HDR_TOKEN, HDR_TOKEN_EXPIRY, TOKEN_TTL};
use crate::providers::{akamai::AkamaiProvider, MetadataProvider};
use crate::providers::akamai::{AkamaiProvider, TOKEN_TTL};
use crate::providers::MetadataProvider;
use mockito::{self};

#[test]
Expand All @@ -10,7 +10,7 @@ fn test_attributes() {
// Mock the PUT /v1/token endpoint.
let put_v1_token = server
.mock("PUT", "/v1/token")
.match_header(HDR_TOKEN_EXPIRY, TOKEN_TTL)
.match_header("metadata-token-expiry-seconds", TOKEN_TTL)
.with_body(token)
.expect_at_least(1)
.create();
Expand Down Expand Up @@ -39,7 +39,7 @@ fn test_attributes() {
let get_v1_instance = server
.mock("GET", "/v1/instance")
.match_header("Accept", "application/json")
.match_header(HDR_TOKEN, token)
.match_header("metadata-token", token)
.with_body(instance_metadata)
.create();

Expand Down Expand Up @@ -73,13 +73,11 @@ fn test_attributes() {
let get_v1_network = server
.mock("GET", "/v1/network")
.match_header("Accept", "application/json")
.match_header(HDR_TOKEN, token)
.match_header("metadata-token", token)
.with_body(network_metadata)
.create();

let mut provider = AkamaiProvider::try_new().unwrap();
provider.client = provider.client.max_retries(0).mock_base_url(server.url());

let provider = AkamaiProvider::with_base_url(server.url()).unwrap();
let attrs = provider.attributes();

// Assert that our endpoints were called.
Expand Down
165 changes: 83 additions & 82 deletions src/providers/akamai/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
#[cfg(test)]
mod mock_tests;

use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use openssh_keys::PublicKey;
use reqwest::header::{HeaderName, HeaderValue};
use serde::Deserialize;
Expand All @@ -29,15 +29,8 @@ use std::collections::HashMap;
use crate::providers::MetadataProvider;
use crate::retry;

const TOKEN_URL: &'static str = "http://169.254.169.254/v1/token";
const INSTANCE_METADATA_URL: &'static str = "http://169.254.169.254/v1/instance";
const NETWORK_METADATA_URL: &'static str = "http://169.254.169.254/v1/network";
const SSHKEYS_URL: &'static str = "http://169.254.169.254/v1/ssh-keys";

static HDR_TOKEN_EXPIRY: &str = "metadata-token-expiry-seconds";
static HDR_TOKEN: &str = "metadata-token";

static TOKEN_TTL: &str = "3600";
/// Default TTL for the metadata token, in seconds.
static TOKEN_TTL: &str = "300";

pub struct AkamaiProvider {
client: retry::Client,
Expand All @@ -46,76 +39,91 @@ pub struct AkamaiProvider {
impl AkamaiProvider {
/// Instantiate a new `AkamaiProvider`.
pub fn try_new() -> Result<Self> {
// Get a metadata token.
let client = retry::Client::try_new()?;
let token = get_token(client)?;

// Create the new client with the token pre-loaded into a header.
// All of the other endpoints accept "text/plain" and "application/json".
// Let's prefer JSON.
let client = retry::Client::try_new()?
.header(
HeaderName::from_static("metadata-token"),
HeaderValue::from_str(&token)?,
)
.header(
HeaderName::from_static("accept"),
HeaderValue::from_static("application/json"),
)
.return_on_404(true);
Ok(Self { client })
}

fn token(&self) -> Result<HeaderValue> {
let token: String = self
.client
.put(retry::Raw, TOKEN_URL.to_string(), None)
/// Instantiate a new `AkamaiProvider` with a specific client.
///
/// NOTE: This method solely exists for testing.
#[cfg(test)]
pub fn with_base_url(url: String) -> Result<Self> {
let client = retry::Client::try_new()?
.mock_base_url(url.clone())
.return_on_404(true)
.max_retries(0);
let token = get_token(client)?;

let client = retry::Client::try_new()?
.header(
HeaderName::from_static(HDR_TOKEN_EXPIRY),
HeaderValue::from_static(TOKEN_TTL),
HeaderName::from_static("metadata-token"),
HeaderValue::from_str(&token)?,
)
.dispatch_put()?
.ok_or_else(|| anyhow!("get metadata token"))?;
.header(
HeaderName::from_static("accept"),
HeaderValue::from_static("application/json"),
)
.mock_base_url(url)
.return_on_404(true)
.max_retries(0);
Ok(Self { client })
}

let token = HeaderValue::from_str(&token).context("create header value from token")?;
Ok(token)
fn endpoint_for(key: &str) -> String {
const URL: &str = "http://169.254.169.254/v1";
format!("{URL}/{key}")
}

/// Fetch the instance metadata.
fn fetch_instance_metadata(&self) -> Result<Instance> {
let token = self.token()?;
let instance: Instance = self
.client
.get(retry::Json, INSTANCE_METADATA_URL.to_string())
.header(HeaderName::from_static(HDR_TOKEN), token)
.header(
HeaderName::from_static("accept"),
HeaderValue::from_static("application/json"),
)
.get(retry::Json, AkamaiProvider::endpoint_for("instance"))
.send()?
.ok_or_else(|| anyhow!("instance metadata not found"))?;
.context("get instance metadata")?;
Ok(instance)
}

/// Fetch the network metadata.
fn fetch_network_metadata(&self) -> Result<Network> {
let network: Network = self
.client
.get(retry::Json, NETWORK_METADATA_URL.to_string())
.header(HeaderName::from_static(HDR_TOKEN), self.token()?)
.header(
HeaderName::from_static("accept"),
HeaderValue::from_static("application/json"),
)
.get(retry::Json, AkamaiProvider::endpoint_for("network"))
.send()?
.ok_or_else(|| anyhow!("network metadata not found"))?;
.context("get network metadata")?;
Ok(network)
}

/// Fetch the SSH keys.
fn fetch_ssh_keys(&self) -> Result<SshKeys> {
/// The returned [HashMap] is a mapping of usernames, to SSH public keys.
fn fetch_ssh_keys(&self) -> Result<HashMap<String, Vec<String>>> {
let ssh_keys: SshKeys = self
.client
.get(retry::Json, SSHKEYS_URL.to_string())
.header(HeaderName::from_static(HDR_TOKEN), self.token()?)
.header(
HeaderName::from_static("accept"),
HeaderValue::from_static("application/json"),
)
.get(retry::Json, AkamaiProvider::endpoint_for("ssh-keys"))
.send()?
.ok_or_else(|| anyhow!("ssh keys not found"))?;
Ok(ssh_keys)
.context("get ssh keys")?;
Ok(ssh_keys.users)
}

/// Convert instance and network metadata into environment variables.
/// All of the instance-related metadata variable names start with `AKAMAI_INSTANCE_`.
/// All of the IPv4 network-related metadata variable names start with `AKAMAI_IPV4_`.
/// All of the IPv6 network-related metadata variable names start with `AKAMAI_IPV6_`.
fn parse_attrs(&self) -> Result<Vec<(String, String)>> {
// Instance metadata.
let data = self.fetch_instance_metadata()?;
let mut attrs = vec![
("AKAMAI_INSTANCE_ID".to_string(), data.id.to_string()),
Expand All @@ -129,73 +137,65 @@ impl AkamaiProvider {
("AKAMAI_INSTANCE_TAGS".to_string(), data.tags.join(":")),
];

// Network metadata.
let data = self.fetch_network_metadata()?;

// Compute the capacity of the Vec for holding all of the network attributes.
// The +2 is for the IPv6 SLAAC and link-local addresses.
let capacity: usize = data.ipv4.public.len()
+ data.ipv4.private.len()
+ data.ipv4.shared.len()
+ 2
+ data.ipv6.ranges.len()
+ data.ipv6.shared_ranges.len();
let mut net_attrs: Vec<(String, String)> = Vec::with_capacity(capacity);

// IPv4
for (i, addr) in data.ipv4.public.iter().enumerate() {
net_attrs.push((format!("AKAMAI_PUBLIC_IPV4_{i}"), format!("{addr}")));
attrs.push((format!("AKAMAI_PUBLIC_IPV4_{i}"), format!("{addr}")));
}

for (i, addr) in data.ipv4.private.iter().enumerate() {
net_attrs.push((format!("AKAMAI_PRIVATE_IPV4_{i}"), format!("{addr}")));
attrs.push((format!("AKAMAI_PRIVATE_IPV4_{i}"), format!("{addr}")));
}

for (i, addr) in data.ipv4.shared.iter().enumerate() {
net_attrs.push((format!("AKAMAI_SHARED_IPV4_{i}"), format!("{addr}")));
attrs.push((format!("AKAMAI_SHARED_IPV4_{i}"), format!("{addr}")));
}

// IPv6
net_attrs.push(("AKAMAI_IPV6_SLAAC".to_string(), data.ipv6.slaac.clone()));
net_attrs.push((
attrs.push(("AKAMAI_IPV6_SLAAC".to_string(), data.ipv6.slaac.clone()));
attrs.push((
"AKAMAI_IPV6_LINK_LOCAL".to_string(),
data.ipv6.link_local.clone(),
));
for (i, v) in data.ipv6.ranges.iter().enumerate() {
net_attrs.push((format!("AKAMAI_IPV6_RANGE_{i}"), format!("{v}")));
attrs.push((format!("AKAMAI_IPV6_RANGE_{i}"), format!("{v}")));
}
for (i, v) in data.ipv6.shared_ranges.iter().enumerate() {
net_attrs.push((format!("AKAMAI_IPV6_SHARED_RANGE_{i}"), format!("{v}")));
attrs.push((format!("AKAMAI_IPV6_SHARED_RANGE_{i}"), format!("{v}")));
}

// Merge the network attributes and the instance attributes.
attrs.extend(net_attrs);

Ok(attrs)
}
}

// Retrieve a token we can use to authenticate future requests to the Linode Metadata Service.
fn get_token(client: retry::Client) -> Result<String> {
let token: String = client
.header(
HeaderName::from_static("metadata-token-expiry-seconds"),
HeaderValue::from_static(TOKEN_TTL),
)
.put(retry::Raw, AkamaiProvider::endpoint_for("token"), None)
.dispatch_put()?
.context("get metadata token")?;
Ok(token)
}

impl MetadataProvider for AkamaiProvider {
fn attributes(&self) -> Result<HashMap<String, String>> {
let attrs = self.parse_attrs()?;
Ok(attrs.into_iter().collect())
}

fn hostname(&self) -> Result<Option<String>> {
let data = self.fetch_instance_metadata()?;
Ok(Some(data.label.clone()))
}

fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
let ssh_keys = self.fetch_ssh_keys()?;
let all_keys: Vec<String> = ssh_keys.users.into_values().flatten().collect();

let mut public_keys: Vec<PublicKey> = Vec::with_capacity(all_keys.len());
for k in all_keys {
let key = PublicKey::parse(&k)?;
public_keys.push(key);
}

Ok(public_keys)
Ok(self
.fetch_ssh_keys()?
.values()
.flatten()
.map(|k| PublicKey::parse(k))
.collect::<Result<_, _>>()?)
}
}

Expand Down Expand Up @@ -276,6 +276,7 @@ struct Ipv6 {
shared_ranges: Vec<String>, // undocumented, might be "elastic-ranges" in the doc
}

/// Used for deserializing a JSON response from the /v1/ssh-keys endpoint.
#[derive(Clone, Deserialize)]
struct SshKeys {
// Mapping of user names, to a list of public keys.
Expand Down

0 comments on commit dd04fe0

Please sign in to comment.