diff --git a/api/v1alpha1/servicepricing_types.go b/api/v1alpha1/servicepricing_types.go index 3f3dc32..e9ee9b5 100644 --- a/api/v1alpha1/servicepricing_types.go +++ b/api/v1alpha1/servicepricing_types.go @@ -14,6 +14,7 @@ import ( // rather than parsing ServiceConfiguration. // // +kubebuilder:validation:XValidation:rule="self.chargeType != 'Usage' || (has(self.metric) && has(self.pricingUnit) && has(self.rates))",message="Usage ServicePricing requires metric, pricingUnit, and rates" +// +kubebuilder:validation:XValidation:rule="self.chargeType == 'Usage' || !has(self.unitConversion)",message="unitConversion is only allowed for Usage ServicePricing" // +kubebuilder:validation:XValidation:rule="self.chargeType != 'OneTime' || (has(self.amount) && has(self.trigger))",message="OneTime ServicePricing requires amount and trigger" // +kubebuilder:validation:XValidation:rule="self.chargeType != 'Recurring' || (has(self.amount) && has(self.interval))",message="Recurring ServicePricing requires amount and interval" type ServicePricingSpec struct { @@ -59,6 +60,14 @@ type ServicePricingSpec struct { // +kubebuilder:validation:MaxLength=64 PricingUnit string `json:"pricingUnit,omitempty"` + // UnitConversion converts the meter's usage into pricingUnit before the + // rates apply, for a metric measured in a different unit than it is + // priced in (e.g. bytes priced per GiB). Omitted when the metric is + // already measured in pricingUnit. Usage charges only. + // + // +kubebuilder:validation:Optional + UnitConversion *UnitConversion `json:"unitConversion,omitempty"` + // Rates is the ordered list of rate entries for Usage charges. // Required when chargeType is Usage. // @@ -85,6 +94,31 @@ type ServicePricingSpec struct { Interval ChargeInterval `json:"interval,omitempty"` } +// UnitConversion converts usage measured in the meter's unit into the +// ServicePricing's pricingUnit: usage in pricingUnit = metered usage / +// meterUnitsPerPricingUnit. Rates and tier bounds stay in pricingUnit, and +// invoices show the converted quantity. +type UnitConversion struct { + // MeterUnitsPerPricingUnit is how many of the meter's units make one + // pricingUnit, as a positive decimal string (e.g. "1073741824" for a + // meter in bytes priced per GiB, "1000" for millicore-seconds priced + // per vCPU-second). + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=64 + // +kubebuilder:validation:Pattern=`^([1-9]\d*(\.\d+)?|0\.\d*[1-9]\d*)$` + MeterUnitsPerPricingUnit string `json:"meterUnitsPerPricingUnit"` + + // Description explains the conversion to the people who maintain the + // pricing (e.g. "Memory is metered in byte-seconds and priced per + // GiB-second; 1 GiB = 2^30 bytes"). + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=512 + Description string `json:"description"` +} + // ServicePricingStatus defines the observed state of a ServicePricing. type ServicePricingStatus struct { // Conditions represent the latest available observations of the diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 60020eb..275db39 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1633,6 +1633,11 @@ func (in *ServicePricingSnapshot) DeepCopy() *ServicePricingSnapshot { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServicePricingSpec) DeepCopyInto(out *ServicePricingSpec) { *out = *in + if in.UnitConversion != nil { + in, out := &in.UnitConversion, &out.UnitConversion + *out = new(UnitConversion) + **out = **in + } if in.Rates != nil { in, out := &in.Rates, &out.Rates *out = make([]PricingRate, len(*in)) @@ -1688,3 +1693,18 @@ func (in *TaxID) DeepCopy() *TaxID { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UnitConversion) DeepCopyInto(out *UnitConversion) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UnitConversion. +func (in *UnitConversion) DeepCopy() *UnitConversion { + if in == nil { + return nil + } + out := new(UnitConversion) + in.DeepCopyInto(out) + return out +} diff --git a/applyconfiguration/api/v1alpha1/servicepricingspec.go b/applyconfiguration/api/v1alpha1/servicepricingspec.go index ce96b81..eba7b90 100644 --- a/applyconfiguration/api/v1alpha1/servicepricingspec.go +++ b/applyconfiguration/api/v1alpha1/servicepricingspec.go @@ -35,6 +35,11 @@ type ServicePricingSpecApplyConfiguration struct { // "gib"). Required when chargeType is Usage. Does not need to be the // literal UCUM unit string of the meter. PricingUnit *string `json:"pricingUnit,omitempty"` + // UnitConversion converts the meter's usage into pricingUnit before the + // rates apply, for a metric measured in a different unit than it is + // priced in (e.g. bytes priced per GiB). Omitted when the metric is + // already measured in pricingUnit. Usage charges only. + UnitConversion *UnitConversionApplyConfiguration `json:"unitConversion,omitempty"` // Rates is the ordered list of rate entries for Usage charges. // Required when chargeType is Usage. Rates []PricingRateApplyConfiguration `json:"rates,omitempty"` @@ -101,6 +106,14 @@ func (b *ServicePricingSpecApplyConfiguration) WithPricingUnit(value string) *Se return b } +// WithUnitConversion sets the UnitConversion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UnitConversion field is set to the value of the last call. +func (b *ServicePricingSpecApplyConfiguration) WithUnitConversion(value *UnitConversionApplyConfiguration) *ServicePricingSpecApplyConfiguration { + b.UnitConversion = value + return b +} + // WithRates adds the given value to the Rates field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Rates field. diff --git a/applyconfiguration/api/v1alpha1/unitconversion.go b/applyconfiguration/api/v1alpha1/unitconversion.go new file mode 100644 index 0000000..e4f5262 --- /dev/null +++ b/applyconfiguration/api/v1alpha1/unitconversion.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// UnitConversionApplyConfiguration represents a declarative configuration of the UnitConversion type for use +// with apply. +// +// UnitConversion converts usage measured in the meter's unit into the +// ServicePricing's pricingUnit: usage in pricingUnit = metered usage / +// meterUnitsPerPricingUnit. Rates and tier bounds stay in pricingUnit, and +// invoices show the converted quantity. +type UnitConversionApplyConfiguration struct { + // MeterUnitsPerPricingUnit is how many of the meter's units make one + // pricingUnit, as a positive decimal string (e.g. "1073741824" for a + // meter in bytes priced per GiB, "1000" for millicore-seconds priced + // per vCPU-second). + MeterUnitsPerPricingUnit *string `json:"meterUnitsPerPricingUnit,omitempty"` + // Description explains the conversion to the people who maintain the + // pricing (e.g. "Memory is metered in byte-seconds and priced per + // GiB-second; 1 GiB = 2^30 bytes"). + Description *string `json:"description,omitempty"` +} + +// UnitConversionApplyConfiguration constructs a declarative configuration of the UnitConversion type for use with +// apply. +func UnitConversion() *UnitConversionApplyConfiguration { + return &UnitConversionApplyConfiguration{} +} + +// WithMeterUnitsPerPricingUnit sets the MeterUnitsPerPricingUnit field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the MeterUnitsPerPricingUnit field is set to the value of the last call. +func (b *UnitConversionApplyConfiguration) WithMeterUnitsPerPricingUnit(value string) *UnitConversionApplyConfiguration { + b.MeterUnitsPerPricingUnit = &value + return b +} + +// WithDescription sets the Description field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Description field is set to the value of the last call. +func (b *UnitConversionApplyConfiguration) WithDescription(value string) *UnitConversionApplyConfiguration { + b.Description = &value + return b +} diff --git a/applyconfiguration/utils.go b/applyconfiguration/utils.go index 6834fbd..398c1cf 100644 --- a/applyconfiguration/utils.go +++ b/applyconfiguration/utils.go @@ -142,6 +142,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.ServicePricingStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("TaxID"): return &apiv1alpha1.TaxIDApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("UnitConversion"): + return &apiv1alpha1.UnitConversionApplyConfiguration{} } return nil diff --git a/config/base/crd/bases/billing.miloapis.com_offers.yaml b/config/base/crd/bases/billing.miloapis.com_offers.yaml index 8b35003..6fa96e4 100644 --- a/config/base/crd/bases/billing.miloapis.com_offers.yaml +++ b/config/base/crd/bases/billing.miloapis.com_offers.yaml @@ -262,6 +262,34 @@ spec: enum: - BillingAccountActivation type: string + unitConversion: + description: |- + UnitConversion converts the meter's usage into pricingUnit before the + rates apply, for a metric measured in a different unit than it is + priced in (e.g. bytes priced per GiB). Omitted when the metric is + already measured in pricingUnit. Usage charges only. + properties: + description: + description: |- + Description explains the conversion to the people who maintain the + pricing (e.g. "Memory is metered in byte-seconds and priced per + GiB-second; 1 GiB = 2^30 bytes"). + maxLength: 512 + minLength: 1 + type: string + meterUnitsPerPricingUnit: + description: |- + MeterUnitsPerPricingUnit is how many of the meter's units make one + pricingUnit, as a positive decimal string (e.g. "1073741824" for a + meter in bytes priced per GiB, "1000" for millicore-seconds priced + per vCPU-second). + maxLength: 64 + pattern: ^([1-9]\d*(\.\d+)?|0\.\d*[1-9]\d*)$ + type: string + required: + - description + - meterUnitsPerPricingUnit + type: object required: - chargeType - currency @@ -272,6 +300,8 @@ spec: and rates rule: self.chargeType != 'Usage' || (has(self.metric) && has(self.pricingUnit) && has(self.rates)) + - message: unitConversion is only allowed for Usage ServicePricing + rule: self.chargeType == 'Usage' || !has(self.unitConversion) - message: OneTime ServicePricing requires amount and trigger rule: self.chargeType != 'OneTime' || (has(self.amount) && has(self.trigger)) diff --git a/config/base/crd/bases/billing.miloapis.com_servicepricings.yaml b/config/base/crd/bases/billing.miloapis.com_servicepricings.yaml index 6bbc8f0..175c133 100644 --- a/config/base/crd/bases/billing.miloapis.com_servicepricings.yaml +++ b/config/base/crd/bases/billing.miloapis.com_servicepricings.yaml @@ -187,6 +187,34 @@ spec: enum: - BillingAccountActivation type: string + unitConversion: + description: |- + UnitConversion converts the meter's usage into pricingUnit before the + rates apply, for a metric measured in a different unit than it is + priced in (e.g. bytes priced per GiB). Omitted when the metric is + already measured in pricingUnit. Usage charges only. + properties: + description: + description: |- + Description explains the conversion to the people who maintain the + pricing (e.g. "Memory is metered in byte-seconds and priced per + GiB-second; 1 GiB = 2^30 bytes"). + maxLength: 512 + minLength: 1 + type: string + meterUnitsPerPricingUnit: + description: |- + MeterUnitsPerPricingUnit is how many of the meter's units make one + pricingUnit, as a positive decimal string (e.g. "1073741824" for a + meter in bytes priced per GiB, "1000" for millicore-seconds priced + per vCPU-second). + maxLength: 64 + pattern: ^([1-9]\d*(\.\d+)?|0\.\d*[1-9]\d*)$ + type: string + required: + - description + - meterUnitsPerPricingUnit + type: object required: - chargeType - currency @@ -196,6 +224,8 @@ spec: - message: Usage ServicePricing requires metric, pricingUnit, and rates rule: self.chargeType != 'Usage' || (has(self.metric) && has(self.pricingUnit) && has(self.rates)) + - message: unitConversion is only allowed for Usage ServicePricing + rule: self.chargeType == 'Usage' || !has(self.unitConversion) - message: OneTime ServicePricing requires amount and trigger rule: self.chargeType != 'OneTime' || (has(self.amount) && has(self.trigger)) - message: Recurring ServicePricing requires amount and interval diff --git a/docs/emitting-usage.md b/docs/emitting-usage.md index d4e6078..2c0c262 100644 --- a/docs/emitting-usage.md +++ b/docs/emitting-usage.md @@ -184,8 +184,8 @@ if err != nil { billing account. The billing controllers attribute project → account using the `BillingAccountBinding` graph at processing time. - **Do not use floating point.** `Quantity` is `int64`. Measure - cpu-*seconds*, not cpu-hours; bytes, not gibibytes. The meter's - `consumedUnit`/`pricingUnit` handle scale. + cpu-*seconds*, not cpu-hours; bytes, not gibibytes. A ServicePricing + priced in a larger unit converts with `unitConversion`. ## Emission contract