pricingset.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. package pricing
  2. import (
  3. "cmp"
  4. "encoding/hex"
  5. "fmt"
  6. "hash/fnv"
  7. "slices"
  8. "time"
  9. )
  10. type PricingSet struct {
  11. ClusterPricing []*ClusterPricing `json:"clusterPricing" yaml:"clusterPricing"`
  12. NetworkPricing []*NetworkPricing `json:"networkPricing" yaml:"networkPricing"`
  13. NodePricing []*NodePricing `json:"nodePricing" yaml:"nodePricing"`
  14. PersistentVolumePricing []*PersistentVolumePricing `json:"persistentVolumePricing" yaml:"persistentVolumePricing"`
  15. ServicePricing []*ServicePricing `json:"servicePricing" yaml:"servicePricing"`
  16. }
  17. func (ps *PricingSet) IsEmpty() bool {
  18. if ps == nil {
  19. return true
  20. }
  21. return len(ps.ClusterPricing) == 0 &&
  22. len(ps.NetworkPricing) == 0 &&
  23. len(ps.NodePricing) == 0 &&
  24. len(ps.PersistentVolumePricing) == 0 &&
  25. len(ps.ServicePricing) == 0
  26. }
  27. // Checksum returns a hash that is stable across map and slice ordering and
  28. // sensitive to both pricing properties and price values.
  29. //
  30. // TODO: Consider commutative hash folding via a multiset hashing algorithm
  31. // if the string-based Checksum() implementation is too resource intensive
  32. // for large pricing sets. For now, this string version is more readable.
  33. func (ps *PricingSet) Checksum() (string, error) {
  34. if ps == nil {
  35. ps = &PricingSet{}
  36. }
  37. // Each item's String() is prefixed with its kind so that items of
  38. // different kinds cannot collide, then all keys are sorted to make the
  39. // hash independent of input ordering.
  40. keys := make([]string, 0,
  41. len(ps.ClusterPricing)+
  42. len(ps.NetworkPricing)+
  43. len(ps.NodePricing)+
  44. len(ps.PersistentVolumePricing)+
  45. len(ps.ServicePricing))
  46. for _, cp := range ps.ClusterPricing {
  47. keys = append(keys, "cluster:"+cp.String())
  48. }
  49. for _, np := range ps.NetworkPricing {
  50. keys = append(keys, "network:"+np.String())
  51. }
  52. for _, np := range ps.NodePricing {
  53. keys = append(keys, "node:"+np.String())
  54. }
  55. for _, pvp := range ps.PersistentVolumePricing {
  56. keys = append(keys, "persistentvolume:"+pvp.String())
  57. }
  58. for _, sp := range ps.ServicePricing {
  59. keys = append(keys, "service:"+sp.String())
  60. }
  61. slices.Sort(keys)
  62. hasher := fnv.New64a()
  63. for _, key := range keys {
  64. if _, err := hasher.Write([]byte(key)); err != nil {
  65. return "", fmt.Errorf("fnv hash: %w", err)
  66. }
  67. }
  68. return hex.EncodeToString(hasher.Sum(nil)), nil
  69. }
  70. // Clone returns a deep copy of the PricingSet. Mutating the returned set
  71. // (including its nested slices, maps, and time pointers) does not affect the
  72. // original, and vice versa. A nil receiver returns an empty, non-nil set.
  73. func (ps *PricingSet) Clone() *PricingSet {
  74. if ps == nil {
  75. return &PricingSet{}
  76. }
  77. return &PricingSet{
  78. ClusterPricing: cloneSlice(ps.ClusterPricing, (*ClusterPricing).clone),
  79. NetworkPricing: cloneSlice(ps.NetworkPricing, (*NetworkPricing).clone),
  80. NodePricing: cloneSlice(ps.NodePricing, (*NodePricing).clone),
  81. PersistentVolumePricing: cloneSlice(ps.PersistentVolumePricing, (*PersistentVolumePricing).clone),
  82. ServicePricing: cloneSlice(ps.ServicePricing, (*ServicePricing).clone),
  83. }
  84. }
  85. // cloneSlice deep-copies a slice of pointers using the provided element clone
  86. // function, preserving a nil source slice as nil.
  87. func cloneSlice[T any](src []*T, clone func(*T) *T) []*T {
  88. if src == nil {
  89. return nil
  90. }
  91. dst := make([]*T, len(src))
  92. for i, e := range src {
  93. dst[i] = clone(e)
  94. }
  95. return dst
  96. }
  97. func (cp *ClusterPricing) clone() *ClusterPricing {
  98. if cp == nil {
  99. return nil
  100. }
  101. clone := *cp
  102. clone.Properties.Start = cloneTime(cp.Properties.Start)
  103. clone.Properties.End = cloneTime(cp.Properties.End)
  104. clone.Prices = clonePrices(cp.Prices)
  105. return &clone
  106. }
  107. func (np *NetworkPricing) clone() *NetworkPricing {
  108. if np == nil {
  109. return nil
  110. }
  111. clone := *np
  112. clone.Properties.Start = cloneTime(np.Properties.Start)
  113. clone.Properties.End = cloneTime(np.Properties.End)
  114. clone.Prices = clonePrices(np.Prices)
  115. return &clone
  116. }
  117. func (np *NodePricing) clone() *NodePricing {
  118. if np == nil {
  119. return nil
  120. }
  121. clone := *np
  122. clone.Properties.Labels = cloneLabels(np.Properties.Labels)
  123. clone.Properties.Start = cloneTime(np.Properties.Start)
  124. clone.Properties.End = cloneTime(np.Properties.End)
  125. clone.Prices = clonePrices(np.Prices)
  126. return &clone
  127. }
  128. func (pvp *PersistentVolumePricing) clone() *PersistentVolumePricing {
  129. if pvp == nil {
  130. return nil
  131. }
  132. clone := *pvp
  133. clone.Properties.Labels = cloneLabels(pvp.Properties.Labels)
  134. clone.Properties.Start = cloneTime(pvp.Properties.Start)
  135. clone.Properties.End = cloneTime(pvp.Properties.End)
  136. clone.Prices = clonePrices(pvp.Prices)
  137. return &clone
  138. }
  139. func (sp *ServicePricing) clone() *ServicePricing {
  140. if sp == nil {
  141. return nil
  142. }
  143. clone := *sp
  144. clone.Properties.Start = cloneTime(sp.Properties.Start)
  145. clone.Properties.End = cloneTime(sp.Properties.End)
  146. clone.Prices = clonePrices(sp.Prices)
  147. return &clone
  148. }
  149. // clonePrices returns a deep copy of a Prices map, preserving nil as nil. Price
  150. // values contain no reference fields, so a shallow value copy per entry is safe.
  151. func clonePrices(src Prices) Prices {
  152. if src == nil {
  153. return nil
  154. }
  155. dst := make(Prices, len(src))
  156. for k, v := range src {
  157. dst[k] = v
  158. }
  159. return dst
  160. }
  161. // cloneLabels returns a deep copy of a labels map, preserving nil as nil.
  162. func cloneLabels(src map[string]string) map[string]string {
  163. if src == nil {
  164. return nil
  165. }
  166. dst := make(map[string]string, len(src))
  167. for k, v := range src {
  168. dst[k] = v
  169. }
  170. return dst
  171. }
  172. // cloneTime returns a copy of the time pointer, preserving nil as nil.
  173. func cloneTime(src *time.Time) *time.Time {
  174. if src == nil {
  175. return nil
  176. }
  177. t := *src
  178. return &t
  179. }
  180. // Sort sorts the pricing data to ensure deterministic serialization.
  181. func (ps *PricingSet) Sort() {
  182. if ps == nil {
  183. return
  184. }
  185. // Sort clusters
  186. slices.SortFunc(ps.ClusterPricing, func(a, b *ClusterPricing) int {
  187. return cmp.Compare(a.String(), b.String())
  188. })
  189. // Sort network
  190. slices.SortFunc(ps.NetworkPricing, func(a, b *NetworkPricing) int {
  191. return cmp.Compare(a.String(), b.String())
  192. })
  193. // Sort nodes
  194. slices.SortFunc(ps.NodePricing, func(a, b *NodePricing) int {
  195. return cmp.Compare(a.String(), b.String())
  196. })
  197. // Sort persistent volumes
  198. slices.SortFunc(ps.PersistentVolumePricing, func(a, b *PersistentVolumePricing) int {
  199. return cmp.Compare(a.String(), b.String())
  200. })
  201. // Sort services
  202. slices.SortFunc(ps.ServicePricing, func(a, b *ServicePricing) int {
  203. return cmp.Compare(a.String(), b.String())
  204. })
  205. }