feat: crud-pricing initial implementation
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:
2025-11-27 04:20:56 -05:00
commit e88609d724
13 changed files with 2494 additions and 0 deletions

199
src/models.rs Normal file
View 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 }),
}
}
}