All checks were successful
kinec.tech/airun-pathfinder-crud-pricing/pipeline/head This commit looks good
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 <noreply@anthropic.com>
186 lines
4.9 KiB
Rust
186 lines
4.9 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
/// Request payload for the Lambda function
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PricingRequest {
|
|
pub operation: PricingOperation,
|
|
}
|
|
|
|
/// Operations supported by crud-pricing
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "camelCase")]
|
|
pub enum PricingOperation {
|
|
/// Get pricing from cache (optionally fetch from AWS if missing)
|
|
Get {
|
|
instance_type: String,
|
|
region: String,
|
|
pricing_type: PricingType,
|
|
#[serde(default)]
|
|
aws_account_id: Option<String>,
|
|
#[serde(default)]
|
|
fetch_if_missing: bool,
|
|
},
|
|
|
|
/// Put pricing data into cache
|
|
Put {
|
|
instance_type: String,
|
|
region: String,
|
|
pricing_type: PricingType,
|
|
pricing_data: PricingData,
|
|
},
|
|
|
|
/// List most commonly accessed instances
|
|
ListCommon {
|
|
#[serde(default = "default_limit")]
|
|
limit: usize,
|
|
#[serde(default)]
|
|
min_access_count: Option<u32>,
|
|
},
|
|
|
|
/// Increment access count for an instance
|
|
IncrementAccess {
|
|
instance_type: String,
|
|
region: String,
|
|
},
|
|
}
|
|
|
|
fn default_limit() -> usize {
|
|
50
|
|
}
|
|
|
|
/// Type of pricing data
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum PricingType {
|
|
/// Public AWS list prices
|
|
Retail,
|
|
/// Account-specific prices (includes EDP/PPA automatically)
|
|
AccountSpecific,
|
|
}
|
|
|
|
/// Complete pricing data for an instance
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PricingData {
|
|
pub instance_type: String,
|
|
pub region: String,
|
|
pub pricing_type: PricingType,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub aws_account_id: Option<String>,
|
|
|
|
/// EC2 instance pricing
|
|
pub ec2_pricing: Ec2Pricing,
|
|
|
|
/// Metadata
|
|
pub source: String, // "aws-pricing-api" or "cost-explorer"
|
|
pub last_updated: String,
|
|
|
|
/// Cache/access tracking
|
|
#[serde(default)]
|
|
pub access_count: u32,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub last_accessed: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub first_cached: Option<String>,
|
|
}
|
|
|
|
/// EC2 instance pricing details
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Ec2Pricing {
|
|
pub instance_family: String, // "m6g", "t3", etc.
|
|
pub vcpus: i32,
|
|
pub memory_gb: f64,
|
|
pub architectures: Vec<String>, // ["x86_64"], ["arm64"], etc.
|
|
|
|
/// OnDemand pricing
|
|
pub on_demand: OnDemandPricing,
|
|
|
|
/// Reserved pricing (if available)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub reserved: Option<ReservedPricing>,
|
|
|
|
/// Spot pricing (if available)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub spot: Option<SpotPricing>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct OnDemandPricing {
|
|
pub hourly: f64,
|
|
pub monthly: f64, // hourly * 730
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReservedPricing {
|
|
pub standard: HashMap<String, ReservedTerm>, // "1yr", "3yr"
|
|
pub convertible: HashMap<String, ReservedTerm>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReservedTerm {
|
|
pub all_upfront: ReservedOption,
|
|
pub partial_upfront: ReservedOption,
|
|
pub no_upfront: ReservedOption,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReservedOption {
|
|
pub effective_hourly: f64,
|
|
pub effective_monthly: f64,
|
|
pub total_upfront: f64,
|
|
pub monthly_payment: f64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SpotPricing {
|
|
pub current_hourly: f64,
|
|
pub avg_hourly: f64, // 30-day average
|
|
pub max_hourly: f64,
|
|
pub interruption_frequency: String, // "<5%", "5-10%", etc.
|
|
pub savings_vs_on_demand_percent: i32,
|
|
}
|
|
|
|
/// Common instance returned by ListCommon operation
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CommonInstance {
|
|
pub instance_type: String,
|
|
pub region: String,
|
|
pub access_count: u32,
|
|
pub last_accessed: Option<String>,
|
|
pub last_updated: String,
|
|
}
|
|
|
|
/// Response envelope
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PricingResponse {
|
|
pub status_code: u16,
|
|
pub body: serde_json::Value,
|
|
}
|
|
|
|
impl PricingResponse {
|
|
pub fn success(data: impl Serialize) -> Self {
|
|
Self {
|
|
status_code: 200,
|
|
body: serde_json::to_value(data).unwrap_or(serde_json::Value::Null),
|
|
}
|
|
}
|
|
|
|
pub fn error(status_code: u16, message: &str) -> Self {
|
|
Self {
|
|
status_code,
|
|
body: serde_json::json!({ "error": message }),
|
|
}
|
|
}
|
|
}
|