| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- package models
- import (
- "fmt"
- "io"
- "math"
- "reflect"
- "strconv"
- "strings"
- "github.com/microcosm-cc/bluemonday"
- "github.com/opencost/opencost/core/pkg/clustercache"
- "github.com/opencost/opencost/core/pkg/log"
- "github.com/opencost/opencost/pkg/config"
- )
- var (
- sanitizePolicy = bluemonday.UGCPolicy()
- )
- const (
- AuthSecretPath = "/var/secrets/service-key.json"
- StorageConfigSecretPath = "/var/azure-storage-config/azure-storage-config.json"
- DefaultShareTenancyCost = "true"
- KarpenterCapacityTypeLabel = "karpenter.sh/capacity-type"
- KarpenterCapacitySpotTypeValue = "spot"
- )
- // ReservedInstanceData keeps record of resources on a node should be
- // priced at reserved rates
- type ReservedInstanceData struct {
- ReservedCPU int64 `json:"reservedCPU"`
- ReservedRAM int64 `json:"reservedRAM"`
- CPUCost float64 `json:"CPUHourlyCost"`
- RAMCost float64 `json:"RAMHourlyCost"`
- }
- // Node is the interface by which the provider and cost model communicate Node prices.
- // The provider will best-effort try to fill out this struct.
- type Node struct {
- Cost string `json:"hourlyCost"`
- VCPU string `json:"CPU"`
- VCPUCost string `json:"CPUHourlyCost"`
- RAM string `json:"RAM"`
- RAMBytes string `json:"RAMBytes"`
- RAMCost string `json:"RAMGBHourlyCost"`
- Storage string `json:"storage"`
- StorageCost string `json:"storageHourlyCost"`
- UsesBaseCPUPrice bool `json:"usesDefaultPrice"`
- BaseCPUPrice string `json:"baseCPUPrice"` // Used to compute an implicit RAM GB/Hr price when RAM pricing is not provided.
- BaseRAMPrice string `json:"baseRAMPrice"` // Used to compute an implicit RAM GB/Hr price when RAM pricing is not provided.
- BaseGPUPrice string `json:"baseGPUPrice"`
- UsageType string `json:"usageType"`
- GPU string `json:"gpu"` // GPU represents the number of GPU on the instance
- GPUName string `json:"gpuName"`
- GPUCost string `json:"gpuCost"`
- VGPU string `json:"vgpu"` // virtualized gpu-- if we are using gpu replicas
- InstanceType string `json:"instanceType,omitempty"`
- Region string `json:"region,omitempty"`
- Reserved *ReservedInstanceData `json:"reserved,omitempty"`
- ProviderID string `json:"providerID,omitempty"`
- PricingType PricingType `json:"pricingType,omitempty"`
- ArchType string `json:"archType,omitempty"`
- }
- // IsSpot determines whether or not a Node uses spot by usage type
- func (n *Node) IsSpot() bool {
- if n != nil {
- return strings.Contains(n.UsageType, "spot") || strings.Contains(n.UsageType, "emptible")
- } else {
- return false
- }
- }
- type OrphanedResource struct {
- Kind string `json:"resourceKind"`
- Region string `json:"region"`
- Description map[string]string `json:"description"`
- Size *int64 `json:"diskSizeInGB,omitempty"`
- DiskName string `json:"diskName,omitempty"`
- Url string `json:"url"`
- Address string `json:"ipAddress,omitempty"`
- MonthlyCost *float64 `json:"monthlyCost"`
- }
- // PV is the interface by which the provider and cost model communicate PV prices.
- // The provider will best-effort try to fill out this struct.
- type PV struct {
- Cost string `json:"hourlyCost"`
- CostPerIO string `json:"costPerIOOperation"`
- Class string `json:"storageClass"`
- Size string `json:"size"`
- Region string `json:"region"`
- ProviderID string `json:"providerID,omitempty"`
- Parameters map[string]string `json:"parameters"`
- }
- // Key represents a way for nodes to match between the k8s API and a pricing API
- type Key interface {
- ID() string // ID represents an exact match
- Features() string // Features are a comma separated string of node metadata that could match pricing
- GPUType() string // GPUType returns "" if no GPU exists or GPUs, but the name of the GPU otherwise
- GPUCount() int // GPUCount returns 0 if no GPU exists or GPUs, but the number of attached GPUs otherwise
- }
- type PVKey interface {
- Features() string
- GetStorageClass() string
- ID() string
- }
- // OutOfClusterAllocation represents a cloud provider cost not associated with kubernetes
- type OutOfClusterAllocation struct {
- Aggregator string `json:"aggregator"`
- Environment string `json:"environment"`
- Service string `json:"service"`
- Cost float64 `json:"cost"`
- Cluster string `json:"cluster"`
- }
- type CustomPricing struct {
- Provider string `json:"provider"`
- Description string `json:"description"`
- // CPU a string-encoded float describing cost per core-hour of CPU.
- CPU string `json:"CPU"`
- // CPU a string-encoded float describing cost per core-hour of CPU for spot
- // nodes.
- SpotCPU string `json:"spotCPU"`
- // RAM a string-encoded float describing cost per GiB-hour of RAM/memory.
- RAM string `json:"RAM"`
- // SpotRAM a string-encoded float describing cost per GiB-hour of RAM/memory
- // for spot nodes.
- SpotRAM string `json:"spotRAM"`
- GPU string `json:"GPU"`
- SpotGPU string `json:"spotGPU"`
- // Storage is a string-encoded float describing cost per GB-hour of storage
- // (e.g. PV, disk) resources.
- Storage string `json:"storage"`
- ZoneNetworkEgress string `json:"zoneNetworkEgress"`
- RegionNetworkEgress string `json:"regionNetworkEgress"`
- InternetNetworkEgress string `json:"internetNetworkEgress"`
- FirstFiveForwardingRulesCost string `json:"firstFiveForwardingRulesCost"`
- AdditionalForwardingRuleCost string `json:"additionalForwardingRuleCost"`
- LBIngressDataCost string `json:"LBIngressDataCost"`
- SpotLabel string `json:"spotLabel,omitempty"`
- SpotLabelValue string `json:"spotLabelValue,omitempty"`
- GpuLabel string `json:"gpuLabel,omitempty"`
- GpuLabelValue string `json:"gpuLabelValue,omitempty"`
- ServiceKeyName string `json:"awsServiceKeyName,omitempty"`
- ServiceKeySecret string `json:"awsServiceKeySecret,omitempty"`
- AlibabaServiceKeyName string `json:"alibabaServiceKeyName,omitempty"`
- AlibabaServiceKeySecret string `json:"alibabaServiceKeySecret,omitempty"`
- AlibabaClusterRegion string `json:"alibabaClusterRegion,omitempty"`
- SpotDataRegion string `json:"awsSpotDataRegion,omitempty"`
- SpotDataBucket string `json:"awsSpotDataBucket,omitempty"`
- SpotDataPrefix string `json:"awsSpotDataPrefix,omitempty"`
- ProjectID string `json:"projectID,omitempty"`
- AthenaProjectID string `json:"athenaProjectID,omitempty"`
- AthenaBucketName string `json:"athenaBucketName"`
- AthenaRegion string `json:"athenaRegion"`
- AthenaDatabase string `json:"athenaDatabase"`
- AthenaCatalog string `json:"athenaCatalog"`
- AthenaTable string `json:"athenaTable"`
- AthenaWorkgroup string `json:"athenaWorkgroup"`
- MasterPayerARN string `json:"masterPayerARN"`
- AthenaCURVersion string `json:"athenaCURVersion,omitempty"` // "1.0" or "2.0", defaults to "2.0"
- BillingDataDataset string `json:"billingDataDataset,omitempty"`
- CustomPricesEnabled string `json:"customPricesEnabled"`
- DefaultIdle string `json:"defaultIdle"`
- AzureSubscriptionID string `json:"azureSubscriptionID"`
- AzureClientID string `json:"azureClientID"`
- AzureClientSecret string `json:"azureClientSecret"`
- AzureTenantID string `json:"azureTenantID"`
- AzureBillingRegion string `json:"azureBillingRegion"`
- AzureBillingAccount string `json:"azureBillingAccount"`
- AzureOfferDurableID string `json:"azureOfferDurableID"`
- AzureStorageSubscriptionID string `json:"azureStorageSubscriptionID"`
- AzureStorageAccount string `json:"azureStorageAccount"`
- AzureStorageAccessKey string `json:"azureStorageAccessKey"`
- AzureStorageContainer string `json:"azureStorageContainer"`
- AzureContainerPath string `json:"azureContainerPath"`
- AzureCloud string `json:"azureCloud"`
- CurrencyCode string `json:"currencyCode"`
- Discount string `json:"discount"`
- NegotiatedDiscount string `json:"negotiatedDiscount"`
- SharedOverhead string `json:"sharedOverhead"`
- ClusterName string `json:"clusterName"`
- ClusterAccountID string `json:"clusterAccount,omitempty"`
- SharedNamespaces string `json:"sharedNamespaces"`
- SharedLabelNames string `json:"sharedLabelNames"`
- SharedLabelValues string `json:"sharedLabelValues"`
- ShareTenancyCosts string `json:"shareTenancyCosts"` // TODO clean up configuration so we can use a type other that string (this should be a bool, but the app panics if it's not a string)
- ReadOnly string `json:"readOnly"`
- EditorAccess string `json:"editorAccess"`
- KubecostToken string `json:"kubecostToken"`
- GoogleAnalyticsTag string `json:"googleAnalyticsTag"`
- ExcludeProviderID string `json:"excludeProviderID"`
- DefaultLBPrice string `json:"defaultLBPrice"`
- }
- // GetSharedOverheadCostPerMonth parses and returns a float64 representation
- // of the configured monthly shared overhead cost. If the string version cannot
- // be parsed into a float, an error is logged and 0.0 is returned.
- func (cp *CustomPricing) GetSharedOverheadCostPerMonth() float64 {
- // Empty string should be interpreted as "no cost", i.e. 0.0
- if cp.SharedOverhead == "" {
- return 0.0
- }
- // Attempt to parse, but log and return 0.0 if that fails.
- sharedCostPerMonth, err := strconv.ParseFloat(cp.SharedOverhead, 64)
- if err != nil {
- log.Errorf("SharedOverhead: failed to parse shared overhead \"%s\": %s", cp.SharedOverhead, err)
- return 0.0
- }
- return sharedCostPerMonth
- }
- func sanitizeFloatString(number string, allowNaN bool) (string, error) {
- num, err := strconv.ParseFloat(number, 64)
- if err != nil {
- return "", fmt.Errorf("expected a string representing a number; got '%s'", number)
- }
- if !allowNaN && math.IsNaN(num) {
- return "", fmt.Errorf("expected a string representing a number; got 'NaN'")
- }
- // Format the numerical string we just parsed.
- return strconv.FormatFloat(num, 'f', -1, 64), nil
- }
- func SetCustomPricingField(obj *CustomPricing, name string, value string) error {
- structValue := reflect.ValueOf(obj).Elem()
- structFieldValue := structValue.FieldByName(name)
- if !structFieldValue.IsValid() {
- return fmt.Errorf("no such field: %s in obj", name)
- }
- if !structFieldValue.CanSet() {
- return fmt.Errorf("cannot set %s field value", name)
- }
- // If the custom pricing field is expected to be a string representation
- // of a floating point number, e.g. a resource price, then do some extra
- // validation work in order to prevent "NaN" and other invalid strings
- // from getting set here.
- switch strings.ToLower(name) {
- case "cpu", "gpu", "ram", "spotcpu", "spotgpu", "spotram", "storage", "zonenetworkegress", "regionnetworkegress", "internetnetworkegress":
- // If we are sent an empty string, ignore the key and don't change the value
- if value == "" {
- return nil
- } else {
- // Validate that "value" represents a real floating point number, and
- // set precision, bits, etc. Do not allow NaN.
- val, err := sanitizeFloatString(value, false)
- if err != nil {
- return fmt.Errorf("invalid numeric value for field '%s': %s", name, value)
- }
- value = val
- }
- default:
- }
- structFieldType := structFieldValue.Type()
- value = sanitizePolicy.Sanitize(value)
- val := reflect.ValueOf(value)
- if structFieldType != val.Type() {
- return fmt.Errorf("provided value type didn't match custom pricing field type")
- }
- structFieldValue.Set(val)
- return nil
- }
- type PricingSources struct {
- PricingSources map[string]*PricingSource
- }
- type PricingSource struct {
- Name string `json:"name"`
- Enabled bool `json:"enabled"`
- Available bool `json:"available"`
- Error string `json:"error"`
- }
- type PricingType string
- const (
- Api PricingType = "api"
- Spot PricingType = "spot"
- Reserved PricingType = "reserved"
- SavingsPlan PricingType = "savingsPlan"
- CsvExact PricingType = "csvExact"
- CsvClass PricingType = "csvClass"
- DefaultPrices PricingType = "defaultPrices"
- )
- type PricingMatchMetadata struct {
- TotalNodes int `json:"TotalNodes"`
- PricingTypeCounts map[PricingType]int `json:"PricingType"`
- }
- // Provider represents a k8s provider.
- type Provider interface {
- ClusterInfo() (map[string]string, error)
- GetAddresses() ([]byte, error)
- GetDisks() ([]byte, error)
- GetOrphanedResources() ([]OrphanedResource, error)
- NodePricing(Key) (*Node, PricingMetadata, error)
- GpuPricing(map[string]string) (string, error)
- PVPricing(PVKey) (*PV, error)
- NetworkPricing() (*Network, error) // TODO: add key interface arg for dynamic price fetching
- LoadBalancerPricing() (*LoadBalancer, error) // TODO: add key interface arg for dynamic price fetching
- AllNodePricing() (interface{}, error)
- DownloadPricingData() error
- GetKey(map[string]string, *clustercache.Node) Key
- GetPVKey(*clustercache.PersistentVolume, map[string]string, string) PVKey
- UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error)
- UpdateConfigFromConfigMap(map[string]string) (*CustomPricing, error)
- GetConfig() (*CustomPricing, error)
- GetManagementPlatform() (string, error)
- ApplyReservedInstancePricing(map[string]*Node)
- ServiceAccountStatus() *ServiceAccountStatus
- PricingSourceStatus() map[string]*PricingSource
- ClusterManagementPricing() (string, float64, error)
- CombinedDiscountForNode(string, bool, float64, float64) float64
- Regions() []string
- PricingSourceSummary() interface{}
- }
- // ProviderConfig describes config storage common to all providers.
- type ProviderConfig interface {
- ConfigFileManager() *config.ConfigFileManager
- GetCustomPricingData() (*CustomPricing, error)
- Update(func(*CustomPricing) error) (*CustomPricing, error)
- UpdateFromMap(map[string]string) (*CustomPricing, error)
- }
|