|
|
@@ -4,18 +4,16 @@ import (
|
|
|
"database/sql"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
- "io"
|
|
|
"net"
|
|
|
"net/http"
|
|
|
"regexp"
|
|
|
- "strconv"
|
|
|
"strings"
|
|
|
- "sync"
|
|
|
"time"
|
|
|
|
|
|
"golang.org/x/text/cases"
|
|
|
"golang.org/x/text/language"
|
|
|
|
|
|
+ "github.com/opencost/opencost/pkg/cloud/types"
|
|
|
"github.com/opencost/opencost/pkg/kubecost"
|
|
|
|
|
|
"github.com/opencost/opencost/pkg/util"
|
|
|
@@ -49,314 +47,9 @@ var createTableStatements = []string{
|
|
|
);`,
|
|
|
}
|
|
|
|
|
|
-// 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"`
|
|
|
- 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"`
|
|
|
-}
|
|
|
-
|
|
|
-// 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
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// LoadBalancer is the interface by which the provider and cost model communicate LoadBalancer prices.
|
|
|
-// The provider will best-effort try to fill out this struct.
|
|
|
-type LoadBalancer struct {
|
|
|
- IngressIPAddresses []string `json:"IngressIPAddresses"`
|
|
|
- Cost float64 `json:"hourlyCost"`
|
|
|
-}
|
|
|
-
|
|
|
-// TODO: used for dynamic cloud provider price fetching.
|
|
|
-// determine what identifies a load balancer in the json returned from the cloud provider pricing API call
|
|
|
-// type LBKey interface {
|
|
|
-// }
|
|
|
-
|
|
|
-// Network is the interface by which the provider and cost model communicate network egress prices.
|
|
|
-// The provider will best-effort try to fill out this struct.
|
|
|
-type Network struct {
|
|
|
- ZoneNetworkEgressCost float64
|
|
|
- RegionNetworkEgressCost float64
|
|
|
- InternetNetworkEgressCost float64
|
|
|
-}
|
|
|
-
|
|
|
-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"`
|
|
|
- AthenaTable string `json:"athenaTable"`
|
|
|
- AthenaWorkgroup string `json:"athenaWorkgroup"`
|
|
|
- MasterPayerARN string `json:"masterPayerARN"`
|
|
|
- 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
|
|
|
-}
|
|
|
-
|
|
|
-type ServiceAccountStatus struct {
|
|
|
- Checks []*ServiceAccountCheck `json:"checks"`
|
|
|
-}
|
|
|
-
|
|
|
-// ServiceAccountChecks is a thread safe map for holding ServiceAccountCheck objects
|
|
|
-type ServiceAccountChecks struct {
|
|
|
- sync.RWMutex
|
|
|
- serviceAccountChecks map[string]*ServiceAccountCheck
|
|
|
-}
|
|
|
-
|
|
|
-// NewServiceAccountChecks initialize ServiceAccountChecks
|
|
|
-func NewServiceAccountChecks() *ServiceAccountChecks {
|
|
|
- return &ServiceAccountChecks{
|
|
|
- serviceAccountChecks: make(map[string]*ServiceAccountCheck),
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (sac *ServiceAccountChecks) set(key string, check *ServiceAccountCheck) {
|
|
|
- sac.Lock()
|
|
|
- defer sac.Unlock()
|
|
|
- sac.serviceAccountChecks[key] = check
|
|
|
-}
|
|
|
-
|
|
|
-// getStatus extracts ServiceAccountCheck objects into a slice and returns them in a ServiceAccountStatus
|
|
|
-func (sac *ServiceAccountChecks) getStatus() *ServiceAccountStatus {
|
|
|
- sac.Lock()
|
|
|
- defer sac.Unlock()
|
|
|
- checks := []*ServiceAccountCheck{}
|
|
|
- for _, v := range sac.serviceAccountChecks {
|
|
|
- checks = append(checks, v)
|
|
|
- }
|
|
|
- return &ServiceAccountStatus{
|
|
|
- Checks: checks,
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-type ServiceAccountCheck struct {
|
|
|
- Message string `json:"message"`
|
|
|
- Status bool `json:"status"`
|
|
|
- AdditionalInfo string `json:"additionalInfo"`
|
|
|
-}
|
|
|
-
|
|
|
-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, 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, *v1.Node) Key
|
|
|
- GetPVKey(*v1.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)
|
|
|
- GetLocalStorageQuery(time.Duration, time.Duration, bool, bool) string
|
|
|
- ApplyReservedInstancePricing(map[string]*Node)
|
|
|
- ServiceAccountStatus() *ServiceAccountStatus
|
|
|
- PricingSourceStatus() map[string]*PricingSource
|
|
|
- ClusterManagementPricing() (string, float64, error)
|
|
|
- CombinedDiscountForNode(string, bool, float64, float64) float64
|
|
|
- Regions() []string
|
|
|
- PricingSourceSummary() interface{}
|
|
|
-}
|
|
|
-
|
|
|
// ClusterName returns the name defined in cluster info, defaulting to the
|
|
|
// CLUSTER_ID environment variable
|
|
|
-func ClusterName(p Provider) string {
|
|
|
+func ClusterName(p types.Provider) string {
|
|
|
info, err := p.ClusterInfo()
|
|
|
if err != nil {
|
|
|
return env.GetClusterID()
|
|
|
@@ -372,7 +65,7 @@ func ClusterName(p Provider) string {
|
|
|
|
|
|
// CustomPricesEnabled returns the boolean equivalent of the cloup provider's custom prices flag,
|
|
|
// indicating whether or not the cluster is using custom pricing.
|
|
|
-func CustomPricesEnabled(p Provider) bool {
|
|
|
+func CustomPricesEnabled(p types.Provider) bool {
|
|
|
config, err := p.GetConfig()
|
|
|
if err != nil {
|
|
|
return false
|
|
|
@@ -387,7 +80,7 @@ func CustomPricesEnabled(p Provider) bool {
|
|
|
|
|
|
// ConfigWatcherFor returns a new ConfigWatcher instance which watches changes to the "pricing-configs"
|
|
|
// configmap
|
|
|
-func ConfigWatcherFor(p Provider) *watcher.ConfigMapWatcher {
|
|
|
+func ConfigWatcherFor(p types.Provider) *watcher.ConfigMapWatcher {
|
|
|
return &watcher.ConfigMapWatcher{
|
|
|
ConfigMapName: env.GetPricingConfigmapName(),
|
|
|
WatchFunc: func(name string, data map[string]string) error {
|
|
|
@@ -398,7 +91,7 @@ func ConfigWatcherFor(p Provider) *watcher.ConfigMapWatcher {
|
|
|
}
|
|
|
|
|
|
// AllocateIdleByDefault returns true if the application settings specify to allocate idle by default
|
|
|
-func AllocateIdleByDefault(p Provider) bool {
|
|
|
+func AllocateIdleByDefault(p types.Provider) bool {
|
|
|
config, err := p.GetConfig()
|
|
|
if err != nil {
|
|
|
return false
|
|
|
@@ -408,7 +101,7 @@ func AllocateIdleByDefault(p Provider) bool {
|
|
|
}
|
|
|
|
|
|
// SharedNamespace returns a list of names of shared namespaces, as defined in the application settings
|
|
|
-func SharedNamespaces(p Provider) []string {
|
|
|
+func SharedNamespaces(p types.Provider) []string {
|
|
|
namespaces := []string{}
|
|
|
|
|
|
config, err := p.GetConfig()
|
|
|
@@ -429,7 +122,7 @@ func SharedNamespaces(p Provider) []string {
|
|
|
// SharedLabel returns the configured set of shared labels as a parallel tuple of keys to values; e.g.
|
|
|
// for app:kubecost,type:staging this returns (["app", "type"], ["kubecost", "staging"]) in order to
|
|
|
// match the signature of the NewSharedResourceInfo
|
|
|
-func SharedLabels(p Provider) ([]string, []string) {
|
|
|
+func SharedLabels(p types.Provider) ([]string, []string) {
|
|
|
names := []string{}
|
|
|
values := []string{}
|
|
|
|
|
|
@@ -459,7 +152,7 @@ func SharedLabels(p Provider) ([]string, []string) {
|
|
|
|
|
|
// ShareTenancyCosts returns true if the application settings specify to share
|
|
|
// tenancy costs by default.
|
|
|
-func ShareTenancyCosts(p Provider) bool {
|
|
|
+func ShareTenancyCosts(p types.Provider) bool {
|
|
|
config, err := p.GetConfig()
|
|
|
if err != nil {
|
|
|
return false
|
|
|
@@ -469,7 +162,7 @@ func ShareTenancyCosts(p Provider) bool {
|
|
|
}
|
|
|
|
|
|
// NewProvider looks at the nodespec or provider metadata server to decide which provider to instantiate.
|
|
|
-func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.ConfigFileManager) (Provider, error) {
|
|
|
+func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.ConfigFileManager) (types.Provider, error) {
|
|
|
nodes := cache.GetAllNodes()
|
|
|
if len(nodes) == 0 {
|
|
|
log.Infof("Could not locate any nodes for cluster.") // valid in ETL readonly mode
|
|
|
@@ -528,7 +221,7 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
|
|
|
Config: NewProviderConfig(config, cp.configFileName),
|
|
|
clusterRegion: cp.region,
|
|
|
clusterAccountID: cp.accountID,
|
|
|
- serviceAccountChecks: NewServiceAccountChecks(),
|
|
|
+ serviceAccountChecks: types.NewServiceAccountChecks(),
|
|
|
}, nil
|
|
|
case kubecost.AzureProvider:
|
|
|
log.Info("Found ProviderID starting with \"azure\", using Azure Provider")
|
|
|
@@ -537,7 +230,7 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
|
|
|
Config: NewProviderConfig(config, cp.configFileName),
|
|
|
clusterRegion: cp.region,
|
|
|
clusterAccountID: cp.accountID,
|
|
|
- serviceAccountChecks: NewServiceAccountChecks(),
|
|
|
+ serviceAccountChecks: types.NewServiceAccountChecks(),
|
|
|
}, nil
|
|
|
case kubecost.AlibabaProvider:
|
|
|
log.Info("Found ProviderID starting with \"alibaba\", using Alibaba Cloud Provider")
|
|
|
@@ -546,7 +239,7 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
|
|
|
Config: NewProviderConfig(config, cp.configFileName),
|
|
|
clusterRegion: cp.region,
|
|
|
clusterAccountId: cp.accountID,
|
|
|
- serviceAccountChecks: NewServiceAccountChecks(),
|
|
|
+ serviceAccountChecks: types.NewServiceAccountChecks(),
|
|
|
}, nil
|
|
|
case kubecost.ScalewayProvider:
|
|
|
log.Info("Found ProviderID starting with \"scaleway\", using Scaleway Provider")
|