Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GTM-52 Fix parsing of GCP pricing and create new node diagnostic pricing API #2083

Merged
merged 1 commit into from Aug 14, 2023

Conversation

nikovacevic
Copy link
Contributor

@nikovacevic nikovacevic commented Aug 11, 2023

What does this PR change?

  • Added support for GCP T2D instance types.
  • Fixes an issue where GCP pricing would fail if there were two nodes of the same family (e.g. e2-small, e2-standard) present.
  • Adds a new Node Pricing Diagnostic API for detecting issues with node pricing. (This is only supported on GCP to start.)

Does this PR relate to any other PRs?

How will this PR impact users?

  • These specific node pricing problems will be resolved.

Does this PR address any GitHub or Zendesk issues?

How was this PR tested?

  • Unit tests
  • Manually, using the new diagnostic API

Here's evidence of the node type at issue, working for currency IDR:
Screenshot from 2023-08-11 16-32-25

Does this PR require changes to documentation?

  • No need to document this API just yet. It's internal only.

Have you labeled this PR and its corresponding Issue as "next release" if it should be part of the next OpenCost release? If not, why not?

  • Yes

@nikovacevic nikovacevic force-pushed the niko/GTM-52/pricing branch 2 times, most recently from 02f7a10 to 214ebb0 Compare August 11, 2023 22:25
@nikovacevic nikovacevic marked this pull request as ready for review August 11, 2023 22:38
Comment on lines +763 to +771

if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "T2D AMD") {
instanceType = "t2dstandard"
}
if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "T2A ARM") {
instanceType = "t2astandard"
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the proximate user issue.

Comment on lines +859 to +869
if len(product.PricingInfo) > 0 {
lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
if lastRateIndex >= 0 {
nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
unitsBaseCurrency, err = strconv.Atoi(product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Units)
if err != nil {
return nil, "", fmt.Errorf("error parsing base unit price for instance: %w", err)
}
} else {
continue
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes some panics. (We used to check the length of the slice after we'd already accessed [0] haha.)

}
break
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this, and the other break, allows pricing to work for multiple instance types from the same family.

}
break
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this, and the other break, allows pricing to work for multiple instance types from the same family.

product = &GCPPricing{}
product.Node = &models.Node{
log.Debugf("GCP Billing API: key '%s': RAM price: %f", candidateKey, hourlyPrice)
pricing := &GCPPricing{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this to a new variable (i.e. pricing := not product =) prevents nil issues and other failures when the original product data needs to be reused later on.

product = &GCPPricing{}
product.Node = &models.Node{
log.Debugf("GCP Billing API: key '%s': RAM price: %f", candidateKeyGPU, hourlyPrice)
pricing := &GCPPricing{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this to a new variable (i.e. pricing := not product =) prevents nil issues and other failures when the original product data needs to be reused later on.

product = &GCPPricing{}
product.Node = &models.Node{
log.Debugf("GCP Billing API: key '%s': CPU price: %f", candidateKey, hourlyPrice)
pricing := &GCPPricing{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this to a new variable (i.e. pricing := not product =) prevents nil issues and other failures when the original product data needs to be reused later on.

product = &GCPPricing{}
product.Node = &models.Node{
log.Debugf("GCP Billing API: key '%s': CPU price: %f", candidateKeyGPU, hourlyPrice)
pricing := &GCPPricing{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this to a new variable (i.e. pricing := not product =) prevents nil issues and other failures when the original product data needs to be reused later on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

brutal. so this is your mutated loop var issue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Seems obvious now, but was not fun to find.

}
if _, ok := gcpPricingList[candidateKey]; ok {
log.Debugf("GCP Billing API: key '%s': RAM price: %f", candidateKey, hourlyPrice)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unified these logs so that they're more legible and clearly related to each other:

{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,a2highgpu,ondemand': RAM price: 0.004237"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,a2highgpu,ondemand,gpu': RAM price: 0.004237"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,a2highgpu,ondemand': CPU price: 0.031611"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,a2highgpu,ondemand,gpu': CPU price: 0.031611"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'asia-southeast1,t2dstandard,ondemand': RAM price: 68.205000"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'asia-southeast1,t2dstandard,ondemand,gpu': RAM price: 68.205000"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'asia-southeast1,t2dstandard,ondemand': CPU price: 508.934997"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'asia-southeast1,t2dstandard,ondemand,gpu': CPU price: 508.934997"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,e2medium,ondemand': CPU price: 327.173848"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,e2medium,ondemand,gpu': CPU price: 327.173848"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,e2standard,ondemand': CPU price: 327.173848"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,e2standard,ondemand,gpu': CPU price: 327.173848"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,e2medium,ondemand': RAM price: 43.852950"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,e2medium,ondemand,gpu': RAM price: 43.852950"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,e2standard,ondemand': RAM price: 43.852950"}
{"level":"debug","time":"2023-08-11T16:46:04-06:00","message":"GCP Billing API: key 'us-central1,e2standard,ondemand,gpu': RAM price: 43.852950"}

@@ -1539,25 +1552,55 @@ func (gcp *GCP) isValidPricingKey(key models.Key) bool {
}

// NodePricing returns GCP pricing data for a single node
func (gcp *GCP) NodePricing(key models.Key) (*models.Node, error) {
func (gcp *GCP) NodePricing(key models.Key) (*models.Node, models.PricingMetadata, error) {
meta := models.PricingMetadata{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is not materially changed. It just has extra lines adding in the metadata for the new diagnostics API.

// Load a reader object on a portion of a GCP api response
// Confirm that the resting *GCP object contains the correctly parsed pricing info
func TestParsePage(t *testing.T) {
func TestKeyFeatures(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new test.

// tests basic parsing of GCP pricing API responses
// Load a reader object on a portion of a GCP api response
// Confirm that the resting *GCP object contains the correctly parsed pricing info
func TestParsePage(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has been updated to add more SKUs, and the SKUs themselves were moved to a testing file.

@@ -273,7 +273,7 @@ type Provider interface {
GetAddresses() ([]byte, error)
GetDisks() ([]byte, error)
GetOrphanedResources() ([]OrphanedResource, error)
NodePricing(Key) (*Node, error)
NodePricing(Key) (*Node, PricingMetadata, error)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change just allows us to use the very same function for getting pricing, and reporting metadata about where that data came from.

product = &GCPPricing{}
product.Node = &models.Node{
log.Debugf("GCP Billing API: key '%s': CPU price: %f", candidateKeyGPU, hourlyPrice)
pricing := &GCPPricing{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

brutal. so this is your mutated loop var issue

pkg/cloud/gcp/provider.go Outdated Show resolved Hide resolved
…ing API

Signed-off-by: Niko Kovacevic <nikovacevic@gmail.com>
Copy link
Contributor

@michaelmdresser michaelmdresser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trusting Alex's review

@nikovacevic nikovacevic merged commit 820f962 into develop Aug 14, 2023
3 checks passed
@nikovacevic nikovacevic deleted the niko/GTM-52/pricing branch August 14, 2023 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants