package kubecost import ( "sync" "time" "golang.org/x/exp/slices" ) // AuditType the types of Audits, each of which should be contained in an AuditSet type AuditType string const ( AuditAllocationReconciliation AuditType = "AuditAllocationReconciliation" AuditAllocationTotalStore AuditType = "AuditAllocationTotalStore" AuditAllocationAggStore AuditType = "AuditAllocationAggStore" AuditAssetReconciliation AuditType = "AuditAssetReconciliation" AuditAssetTotalStore AuditType = "AuditAssetTotalStore" AuditAssetAggStore AuditType = "AuditAssetAggStore" AuditClusterEquality AuditType = "AuditClusterEquality" AuditAll AuditType = "" AuditInvalidType AuditType = "InvalidType" ) // ToAuditType converts a string to an Audit type func ToAuditType(check string) AuditType { switch check { case string(AuditAllocationReconciliation): return AuditAllocationReconciliation case string(AuditAllocationTotalStore): return AuditAllocationTotalStore case string(AuditAllocationAggStore): return AuditAllocationAggStore case string(AuditAssetReconciliation): return AuditAssetReconciliation case string(AuditAssetTotalStore): return AuditAssetTotalStore case string(AuditAssetAggStore): return AuditAssetAggStore case string(AuditClusterEquality): return AuditClusterEquality case string(AuditAll): return AuditAll default: return AuditInvalidType } } // AuditStatus are possible outcomes of an audit type AuditStatus string const ( FailedStatus AuditStatus = "Failed" WarningStatus = "Warning" PassedStatus = "Passed" ) // AuditMissingValue records when a value that should be present in a store or in the audit generated results are missing type AuditMissingValue struct { Description string Key string } // AuditFloatResult structure for holding the results of a failed audit on a float value, Expected should be the value // calculated by the Audit func while Actual is what is contained in the relevant store. type AuditFloatResult struct { Expected float64 Actual float64 } // Clone returns a deep copy of the caller func (afr *AuditFloatResult) Clone() *AuditFloatResult { return &AuditFloatResult{ Expected: afr.Expected, Actual: afr.Actual, } } // AllocationReconciliationAudit records the differences of between compute resources (cpu, ram, gpu) costs between // allocations by nodes and node assets keyed on node name and compute resource type AllocationReconciliationAudit struct { Status AuditStatus Description string LastRun time.Time Resources map[string]map[string]*AuditFloatResult MissingValues []*AuditMissingValue } // Clone returns a deep copy of the caller func (ara *AllocationReconciliationAudit) Clone() *AllocationReconciliationAudit { if ara == nil { return nil } resources := make(map[string]map[string]*AuditFloatResult, len(ara.Resources)) for node, resourceMap := range ara.Resources { copyResourceMap := make(map[string]*AuditFloatResult, len(resourceMap)) for resourceName, val := range resourceMap { copyResourceMap[resourceName] = val.Clone() } resources[node] = copyResourceMap } return &AllocationReconciliationAudit{ Status: ara.Status, Description: ara.Description, LastRun: ara.LastRun, Resources: resources, MissingValues: slices.Clone(ara.MissingValues), } } // TotalAudit records the differences between a total store and the totaled results of the store that it is based on // keyed by cluster and node names type TotalAudit struct { Status AuditStatus Description string LastRun time.Time TotalByNode map[string]*AuditFloatResult TotalByCluster map[string]*AuditFloatResult MissingValues []*AuditMissingValue } // Clone returns a deep copy of the caller func (ta *TotalAudit) Clone() *TotalAudit { if ta == nil { return nil } tbn := make(map[string]*AuditFloatResult, len(ta.TotalByNode)) for k, v := range ta.TotalByNode { tbn[k] = v } tbc := make(map[string]*AuditFloatResult, len(ta.TotalByNode)) for k, v := range ta.TotalByCluster { tbc[k] = v } return &TotalAudit{ Status: ta.Status, Description: ta.Description, LastRun: ta.LastRun, TotalByNode: tbn, TotalByCluster: tbc, MissingValues: slices.Clone(ta.MissingValues), } } // AggAudit contains the results of an Audit on an AggStore keyed on aggregation prop and Allocation key type AggAudit struct { Status AuditStatus Description string LastRun time.Time Results map[string]map[string]*AuditFloatResult MissingValues []*AuditMissingValue } // Clone returns a deep copy of the caller func (aa *AggAudit) Clone() *AggAudit { if aa == nil { return nil } res := make(map[string]map[string]*AuditFloatResult, len(aa.Results)) for aggType, aggResults := range aa.Results { copyAggResult := make(map[string]*AuditFloatResult, len(aggResults)) for aggName, auditFloatResult := range aggResults { copyAggResult[aggName] = auditFloatResult } res[aggType] = copyAggResult } return &AggAudit{ Status: aa.Status, Description: aa.Description, LastRun: aa.LastRun, Results: res, MissingValues: slices.Clone(aa.MissingValues), } } // AssetReconciliationAudit records differences in assets and the Cloud type AssetReconciliationAudit struct { Status AuditStatus Description string LastRun time.Time Results map[string]map[string]*AuditFloatResult MissingValues []*AuditMissingValue } // Clone returns a deep copy of the caller func (ara *AssetReconciliationAudit) Clone() *AssetReconciliationAudit { res := make(map[string]map[string]*AuditFloatResult, len(ara.Results)) for aggType, aggResults := range ara.Results { copyAggResult := make(map[string]*AuditFloatResult, len(aggResults)) for aggName, auditFloatResult := range aggResults { copyAggResult[aggName] = auditFloatResult } res[aggType] = copyAggResult } return &AssetReconciliationAudit{ Status: ara.Status, Description: ara.Description, LastRun: ara.LastRun, Results: res, MissingValues: slices.Clone(ara.MissingValues), } } // EqualityAudit records the difference in cost between Allocations and Assets aggregated by cluster and keyed on cluster type EqualityAudit struct { Status AuditStatus Description string LastRun time.Time Clusters map[string]*AuditFloatResult MissingValues []*AuditMissingValue } // Clone returns a deep copy of the caller func (ea *EqualityAudit) Clone() *EqualityAudit { if ea == nil { return nil } clusters := make(map[string]*AuditFloatResult, len(ea.Clusters)) for k, v := range ea.Clusters { clusters[k] = v } return &EqualityAudit{ Status: ea.Status, Description: ea.Description, LastRun: ea.LastRun, Clusters: clusters, MissingValues: slices.Clone(ea.MissingValues), } } // AuditCoverage tracks coverage of each audit type type AuditCoverage struct { AllocationReconciliation Window `json:"allocationReconciliation"` AllocationAgg Window `json:"allocationAgg"` AllocationTotal Window `json:"allocationTotal"` AssetTotal Window `json:"assetTotal"` AssetReconciliation Window `json:"assetReconciliation"` ClusterEquality Window `json:"clusterEquality"` } // NewAuditCoverage create default AuditCoverage func NewAuditCoverage() *AuditCoverage { return &AuditCoverage{} } // Update expands the coverage of each Window in the coverage that the given AuditSet's Window if the corresponding Audit is not nil // Note: This means of determining coverage can lead to holes in the given window func (ac *AuditCoverage) Update(as *AuditSet) { if as != nil && as.AllocationReconciliation != nil { ac.AllocationReconciliation.Expand(as.Window) ac.AllocationAgg.Expand(as.Window) ac.AllocationTotal.Expand(as.Window) ac.AssetTotal.Expand(as.Window) ac.AssetReconciliation.Expand(as.Window) ac.ClusterEquality.Expand(as.Window) } } // AuditSet is a ETLSet which contains all kind of Audits for a given Window type AuditSet struct { sync.RWMutex AllocationReconciliation *AllocationReconciliationAudit `json:"allocationReconciliation"` AllocationAgg *AggAudit `json:"allocationAgg"` AllocationTotal *TotalAudit `json:"allocationTotal"` AssetTotal *TotalAudit `json:"assetTotal"` AssetReconciliation *AssetReconciliationAudit `json:"assetReconciliation"` ClusterEquality *EqualityAudit `json:"clusterEquality"` Window Window `json:"window"` } // NewAuditSet creates an empty AuditSet with the given window func NewAuditSet(start, end time.Time) *AuditSet { return &AuditSet{ Window: NewWindow(&start, &end), } } // UpdateAuditSet overwrites any audit fields in the caller with those in the given AuditSet which are not nil func (as *AuditSet) UpdateAuditSet(that *AuditSet) *AuditSet { if as == nil { return that } if that.AllocationReconciliation != nil { as.AllocationReconciliation = that.AllocationReconciliation } if that.AllocationAgg != nil { as.AllocationAgg = that.AllocationAgg } if that.AllocationTotal != nil { as.AllocationTotal = that.AllocationTotal } if that.AssetTotal != nil { as.AssetTotal = that.AssetTotal } if that.AssetReconciliation != nil { as.AssetReconciliation = that.AssetReconciliation } if that.ClusterEquality != nil { as.ClusterEquality = that.ClusterEquality } return as } // ConstructSet fulfills the ETLSet interface to provide an empty version of itself so that it can be initialized in its // generic form. func (as *AuditSet) ConstructSet() ETLSet { return &AuditSet{} } // IsEmpty returns true if any of the audits are non-nil func (as *AuditSet) IsEmpty() bool { return as == nil || (as.AllocationReconciliation == nil && as.AllocationAgg == nil && as.AllocationTotal == nil && as.AssetTotal == nil && as.AssetReconciliation == nil && as.ClusterEquality == nil) } // GetWindow returns AuditSet Window func (as *AuditSet) GetWindow() Window { return as.Window } // Clone returns a deep copy of the caller func (as *AuditSet) Clone() *AuditSet { if as == nil { return nil } as.RLock() defer as.RUnlock() return &AuditSet{ AllocationReconciliation: as.AllocationReconciliation.Clone(), AllocationAgg: as.AllocationAgg.Clone(), AllocationTotal: as.AllocationTotal.Clone(), AssetTotal: as.AssetTotal.Clone(), AssetReconciliation: as.AssetReconciliation.Clone(), ClusterEquality: as.ClusterEquality.Clone(), Window: as.Window.Clone(), } } // CloneSet returns a deep copy of the caller and returns set func (as *AuditSet) CloneSet() ETLSet { return as.Clone() } // AuditSetRange SetRange of AuditSets type AuditSetRange struct { SetRange[*AuditSet] }