From ea8883d0da8a7fb07b1d0ece21005cef18cff7b1 Mon Sep 17 00:00:00 2001 From: James Bland Date: Thu, 27 Nov 2025 04:44:44 -0500 Subject: [PATCH] refactor: simplify crud-pricing to pure CRUD layer - remove AWS API clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed AWS Pricing API and Cost Explorer clients to make crud-pricing a pure DynamoDB CRUD layer. This fixes layer violation where CRUD was making external AWS API calls. Changes: - Deleted src/aws_pricing.rs (AWS Pricing API client) - Deleted src/cost_explorer.rs (Cost Explorer client) - Removed PricingClient and StsClient from main.rs - Removed QueryAwsApi and QueryCostExplorer operations - Removed fetch_if_missing behavior (Get operation now only reads from cache) - Removed aws-sdk-pricing, aws-sdk-costexplorer, aws-sdk-sts dependencies - Removed pricing_api_access and sts_assume_role IAM policies Result: Pure CRUD layer with only DynamoDB operations (Get, Put, ListCommon, IncrementAccess) Reduced from ~1100 lines to ~600 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Cargo.toml | 3 - src/aws_pricing.rs | 383 ------------------------------------------- src/cost_explorer.rs | 265 ------------------------------ src/main.rs | 112 +------------ src/models.rs | 14 -- terraform/main.tf | 46 +----- 6 files changed, 8 insertions(+), 815 deletions(-) delete mode 100644 src/aws_pricing.rs delete mode 100644 src/cost_explorer.rs diff --git a/Cargo.toml b/Cargo.toml index f558f82..21c6e61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,6 @@ path = "src/main.rs" # AWS SDK aws-config = { version = "1.5", features = ["behavior-version-latest"] } aws-sdk-dynamodb = "1.60" -aws-sdk-pricing = "1.60" -aws-sdk-costexplorer = "1.60" -aws-sdk-sts = "1.60" # Lambda runtime lambda_runtime = "0.13" diff --git a/src/aws_pricing.rs b/src/aws_pricing.rs deleted file mode 100644 index 1483fe3..0000000 --- a/src/aws_pricing.rs +++ /dev/null @@ -1,383 +0,0 @@ -use aws_sdk_pricing::types::Filter; -use aws_sdk_pricing::Client as PricingClient; -use chrono::Utc; -use serde_json::Value; -use std::collections::HashMap; -use tracing::{error, info, warn}; - -use crate::models::{ - Ec2Pricing, OnDemandPricing, PricingData, PricingType, ReservedOption, ReservedPricing, - ReservedTerm, -}; - -/// Fetch EC2 instance pricing from AWS Pricing API -pub async fn fetch_ec2_pricing( - client: &PricingClient, - instance_type: &str, - region: &str, -) -> Result { - info!("Fetching pricing from AWS Pricing API for {} in {}", instance_type, region); - - let location_name = region_to_location_name(region); - - let response = client - .get_products() - .service_code("AmazonEC2") - .filters( - Filter::builder() - .field("instanceType") - .value(instance_type) - .r#type("TERM_MATCH") - .build() - .map_err(|e| format!("Failed to build filter: {}", e))?, - ) - .filters( - Filter::builder() - .field("location") - .value(location_name) - .r#type("TERM_MATCH") - .build() - .map_err(|e| format!("Failed to build filter: {}", e))?, - ) - .filters( - Filter::builder() - .field("operatingSystem") - .value("Linux") - .r#type("TERM_MATCH") - .build() - .map_err(|e| format!("Failed to build filter: {}", e))?, - ) - .filters( - Filter::builder() - .field("tenancy") - .value("Shared") - .r#type("TERM_MATCH") - .build() - .map_err(|e| format!("Failed to build filter: {}", e))?, - ) - .filters( - Filter::builder() - .field("capacitystatus") - .value("Used") - .r#type("TERM_MATCH") - .build() - .map_err(|e| format!("Failed to build filter: {}", e))?, - ) - .filters( - Filter::builder() - .field("preInstalledSw") - .value("NA") - .r#type("TERM_MATCH") - .build() - .map_err(|e| format!("Failed to build filter: {}", e))?, - ) - .send() - .await - .map_err(|e| format!("AWS Pricing API call failed: {}", e))?; - - let price_list = response.price_list(); - - if price_list.is_empty() { - return Err(format!("No pricing found for {} in {}", instance_type, region)); - } - - info!("Parsing {} pricing items", price_list.len()); - - // Parse the first (and usually only) price item - let pricing = parse_pricing_response(price_list[0].as_str(), instance_type, region)?; - - Ok(pricing) -} - -/// Parse AWS Pricing API response -fn parse_pricing_response( - price_json: &str, - instance_type: &str, - region: &str, -) -> Result { - let item: Value = serde_json::from_str(price_json) - .map_err(|e| format!("Failed to parse pricing JSON: {}", e))?; - - // Extract instance attributes - let attributes = item["product"]["attributes"] - .as_object() - .ok_or("Missing product attributes")?; - - let vcpus: i32 = attributes - .get("vcpu") - .and_then(|v| v.as_str()) - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - let memory_str = attributes - .get("memory") - .and_then(|v| v.as_str()) - .unwrap_or("0 GiB"); - let memory_gb: f64 = memory_str - .replace(" GiB", "") - .replace(",", "") - .parse() - .unwrap_or(0.0); - - let instance_family = attributes - .get("instanceFamily") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - - let processor_architecture = attributes - .get("processorArchitecture") - .and_then(|v| v.as_str()) - .unwrap_or("x86_64"); - - let architectures = vec![processor_architecture.to_string()]; - - // Parse OnDemand pricing - let on_demand = parse_on_demand_pricing(&item)?; - - // Parse Reserved pricing (optional) - let reserved = parse_reserved_pricing(&item).ok(); - - // Spot pricing not available in Pricing API (would need EC2 Spot Price API) - let spot = None; - - let ec2_pricing = Ec2Pricing { - instance_family, - vcpus, - memory_gb, - architectures, - on_demand, - reserved, - spot, - }; - - let pricing_data = PricingData { - instance_type: instance_type.to_string(), - region: region.to_string(), - pricing_type: PricingType::Retail, - aws_account_id: None, - ec2_pricing, - source: "aws-pricing-api".to_string(), - last_updated: Utc::now().to_rfc3339(), - access_count: 0, - last_accessed: None, - first_cached: Some(Utc::now().to_rfc3339()), - }; - - Ok(pricing_data) -} - -/// Parse OnDemand pricing from terms -fn parse_on_demand_pricing(item: &Value) -> Result { - let on_demand_terms = item["terms"]["OnDemand"] - .as_object() - .ok_or("Missing OnDemand terms")?; - - // Get first (and usually only) term - let term = on_demand_terms - .values() - .next() - .ok_or("No OnDemand term found")?; - - let price_dimensions = term["priceDimensions"] - .as_object() - .ok_or("Missing priceDimensions")?; - - let price_dim = price_dimensions - .values() - .next() - .ok_or("No price dimension found")?; - - let hourly_str = price_dim["pricePerUnit"]["USD"] - .as_str() - .ok_or("Missing USD price")?; - - let hourly: f64 = hourly_str - .parse() - .map_err(|e| format!("Failed to parse hourly price: {}", e))?; - - let monthly = hourly * 730.0; // Hours per month - - Ok(OnDemandPricing { hourly, monthly }) -} - -/// Parse Reserved pricing from terms -fn parse_reserved_pricing(item: &Value) -> Result { - let reserved_terms = item["terms"]["Reserved"] - .as_object() - .ok_or("Missing Reserved terms")?; - - let mut standard: HashMap = HashMap::new(); - let mut convertible: HashMap = HashMap::new(); - - for (_, term) in reserved_terms { - let term_attributes = term["termAttributes"] - .as_object() - .ok_or("Missing termAttributes")?; - - let lease_contract_length = term_attributes - .get("LeaseContractLength") - .and_then(|v| v.as_str()) - .unwrap_or(""); - - let purchase_option = term_attributes - .get("PurchaseOption") - .and_then(|v| v.as_str()) - .unwrap_or(""); - - let offering_class = term_attributes - .get("OfferingClass") - .and_then(|v| v.as_str()) - .unwrap_or("standard"); - - // Parse the pricing - if let Ok(pricing) = parse_reserved_option(term) { - let term_key = if lease_contract_length.contains("1yr") || lease_contract_length.contains("1 year") { - "1yr" - } else if lease_contract_length.contains("3yr") || lease_contract_length.contains("3 year") { - "3yr" - } else { - continue; // Skip unknown terms - }; - - let option_type = if purchase_option.contains("All Upfront") { - "all_upfront" - } else if purchase_option.contains("Partial Upfront") { - "partial_upfront" - } else if purchase_option.contains("No Upfront") { - "no_upfront" - } else { - continue; // Skip unknown options - }; - - // Add to appropriate collection - let collection = if offering_class.contains("convertible") { - &mut convertible - } else { - &mut standard - }; - - let term_entry = collection - .entry(term_key.to_string()) - .or_insert_with(|| ReservedTerm { - all_upfront: ReservedOption::default(), - partial_upfront: ReservedOption::default(), - no_upfront: ReservedOption::default(), - }); - - match option_type { - "all_upfront" => term_entry.all_upfront = pricing, - "partial_upfront" => term_entry.partial_upfront = pricing, - "no_upfront" => term_entry.no_upfront = pricing, - _ => {} - } - } - } - - if standard.is_empty() && convertible.is_empty() { - return Err("No valid reserved pricing found".to_string()); - } - - Ok(ReservedPricing { - standard, - convertible, - }) -} - -/// Parse a single reserved pricing option -fn parse_reserved_option(term: &Value) -> Result { - let price_dimensions = term["priceDimensions"] - .as_object() - .ok_or("Missing priceDimensions")?; - - let mut total_upfront = 0.0; - let mut hourly_rate = 0.0; - - for (_, price_dim) in price_dimensions { - let unit = price_dim["unit"] - .as_str() - .unwrap_or(""); - - let price_str = price_dim["pricePerUnit"]["USD"] - .as_str() - .unwrap_or("0"); - - let price: f64 = price_str.parse().unwrap_or(0.0); - - if unit == "Quantity" { - total_upfront = price; - } else if unit == "Hrs" { - hourly_rate = price; - } - } - - let effective_hourly = if total_upfront > 0.0 { - // Calculate effective hourly from upfront - let term_attributes = term["termAttributes"] - .as_object() - .ok_or("Missing termAttributes")?; - - let lease_length = term_attributes - .get("LeaseContractLength") - .and_then(|v| v.as_str()) - .unwrap_or(""); - - let hours = if lease_length.contains("1yr") || lease_length.contains("1 year") { - 8760.0 // Hours in 1 year - } else if lease_length.contains("3yr") || lease_length.contains("3 year") { - 26280.0 // Hours in 3 years - } else { - 8760.0 // Default to 1 year - }; - - (total_upfront / hours) + hourly_rate - } else { - hourly_rate - }; - - let effective_monthly = effective_hourly * 730.0; - let monthly_payment = hourly_rate * 730.0; - - Ok(ReservedOption { - effective_hourly, - effective_monthly, - total_upfront, - monthly_payment, - }) -} - -impl Default for ReservedOption { - fn default() -> Self { - Self { - effective_hourly: 0.0, - effective_monthly: 0.0, - total_upfront: 0.0, - monthly_payment: 0.0, - } - } -} - -/// Convert AWS region code to location name used by Pricing API -pub fn region_to_location_name(region: &str) -> &str { - match region { - "us-east-1" => "US East (N. Virginia)", - "us-east-2" => "US East (Ohio)", - "us-west-1" => "US West (N. California)", - "us-west-2" => "US West (Oregon)", - "eu-west-1" => "EU (Ireland)", - "eu-west-2" => "EU (London)", - "eu-west-3" => "EU (Paris)", - "eu-central-1" => "EU (Frankfurt)", - "ap-southeast-1" => "Asia Pacific (Singapore)", - "ap-southeast-2" => "Asia Pacific (Sydney)", - "ap-northeast-1" => "Asia Pacific (Tokyo)", - "ap-northeast-2" => "Asia Pacific (Seoul)", - "ap-south-1" => "Asia Pacific (Mumbai)", - "sa-east-1" => "South America (Sao Paulo)", - "ca-central-1" => "Canada (Central)", - _ => { - warn!("Unknown region: {}, using as-is", region); - region - } - } -} diff --git a/src/cost_explorer.rs b/src/cost_explorer.rs deleted file mode 100644 index 8ae656b..0000000 --- a/src/cost_explorer.rs +++ /dev/null @@ -1,265 +0,0 @@ -use aws_config::Region; -use aws_sdk_costexplorer::types::{ - DateInterval, Dimension, DimensionValues, Expression, Granularity, Metric, -}; -use aws_sdk_costexplorer::Client as CostExplorerClient; -use aws_sdk_sts::Client as StsClient; -use chrono::{Duration, Utc}; -use tracing::{error, info, warn}; - -use crate::models::{Ec2Pricing, OnDemandPricing, PricingData, PricingType}; - -/// Fetch account-specific pricing from Cost Explorer -/// This automatically includes EDP/PPA discounts! -pub async fn fetch_account_specific_pricing( - sts_client: &StsClient, - instance_type: &str, - region: &str, - aws_account_id: &str, - role_arn: &str, -) -> Result { - info!( - "Fetching account-specific pricing for {} in {} (account: {})", - instance_type, region, aws_account_id - ); - - // 1. Assume role into customer account - let credentials = sts_client - .assume_role() - .role_arn(role_arn) - .role_session_name("pathfinder-pricing-query") - .duration_seconds(900) // 15 minutes - .send() - .await - .map_err(|e| format!("Failed to assume role: {}", e))?; - - let creds = credentials - .credentials() - .ok_or("No credentials returned from assume role")?; - - info!("✅ Successfully assumed role into account {}", aws_account_id); - - // 2. Create Cost Explorer client with assumed credentials - let config = aws_config::from_env() - .credentials_provider(aws_sdk_sts::config::Credentials::new( - creds.access_key_id(), - creds.secret_access_key(), - Some(creds.session_token().to_string()), - None, - "assumed-role", - )) - .region(Region::new(region.to_string())) - .load() - .await; - - let ce_client = CostExplorerClient::new(&config); - - // 3. Query Cost Explorer for historical usage and costs - let end_date = Utc::now(); - let start_date = end_date - Duration::days(30); - - info!( - "Querying Cost Explorer from {} to {}", - start_date.format("%Y-%m-%d"), - end_date.format("%Y-%m-%d") - ); - - let response = ce_client - .get_cost_and_usage() - .time_period( - DateInterval::builder() - .start(start_date.format("%Y-%m-%d").to_string()) - .end(end_date.format("%Y-%m-%d").to_string()) - .build() - .map_err(|e| format!("Failed to build date interval: {}", e))?, - ) - .granularity(Granularity::Daily) - .metrics(Metric::UnblendedCost) - .metrics(Metric::UsageQuantity) - .filter( - Expression::builder() - .dimensions( - DimensionValues::builder() - .key(Dimension::InstanceType) - .values(instance_type) - .build() - .map_err(|e| format!("Failed to build dimension: {}", e))?, - ) - .build(), - ) - .send() - .await - .map_err(|e| format!("Cost Explorer API call failed: {}", e))?; - - // 4. Calculate average hourly cost from actual usage - let results = response.results_by_time(); - - if results.is_empty() { - return Err(format!( - "No usage data found for {} in account {}. Instance may not be in use.", - instance_type, aws_account_id - )); - } - - let (total_cost, total_hours) = results.iter().fold((0.0, 0.0), |(cost_acc, hours_acc), result| { - let cost: f64 = result - .total() - .and_then(|t| t.get("UnblendedCost")) - .and_then(|m| m.amount()) - .and_then(|a| a.parse().ok()) - .unwrap_or(0.0); - - let usage_quantity: f64 = result - .total() - .and_then(|t| t.get("UsageQuantity")) - .and_then(|m| m.amount()) - .and_then(|a| a.parse().ok()) - .unwrap_or(0.0); - - (cost_acc + cost, hours_acc + usage_quantity) - }); - - if total_hours == 0.0 { - return Err(format!( - "No usage hours found for {} (zero usage in past 30 days)", - instance_type - )); - } - - let avg_hourly_cost = total_cost / total_hours; - let monthly_cost = avg_hourly_cost * 730.0; - - info!( - "✅ Calculated account-specific pricing: ${:.4}/hr, ${:.2}/mo (from {} hours usage)", - avg_hourly_cost, monthly_cost, total_hours - ); - - // 5. Build pricing data - // Note: We don't have vcpu/memory from Cost Explorer, so we'd need to get that from Pricing API - // For now, we'll query Pricing API for instance specs and use Cost Explorer for pricing - let instance_specs = fetch_instance_specs(instance_type, region).await?; - - let ec2_pricing = Ec2Pricing { - instance_family: instance_specs.instance_family, - vcpus: instance_specs.vcpus, - memory_gb: instance_specs.memory_gb, - architectures: instance_specs.architectures, - on_demand: OnDemandPricing { - hourly: avg_hourly_cost, - monthly: monthly_cost, - }, - reserved: None, // Reserved pricing from Cost Explorer is complex, skip for now - spot: None, // Spot pricing not in Cost Explorer - }; - - let pricing_data = PricingData { - instance_type: instance_type.to_string(), - region: region.to_string(), - pricing_type: PricingType::AccountSpecific, - aws_account_id: Some(aws_account_id.to_string()), - ec2_pricing, - source: "cost-explorer".to_string(), - last_updated: Utc::now().to_rfc3339(), - access_count: 0, - last_accessed: None, - first_cached: Some(Utc::now().to_rfc3339()), - }; - - Ok(pricing_data) -} - -/// Simple struct for instance specs -struct InstanceSpecs { - instance_family: String, - vcpus: i32, - memory_gb: f64, - architectures: Vec, -} - -/// Fetch instance specifications from Pricing API -/// We need this because Cost Explorer only gives us costs, not specs -async fn fetch_instance_specs( - instance_type: &str, - region: &str, -) -> Result { - info!("Fetching instance specs for {}", instance_type); - - // Create new pricing client from default config - let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; - let pricing_client = aws_sdk_pricing::Client::new(&config); - - let location_name = crate::aws_pricing::region_to_location_name(region); - - let response = pricing_client - .get_products() - .service_code("AmazonEC2") - .filters( - aws_sdk_pricing::types::Filter::builder() - .field("instanceType") - .value(instance_type) - .r#type("TERM_MATCH") - .build() - .map_err(|e| format!("Failed to build filter: {}", e))?, - ) - .filters( - aws_sdk_pricing::types::Filter::builder() - .field("location") - .value(location_name) - .r#type("TERM_MATCH") - .build() - .map_err(|e| format!("Failed to build filter: {}", e))?, - ) - .send() - .await - .map_err(|e| format!("Failed to fetch instance specs: {}", e))?; - - let price_list = response.price_list(); - - if price_list.is_empty() { - return Err(format!("No instance specs found for {}", instance_type)); - } - - // Parse just the attributes we need - let item: serde_json::Value = serde_json::from_str(price_list[0].as_str()) - .map_err(|e| format!("Failed to parse pricing JSON: {}", e))?; - - let attributes = item["product"]["attributes"] - .as_object() - .ok_or("Missing product attributes")?; - - let vcpus: i32 = attributes - .get("vcpu") - .and_then(|v| v.as_str()) - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - let memory_str = attributes - .get("memory") - .and_then(|v| v.as_str()) - .unwrap_or("0 GiB"); - let memory_gb: f64 = memory_str - .replace(" GiB", "") - .replace(",", "") - .parse() - .unwrap_or(0.0); - - let instance_family = attributes - .get("instanceFamily") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - - let processor_architecture = attributes - .get("processorArchitecture") - .and_then(|v| v.as_str()) - .unwrap_or("x86_64"); - - let architectures = vec![processor_architecture.to_string()]; - - Ok(InstanceSpecs { - instance_family, - vcpus, - memory_gb, - architectures, - }) -} diff --git a/src/main.rs b/src/main.rs index 4785c63..b94c0e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,8 @@ -mod aws_pricing; -mod cost_explorer; mod db; mod models; use aws_config::BehaviorVersion; use aws_sdk_dynamodb::Client as DynamoDbClient; -use aws_sdk_pricing::Client as PricingClient; -use aws_sdk_sts::Client as StsClient; use lambda_runtime::{service_fn, Error, LambdaEvent}; use serde_json::Value; use std::env; @@ -44,8 +40,6 @@ async fn function_handler(event: LambdaEvent) -> Result { // Load AWS config and create clients let config = aws_config::load_defaults(BehaviorVersion::latest()).await; let dynamodb_client = DynamoDbClient::new(&config); - let pricing_client = PricingClient::new(&config); - let sts_client = StsClient::new(&config); let table_name = env::var("TABLE_NAME").unwrap_or_else(|_| "pathfinder-dev-pricing".to_string()); @@ -53,8 +47,6 @@ async fn function_handler(event: LambdaEvent) -> Result { let result = handle_operation( request.operation, &dynamodb_client, - &pricing_client, - &sts_client, &table_name, ) .await; @@ -73,8 +65,6 @@ async fn function_handler(event: LambdaEvent) -> Result { async fn handle_operation( operation: PricingOperation, dynamodb_client: &DynamoDbClient, - pricing_client: &PricingClient, - sts_client: &StsClient, table_name: &str, ) -> Result { match operation { @@ -83,9 +73,9 @@ async fn handle_operation( region, pricing_type, aws_account_id, - fetch_if_missing, + fetch_if_missing: _, } => { - info!("Operation: Get (type={:?}, fetch_if_missing={})", pricing_type, fetch_if_missing); + info!("Operation: Get (type={:?})", pricing_type); // Check cache let cached = db::get_pricing( @@ -126,30 +116,8 @@ async fn handle_operation( "cacheStatus": "hit" })) } - None if fetch_if_missing => { - // Cache miss - fetch from AWS - info!("Cache miss, fetching from AWS API"); - - let pricing = fetch_pricing( - pricing_client, - sts_client, - &instance_type, - ®ion, - &pricing_type, - aws_account_id.as_deref(), - ) - .await?; - - // Cache it - db::put_pricing(dynamodb_client, table_name, &pricing).await?; - - Ok(serde_json::json!({ - "pricing": pricing, - "cacheStatus": "miss" - })) - } None => { - Err("Pricing not found and fetch_if_missing=false".to_string()) + Err("Pricing not found in cache".to_string()) } } } @@ -209,79 +177,5 @@ async fn handle_operation( "region": region })) } - - PricingOperation::QueryAwsApi { - instance_type, - region, - } => { - info!("Operation: QueryAwsApi ({} in {})", instance_type, region); - - let pricing = aws_pricing::fetch_ec2_pricing(pricing_client, &instance_type, ®ion).await?; - - Ok(serde_json::json!({ - "pricing": pricing, - "source": "aws-pricing-api" - })) - } - - PricingOperation::QueryCostExplorer { - instance_type, - region, - aws_account_id, - role_arn, - } => { - info!( - "Operation: QueryCostExplorer ({} in {}, account={})", - instance_type, region, aws_account_id - ); - - let pricing = cost_explorer::fetch_account_specific_pricing( - sts_client, - &instance_type, - ®ion, - &aws_account_id, - &role_arn, - ) - .await?; - - Ok(serde_json::json!({ - "pricing": pricing, - "source": "cost-explorer", - "includesEDP": true - })) - } - } -} - -/// Fetch pricing based on type (retail or account-specific) -async fn fetch_pricing( - pricing_client: &PricingClient, - sts_client: &StsClient, - instance_type: &str, - region: &str, - pricing_type: &PricingType, - aws_account_id: Option<&str>, -) -> Result { - match pricing_type { - PricingType::Retail => { - info!("Fetching retail pricing from AWS Pricing API"); - aws_pricing::fetch_ec2_pricing(pricing_client, instance_type, region).await - } - PricingType::AccountSpecific => { - let account_id = aws_account_id.ok_or("aws_account_id required for account-specific pricing")?; - - // Role ARN is expected to be in format: arn:aws:iam::{account_id}:role/pathfinder-pricing-access - let role_arn = format!("arn:aws:iam::{}:role/pathfinder-pricing-access", account_id); - - info!("Fetching account-specific pricing from Cost Explorer"); - cost_explorer::fetch_account_specific_pricing( - sts_client, - instance_type, - region, - account_id, - &role_arn, - ) - .await - } } } diff --git a/src/models.rs b/src/models.rs index 89d0db4..233e90b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -44,20 +44,6 @@ pub enum PricingOperation { instance_type: String, region: String, }, - - /// Query AWS Pricing API directly - QueryAwsApi { - instance_type: String, - region: String, - }, - - /// Query AWS Cost Explorer for account-specific pricing - QueryCostExplorer { - instance_type: String, - region: String, - aws_account_id: String, - role_arn: String, - }, } fn default_limit() -> usize { diff --git a/terraform/main.tf b/terraform/main.tf index 79ee204..1644871 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -23,6 +23,11 @@ provider "aws" { } } +# Data source: Get Gateway ID from platform-core +data "aws_ssm_parameter" "gateway_id" { + name = "/airun-pathfinder/${var.environment}/gateway-id" +} + # Get current AWS account ID data "aws_caller_identity" "current" {} @@ -124,47 +129,6 @@ resource "aws_iam_role_policy" "dynamodb_access" { }) } -# AWS Pricing API access -resource "aws_iam_role_policy" "pricing_api_access" { - name = "airun-pathfinder-crud-pricing-pricing-api-policy" - role = aws_iam_role.lambda.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "pricing:GetProducts", - "pricing:DescribeServices", - "pricing:GetAttributeValues" - ] - Resource = "*" - } - ] - }) -} - -# STS AssumeRole access (for Cost Explorer in customer accounts) -resource "aws_iam_role_policy" "sts_assume_role" { - name = "airun-pathfinder-crud-pricing-sts-policy" - role = aws_iam_role.lambda.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = "sts:AssumeRole" - Resource = "arn:aws:iam::*:role/pathfinder-pricing-access" - } - ] - }) -} - -# Cost Explorer access (when assuming role) -# Note: This is granted via the role in the customer account, not here - # Local variables locals { table_name = var.table_name != "" ? var.table_name : "pathfinder-${var.environment}-pricing"