124 lines
4.4 KiB
Go
124 lines
4.4 KiB
Go
package cloud
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// FetchDailyBill 通过 AWS Cost Explorer 按服务和区域同步指定日的未摊销成本。 // Logan.Li
|
|
func (p *AWSProvider) FetchDailyBill(billingDate string) ([]CloudBillingRecord, error) {
|
|
day, err := time.Parse("2006-01-02", billingDate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("账单日期格式错误: %w", err)
|
|
}
|
|
payload, err := json.Marshal(map[string]interface{}{
|
|
"TimePeriod": map[string]string{
|
|
"Start": day.Format("2006-01-02"),
|
|
"End": day.AddDate(0, 0, 1).Format("2006-01-02"),
|
|
},
|
|
"Granularity": "DAILY",
|
|
"Metrics": []string{"UnblendedCost"},
|
|
"GroupBy": []map[string]string{
|
|
{"Type": "DIMENSION", "Key": "SERVICE"},
|
|
{"Type": "DIMENSION", "Key": "REGION"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("构造 AWS Cost Explorer 请求失败: %w", err)
|
|
}
|
|
body, err := p.callCostExplorer(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseAWSCostExplorerBill(billingDate, body)
|
|
}
|
|
|
|
// callCostExplorer 使用 SigV4 调用 Cost Explorer;该服务固定使用 us-east-1。 // Logan.Li
|
|
func (p *AWSProvider) callCostExplorer(payload []byte) ([]byte, error) {
|
|
const host = "ce.us-east-1.amazonaws.com"
|
|
now := time.Now().UTC()
|
|
date := now.Format("20060102")
|
|
amzDate := now.Format("20060102T150405Z")
|
|
payloadHash := sha256Hex(string(payload))
|
|
canonicalHeaders := "content-type:application/x-amz-json-1.1\n" +
|
|
"host:" + host + "\n" +
|
|
"x-amz-date:" + amzDate + "\n" +
|
|
"x-amz-target:AWSInsightsIndexService.GetCostAndUsage\n"
|
|
signedHeaders := "content-type;host;x-amz-date;x-amz-target"
|
|
canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + payloadHash
|
|
credentialScope := date + "/us-east-1/ce/aws4_request"
|
|
stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + credentialScope + "\n" + sha256Hex(canonicalRequest)
|
|
kDate := hmacSHA256([]byte("AWS4"+p.secretKey), date)
|
|
kRegion := hmacSHA256(kDate, "us-east-1")
|
|
kService := hmacSHA256(kRegion, "ce")
|
|
signingKey := hmacSHA256(kService, "aws4_request")
|
|
signature := hex.EncodeToString(hmacSHA256(signingKey, stringToSign))
|
|
|
|
req, err := http.NewRequest(http.MethodPost, "https://"+host+"/", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("创建 AWS Cost Explorer 请求失败: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
|
|
req.Header.Set("X-Amz-Date", amzDate)
|
|
req.Header.Set("X-Amz-Target", "AWSInsightsIndexService.GetCostAndUsage")
|
|
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential="+p.accessKey+"/"+credentialScope+", SignedHeaders="+signedHeaders+", Signature="+signature)
|
|
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("请求 AWS Cost Explorer 失败: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取 AWS Cost Explorer 响应失败: %w", err)
|
|
}
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return nil, fmt.Errorf("AWS Cost Explorer 返回 HTTP %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func parseAWSCostExplorerBill(billingDate string, body []byte) ([]CloudBillingRecord, error) {
|
|
var response struct {
|
|
ResultsByTime []struct {
|
|
Groups []struct {
|
|
Keys []string `json:"Keys"`
|
|
Metrics map[string]struct {
|
|
Amount string `json:"Amount"`
|
|
Unit string `json:"Unit"`
|
|
} `json:"Metrics"`
|
|
} `json:"Groups"`
|
|
} `json:"ResultsByTime"`
|
|
}
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|
return nil, fmt.Errorf("解析 AWS Cost Explorer 响应失败: %w", err)
|
|
}
|
|
records := make([]CloudBillingRecord, 0)
|
|
for _, result := range response.ResultsByTime {
|
|
for _, group := range result.Groups {
|
|
metric, ok := group.Metrics["UnblendedCost"]
|
|
if !ok {
|
|
continue
|
|
}
|
|
amount, err := strconv.ParseFloat(metric.Amount, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("解析 AWS 成本金额失败: %w", err)
|
|
}
|
|
service, region := "AWS", "global"
|
|
if len(group.Keys) > 0 && group.Keys[0] != "" {
|
|
service = group.Keys[0]
|
|
}
|
|
if len(group.Keys) > 1 && group.Keys[1] != "" {
|
|
region = group.Keys[1]
|
|
}
|
|
records = append(records, CloudBillingRecord{BillingDate: billingDate, Provider: "aws", Cost: amount, Currency: metric.Unit, Service: service, Region: region, Description: "AWS Cost Explorer UnblendedCost"})
|
|
}
|
|
}
|
|
return records, nil
|
|
}
|