feat: crud-pricing initial implementation
All checks were successful
kinec.tech/airun-pathfinder-crud-pricing/pipeline/head This commit looks good
All checks were successful
kinec.tech/airun-pathfinder-crud-pricing/pipeline/head This commit looks good
Complete CRUD service for AWS pricing operations - single source of truth. Features: - Dual pricing model (retail + account-specific with auto EDP/PPA detection) - Get/Put pricing operations with intelligent caching - AWS Pricing API integration for public list prices - AWS Cost Explorer integration for account-specific pricing - Access counting for self-learning 14-day refresh - Query most-accessed instances (powers smart refresh) - TTL: 30 days (retail), 7 days (account-specific) Architecture: - All other lambdas use this for pricing operations - No direct DynamoDB access from other components - Consistent schema enforcement - Complete IAM setup for Pricing API, Cost Explorer, STS Infrastructure: - Complete Terraform configuration - Full CI/CD pipeline (Jenkinsfile) - Comprehensive documentation - Production-ready scaffolding Part of Phase 1 - foundation for pricing system. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
199
src/models.rs
Normal file
199
src/models.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
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,
|
||||
},
|
||||
|
||||
/// 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 {
|
||||
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 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user