6868type ClusterCost struct {
6969 sync.RWMutex
7070 npCostMap map [string ]* NodePoolCost // nodepool.Name -> NodePoolCost
71- // nodeClaimSet tracks which NodeClaims are currently being monitored for cost
71+ // nodeClaimMap tracks which NodeClaims are currently being monitored for cost
7272 nodeClaimMap map [types.NamespacedName ]NodeClaimMetaData // nodeClaim object key -> NodeClaimMetaData
7373
7474 cloudProvider cloudprovider.CloudProvider
@@ -79,7 +79,7 @@ type ClusterCost struct {
7979// It maintains the current cost, available instance types, and count of active offerings.
8080type NodePoolCost struct {
8181 cost float64
82- // offeringCounts tracks how many instances of each offering type are currently active
82+ // offeringCounts tracks how many instances of each offering type are currently active (Count > 0)
8383 offeringCounts map [OfferingKey ]OfferingCount
8484}
8585
@@ -103,7 +103,7 @@ type NodeClaimMetaData struct {
103103
104104// NewClusterCost creates and initializes a new ClusterCost instance for tracking
105105// compute costs across the cluster. It requires a cloud provider for accessing
106- // instance type and pricing information, and a Kubernetes client for NodePool loofferingKeyups .
106+ // instance type and pricing information, and a Kubernetes client for NodePool lookups .
107107func NewClusterCost (ctx context.Context , cloudProvider cloudprovider.CloudProvider , client client.Client ) * ClusterCost {
108108 return & ClusterCost {
109109 npCostMap : make (map [string ]* NodePoolCost ),
@@ -125,75 +125,35 @@ func (cc *ClusterCost) UpdateOfferings(ctx context.Context, np *v1.NodePool, ins
125125 cc .internalUpdateOfferings (np , instanceTypes )
126126}
127127
128- func (cc * ClusterCost ) internalNodepoolUpdate (ctx context.Context , np * v1.NodePool ) error {
129- instanceTypes , err := cc .cloudProvider .GetInstanceTypes (ctx , np )
130- if err != nil {
131- return fmt .Errorf ("failed to get instance types for nodepool %q, %w" , np .Name , err )
132- }
133- cc .internalUpdateOfferings (np , instanceTypes )
134- return nil
135- }
136-
137128func (cc * ClusterCost ) internalUpdateOfferings (np * v1.NodePool , instanceTypes []* cloudprovider.InstanceType ) {
138- npCost , exists := cc .npCostMap [np .Name ]
129+ instanceTypes = lo .Filter (instanceTypes , func (it * cloudprovider.InstanceType , _ int ) bool {
130+ return it != nil
131+ })
139132
133+ npCost , exists := cc .npCostMap [np .Name ]
140134 if ! exists {
141- cc .createNewNodePoolCost (np .Name , instanceTypes )
142- } else {
143- instanceTypes = lo .Filter (instanceTypes , func (it * cloudprovider.InstanceType , _ int ) bool {
144- return it != nil
145- })
146- newMap := map [OfferingKey ]OfferingCount {}
147- for _ , it := range instanceTypes {
148- for _ , o := range it .Offerings {
149- offeringKey := OfferingKey {InstanceName : it .Name , Zone : o .Zone (), CapacityType : o .CapacityType ()}
150- oldCount , exists := npCost .offeringCounts [offeringKey ]
151- newMap [offeringKey ] = OfferingCount {
152- Count : lo .Ternary (exists , oldCount .Count , 0 ),
153- Price : o .Price ,
154- }
155- }
156- }
157- // Add back all of the offering counts that don't exist in the new instance types
158- // This can't occur on container restart, so we may lose cost data from offerings that are no longer returned
159- // from the cloud provider but still have nodeclaims.
160- for key , count := range npCost .offeringCounts {
161- _ , exists := newMap [key ]
162- if ! exists {
163- newMap [key ] = count
164- }
165- }
166-
167- npCost .offeringCounts = newMap
168- // re-calculate the cost as the instances have changed
169- cost := npCost .updateCost ()
170- cc .npCostMap [np .Name ].cost = cost
171- }
172- }
173-
174- func (npc * NodePoolCost ) updateCost () float64 {
175- cost := 0.0
176- for _ , oc := range npc .offeringCounts {
177- // add the new price times the count of that offering
178- cost = cost + (float64 (oc .Count ) * oc .Price )
135+ cc .npCostMap [np .Name ] = & NodePoolCost {offeringCounts : make (map [OfferingKey ]OfferingCount ), cost : 0.0 }
136+ return
179137 }
180- return cost
181- }
182138
183- func (cc * ClusterCost ) createNewNodePoolCost (npName string , instanceTypes []* cloudprovider.InstanceType ) {
184- // create the new npc
185- cc .npCostMap [npName ] = & NodePoolCost {
186- offeringCounts : make (map [OfferingKey ]OfferingCount ),
187- cost : 0.0 ,
188- }
139+ // Build a temporary price index from the new instance types
140+ prices := make (map [OfferingKey ]float64 , len (instanceTypes )* 3 )
189141 for _ , it := range instanceTypes {
190142 for _ , o := range it .Offerings {
191- cc .npCostMap [npName ].offeringCounts [OfferingKey {InstanceName : it .Name , Zone : o .Zone (), CapacityType : o .CapacityType ()}] = OfferingCount {
192- Count : 0 ,
193- Price : o .Price ,
194- }
143+ prices [OfferingKey {InstanceName : it .Name , Zone : o .Zone (), CapacityType : o .CapacityType ()}] = o .Price
144+ }
145+ }
146+
147+ // Update prices on active offerings and recalculate cost
148+ cost := 0.0
149+ for key , oc := range npCost .offeringCounts {
150+ if newPrice , ok := prices [key ]; ok {
151+ oc .Price = newPrice
152+ npCost .offeringCounts [key ] = oc
195153 }
154+ cost += float64 (oc .Count ) * oc .Price
196155 }
156+ npCost .cost = cost
197157}
198158
199159// UpdateNodeClaim adds a NodeClaim to cost tracking. The NodeClaim must have
@@ -286,38 +246,30 @@ func (cc *ClusterCost) internalAddOffering(ctx context.Context, npName string, o
286246 return err
287247 }
288248
289- _ , exists := cc .npCostMap [npName ]
290- if ! exists {
291- // create the new npc
292- instanceTypes , err := cc .cloudProvider .GetInstanceTypes (ctx , np )
293- if err != nil {
294- return fmt .Errorf ("failed to get instance types for new nodepool %q while adding offering for instance %q, %w" , np .Name , offeringKey .InstanceName , err )
295- }
296- cc .createNewNodePoolCost (npName , instanceTypes )
249+ if _ , exists := cc .npCostMap [npName ]; ! exists {
250+ cc .npCostMap [npName ] = & NodePoolCost {offeringCounts : make (map [OfferingKey ]OfferingCount ), cost : 0.0 }
297251 }
298252
299253 oc , exists := cc .npCostMap [npName ].offeringCounts [offeringKey ]
300254 if ! exists {
301- // our offerings must be out of date, we should update and retry
302- err := cc .internalNodepoolUpdate (ctx , np )
255+ instanceTypes , err := cc .cloudProvider .GetInstanceTypes (ctx , np )
303256 if err != nil {
304- return fmt .Errorf ("failed to update nodepool %q during retry while searching for offering for instance %q in zone %q with capacity %q , %w" , np .Name , offeringKey .InstanceName , offeringKey . Zone , offeringKey . CapacityType , err )
257+ return fmt .Errorf ("failed to get instance types for nodepool %q while adding offering for instance %q, %w" , np .Name , offeringKey .InstanceName , err )
305258 }
306- oc , exists = cc .npCostMap [npName ].offeringCounts [offeringKey ]
307- if ! exists {
308- // Start at 0; the unconditional oc.Count += 1 below accounts for this add.
309- oc = OfferingCount {Count : 0 , Price : 0.0 }
310- log .FromContext (ctx ).Error (fmt .Errorf ("failed to find offering %q during retry while searching for instance %q in zone %q with capacity %q in nodepool %q" , offeringKey , offeringKey .InstanceName , offeringKey .Zone , offeringKey .CapacityType , npName ), "offering price unknown after retry — cost tracking will undercount for this nodeclaim until next UpdateOfferings" )
259+ price , found := findOfferingPrice (instanceTypes , offeringKey )
260+ if ! found {
261+ log .FromContext (ctx ).Error (fmt .Errorf ("failed to find offering for instance %q in zone %q with capacity %q in nodepool %q" , offeringKey .InstanceName , offeringKey .Zone , offeringKey .CapacityType , npName ), "offering price unknown — cost tracking will undercount for this nodeclaim until next update" )
311262 }
263+ oc = OfferingCount {Count : 0 , Price : price }
312264 }
313- oc .Count += 1
265+ oc .Count ++
314266 cc .npCostMap [npName ].offeringCounts [offeringKey ] = oc
315267 cc .npCostMap [npName ].cost += oc .Price
316268 return nil
317269}
318270
319271// internalRemoveOffering updates the internal clusterCost state to remove an existing offering for a given nodepool.
320- // It is used to decrement the overall cost when a node leeaves the cluster. It is only called by DeleteNodeClaim
272+ // It is used to decrement the overall cost when a node leaves the cluster. It is only called by DeleteNodeClaim
321273// after that function has determined if a nodeclaim is already being accounted for.
322274func (cc * ClusterCost ) internalRemoveOffering (npName string , offeringKey OfferingKey ) error {
323275 npc , exists := cc .npCostMap [npName ]
@@ -330,13 +282,14 @@ func (cc *ClusterCost) internalRemoveOffering(npName string, offeringKey Offerin
330282 return fmt .Errorf ("attempted to remove nonexistent offering from nodepool %q (instance, %q, zone, %q, capacity, %q)" , npName , offeringKey .InstanceName , offeringKey .Zone , offeringKey .CapacityType )
331283 }
332284
333- oc .Count -= 1
334- npc .offeringCounts [offeringKey ] = oc
285+ oc .Count --
335286 npc .cost -= oc .Price
336287 if oc .Count == 0 {
337288 delete (npc .offeringCounts , offeringKey )
289+ } else {
290+ npc .offeringCounts [offeringKey ] = oc
338291 }
339- if len (lo . Values ( npc .offeringCounts ) ) == 0 {
292+ if len (npc .offeringCounts ) == 0 {
340293 delete (cc .npCostMap , npName )
341294 }
342295 return nil
@@ -369,6 +322,20 @@ func (cc *ClusterCost) GetNodepoolCost(np *v1.NodePool) float64 {
369322 return npc .cost
370323}
371324
325+ func findOfferingPrice (instanceTypes []* cloudprovider.InstanceType , key OfferingKey ) (float64 , bool ) {
326+ for _ , it := range instanceTypes {
327+ if it == nil || it .Name != key .InstanceName {
328+ continue
329+ }
330+ for _ , o := range it .Offerings {
331+ if o .Zone () == key .Zone && o .CapacityType () == key .CapacityType {
332+ return o .Price , true
333+ }
334+ }
335+ }
336+ return 0 , false
337+ }
338+
372339func nodeClaimMissingLabels (nc v1.NodeClaim ) bool {
373340 var missingLabels []string
374341 for _ , key := range NecessaryLabels {
0 commit comments