Skip to content

fix(gcp): derive CUD commitment type from machine family - #1650

Merged
cristim merged 3 commits into
mainfrom
fix/1538-gcp-cud-commitment-type
Aug 3, 2026
Merged

cristim merged 3 commits into
mainfrom
fix/1538-gcp-cud-commitment-type

Conversation

@cristim

@cristim cristim commented Jul 28, 2026 •

Copy link
Copy Markdown
Member

What was bought vs. what was requested

buildInsertRequest pinned the GCP commitment Type to the string literal
"GENERAL_PURPOSE" on every purchase. GENERAL_PURPOSE is the N1-only
member of computepb.Commitment_Type (the SDK's own enum doc: "Type
GENERAL_PURPOSE specifies a commitment that applies only to eligible resources
of general purpose N1 machine series"
), so requested and purchased diverged on
every non-N1 recommendation:

Recommended (rec.ResourceType) Purchased Type (before) Purchased Type (after)
n2-standard-16 GENERAL_PURPOSE GENERAL_PURPOSE_N2
c3-highcpu-22 GENERAL_PURPOSE COMPUTE_OPTIMIZED_C3
m3-ultramem-32 GENERAL_PURPOSE MEMORY_OPTIMIZED_M3
a3-megagpu-8g GENERAL_PURPOSE ACCELERATOR_OPTIMIZED_A3_MEGA

Commitment.Type selects which machine series the CUD discounts. An N1
commitment discounts no N2/C3/M3/A3 instance, so the customer paid the full
1- or 3-year commitment on top of undimmed on-demand charges, while the
purchase was reported as realized savings. The recommended machine type was
already on rec.ResourceType but was only read into the Description string.

The fix

Why a silent default is unacceptable here

An unmappable family returns an explicit error naming the machine type and the
purchase is refused before any Insert reaches GCP. It does not fall back
to GENERAL_PURPOSE. On a money path a silent default spends real money on a
commitment that discounts nothing and cannot be cancelled — strictly worse than
refusing and surfacing the gap.

Deliberately unmapped (fail loud): m4 and x4, whose commitment types split
into size buckets (MEMORY_OPTIMIZED_M4 vs _M4_6TB; _X4_480_6T,
_X4_960_12T, _X4_1440_24T, …) that are not derivable from the machine-type
name; t2a; and any series GCP has not published a Commitment_Type for.

Also in this diff

The sibling ResourceCommitment VCPU/MEMORY wire values now come from
computepb.ResourceCommitment enum constants instead of string literals,
keeping the #1022 "MEMORY_MB" regression compiler-checked. termPlan already
sourced TWELVE_MONTH/THIRTY_SIX_MONTH from the SDK enum and fails loud, so
it needed no change.

Regression coverage

Asserts on the Type carried by the InsertRegionCommitmentRequest actually
handed to the SDK client (captured by the mock), not on an intermediate struct
that could be right while the wire value is wrong:

  • TestComputeEngineClient_PurchaseCommitment_TypeMatchesMachineFamily
  • TestComputeEngineClient_PurchaseCommitment_UnmappableFamilyRefuses — asserts
    an error and that insertReqs stays empty.
  • TestComputeEngineClient_PurchaseCommitment_ResourceTypesUseSDKEnums
  • TestCommitmentTypeForMachineType_MapsFamilyToSDKEnum / _UnmappableFailsLoud

Confirmed failing pre-fix. Reverting only the Type derivation:

--- FAIL: TestComputeEngineClient_PurchaseCommitment_TypeMatchesMachineFamily
    --- PASS: .../n1-standard-4
    --- FAIL: .../n2-standard-16
        Error: Not equal:
            expected: "GENERAL_PURPOSE_N2"
            actual  : "GENERAL_PURPOSE"
        Messages: n2-standard-16 must be committed as GENERAL_PURPOSE_N2;
                  buying GENERAL_PURPOSE instead discounts nothing (issue LeanerCloud/cloud-commitments-cli#1538)
    --- FAIL: .../c3-highcpu-22   expected "COMPUTE_OPTIMIZED_C3",   actual "GENERAL_PURPOSE"
    --- FAIL: .../m3-ultramem-32  expected "MEMORY_OPTIMIZED_M3",    actual "GENERAL_PURPOSE"
    --- FAIL: .../a3-megagpu-8g   expected "ACCELERATOR_OPTIMIZED_A3_MEGA", actual "GENERAL_PURPOSE"

--- FAIL: TestComputeEngineClient_PurchaseCommitment_UnmappableFamilyRefuses
    --- FAIL: .../x4-megamem-960-metal
        Error: An error is expected but got nil.
        Error: Should be empty, but was [commitment_resource:{
                 description:"CUD for x4-megamem-960-metal" plan:"TWELVE_MONTH"
                 resources:{amount:8 type:"VCPU"} resources:{amount:65536 type:"MEMORY"}
                 type:"GENERAL_PURPOSE"} project:"test-project" region:"us-central1"]
        Messages: no Insert may reach GCP for an unmappable machine family (issue LeanerCloud/cloud-commitments-cli#1538)

n1-standard-4 passing pre-fix is the control: GENERAL_PURPOSE was already
correct for N1, which is exactly why the bug was invisible.

Gates

providers/gcp is a separate Go module, so its commands were run from inside
providers/gcp/.

Gate Exit
go build ./... (root) 0
go vet ./... (root) 0
go build ./... (providers/gcp) 0
go vet ./... (providers/gcp) 0
go test ./... (providers/gcp) 0
go test -race ./services/computeengine/ 0
gocyclo -over 10 providers/gcp/ 0 (no output)
gofmt -l providers/gcp/services/computeengine/ 0 (no output)
golangci-lint run ./... (v2.10.1, CI-pinned, whole providers/gcp module) 1
golangci-lint run ./services/computeengine/ (v2.10.1) 1

The golangci-lint exit 1 is pre-existing debt in this module, which is not
covered by CI (#1478).

Corrected measurement (an earlier revision of this body quoted 684, which was
the raw output line count -- each finding prints with ~3 lines of source
context -- not a finding count; scope was also unstated):

Scope, golangci-lint v2.10.1 Findings
providers/gcp/... before this PR 224
providers/gcp/... after this PR 222
providers/gcp/services/computeengine after this PR 50

Zero new findings: the finding sets with line numbers stripped diff to empty in
the new-findings direction. The two-finding drop is incidental -- both are
pre-existing godot "comment should end in a period" findings on doc comments
this PR rewrote. Baseline was produced by re-running the same pinned binary with
this diff reverted in place.

Closes #1538


Review follow-up (commit f5e645c6f)

Input side: the machine-type operation is now selected explicitly

extractResourceTypeFromOperations took the last / segment of the first
operation with a non-empty Resource, with no check that the path named a
machine type. A CUD recommendation's operations reference the commitment
resource itself, so a commitment-only payload set
rec.ResourceType = "cud-001" (the commitment name), which the new
derivation then rejected as machine family cud. The refusal was correct; the
stated cause was invented.

Now: only a resource path containing /machineTypes/ yields a machine type.
When none exists, the extractor returns an error naming the segment it looked
for and the resource paths it actually found, and ResourceType is left
empty rather than filled from an unrelated path segment. The purchase then
refuses with no machine type on the recommendation.

The recommendation is not dropped. Six existing tests depend on
operation-less payloads still converting, and a listed-but-unpurchasable
recommendation surfaces the problem where a silently discarded one would not.

This surfaces a pre-existing bug; it does not create one

The commitment-name-as-machine-type defect is already live on main, not
introduced here. The pre-fix run below emits, from the old extractor:

computeengine: pricing unavailable for cud-001 in us-central1

That is enrichRecWithPricing calling getComputePricing(rec.ResourceType, ...)
with the commitment name. The extractor has been returning a commitment name
as a machine type all along, and the pricing lookup has been silently failing on
it. Nothing consumed ResourceType strictly enough to make that visible.

So the change is not "a risky new refusal". It adds the first consumer that
cannot tolerate a non-machine-type value, which is what makes the pre-existing
data-shape bug observable, and then fixes the extractor that produced it.

Pre-fix proof, restoring only the old "first non-empty Resource, last segment"
extractor while keeping the new tests:

--- FAIL: TestExtractResourceTypeFromRecommendation_CommitmentOnlyOpsFailLoud
    Error:    An error is expected but got nil.
    Messages: a payload with no machineTypes operation must fail loud

--- FAIL: TestPurchaseCommitment_CommitmentOnlyRecommendationRefuses
    Error:    Should be empty, but was cud-001
    Messages: a commitment-only payload must not yield a machine type (issue LeanerCloud/cloud-commitments-cli#1538)
    (log)     computeengine: pricing unavailable for cud-001 in us-central1

--- FAIL: TestMachineTypeFromResourcePath_SelectsOnlyMachineTypePaths
    Error:    Should be false
    Messages: "//compute.googleapis.com/projects/test/regions/us-central1/commitments/cud-001"
              does not name a machine type
    Error:    Should be empty, but was cud-001

Accelerator / graphics / storage families now refuse

buildInsertRequest sends only VCPU and MEMORY. GCP requires an
ACCELERATOR resource in an ACCELERATOR_OPTIMIZED* / GRAPHICS_OPTIMIZED*
commitment and a LOCAL_SSD resource in STORAGE_OPTIMIZED_Z3, so the first
revision newly declared types whose required resources were not being sent.
Best case GCP rejects the insert; worst case it creates a commitment covering
the cheap vCPU/RAM and none of the GPUs or local SSDs that dominate the spend.

a2, a3, a4, g2, g4, z3 now take the same "not derivable, do not
guess" refusal as m4/x4, with an error naming the missing resource kind.
The a3 sub-series map is gone with them.

f1 / g1

Legacy shared-core members of the N1 lineage, correct under GENERAL_PURPOSE
before this mapping existed. Mapped alongside custom-* so the mapping does not
turn a working purchase into a refusal.

GroupCommitments and one vacuous assertion

Deliberately not done

The commitment recommender's operation value for path / carries the
commitment's own type, which would remove the family-mapping guess entirely.
Carrying it to buildInsertRequest requires a new field on
pkg/common.Recommendation -- a separate Go module shared by all three
providers, with persistence implications. Out of scope here; filed as LeanerCloud/cloud-commitments-go#60.

Also filed: LeanerCloud/cloud-commitments-go#61 — GroupCommitments groups by account+region+term only,
ignoring ResourceType, and its CommitmentRequest has no Type field at all,
so a future caller would sum n2 and c3 vCPUs into one untyped commitment. No
non-test caller today, which is exactly when it is cheap to fix.

Gates after the follow-up

Gate Exit
go build ./... (root) 0
go vet ./... (root) 0
go build ./... (providers/gcp) 0
go vet ./... (providers/gcp) 0
go test ./... (providers/gcp) 0 (119 tests in computeengine)
go test -race ./services/computeengine/ 0
gocyclo -over 10 providers/gcp/ 0 (no output)
gofmt -l providers/gcp/services/computeengine/ 0 (no output)
golangci-lint run ./... (v2.10.1, providers/gcp) 1 -- 222 findings, zero new vs the 224-finding baseline

Summary by CodeRabbit

  • Bug Fixes
    • Commitment purchases now use the recommended machine family’s correct commitment type instead of assuming a default.
    • Unsupported, incompatible, or incomplete recommendations are rejected safely before purchase.
    • Resource detection now ignores unrelated paths and avoids guessing when machine details are missing.
    • Commitment and resource types are validated against supported cloud provider values.

@cristim cristim added effort/m Days impact/many Affects most users priority/p0 Drop everything; same-day fix severity/critical Major harm when it happens triaged Item has been triaged type/bug Defect urgency/now Drop other things labels Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6002f29a-cbf3-4e0d-833c-459de44f4ac9

📥 Commits

Reviewing files that changed from the base of the PR and between a233ce0 and 928baaf.

📒 Files selected for processing (2)
  • providers/gcp/services/computeengine/client.go
  • providers/gcp/services/computeengine/client_test.go
📝 Walkthrough

Walkthrough

GCP Compute Engine commitment purchases now derive commitment types from recommended machine families. Recommendation parsing accepts only machine type paths. Unsupported families and recommendations without machine types fail before insertion. Resource types use Compute SDK enum values.

Changes

GCP commitment type and machine resource validation

Layer / File(s) Summary
Machine type extraction and validation
providers/gcp/services/computeengine/client.go, providers/gcp/services/computeengine/client_test.go
Recommendation parsing now accepts only /machineTypes/<name> paths. Commitment-only, instance, malformed, and incomplete paths fail explicitly.
Machine family commitment mapping
providers/gcp/services/computeengine/client.go, providers/gcp/services/computeengine/client_test.go
Machine families map to Compute SDK commitment enums. Unknown, unsupported, accelerator-dependent, and local-SSD-dependent families return errors instead of defaulting to GENERAL_PURPOSE.
Validated commitment request construction
providers/gcp/services/computeengine/client.go, providers/gcp/services/computeengine/client_test.go
Commitment requests use the derived family-specific type and SDK enum values for VCPU and MEMORY. Invalid purchases fail before the GCP insertion call.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Recommender
  participant ComputeEngineClient
  participant buildInsertRequest
  participant ComputeEngineAPI
  Recommender->>ComputeEngineClient: provide recommendation resource paths
  ComputeEngineClient->>ComputeEngineClient: extract machine type
  ComputeEngineClient->>buildInsertRequest: provide machine family and resources
  buildInsertRequest->>buildInsertRequest: derive SDK commitment type
  buildInsertRequest->>ComputeEngineAPI: insert validated commitment
  ComputeEngineAPI-->>ComputeEngineClient: return purchase result
Loading

Possibly related issues

  • LeanerCloud/CUDly#1663: Concerns GCP commitment type determination in the same client and overlaps with the machine-family mapping changes.
  • LeanerCloud/CUDly#1561: Modifies GCP recommendation and commitment handling in the same client, although it addresses different logic.

Possibly related PRs

  • LeanerCloud/CUDly#666: Modifies ComputeEngineClient.PurchaseCommitment and its tests for GCP commitment creation.
  • LeanerCloud/CUDly#1047: Modifies recommendation parsing, resource type handling, and commitment request construction.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes deriving GCP CUD commitment types from machine families, which is the primary change.
Linked Issues check ✅ Passed The changes derive commitment types from machine families, use SDK enums, and fail closed for unmapped or unsupported families as required by issue #1538.
Out of Scope Changes check ✅ Passed The implementation and regression tests support issue #1538 by improving type mapping, resource extraction, and refusal of unsafe purchases.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1538-gcp-cud-commitment-type

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Pushed f5e645c6f addressing the review findings. Summary (details in the updated PR description):

  • Input side (HIGH): extractResourceTypeFromOperations took the last / segment of the first operation with a non-empty Resource, so a commitment-only payload yielded the commitment name (cud-001), which the new derivation then rejected as machine family cud. The machine-type operation is now selected explicitly (path must contain /machineTypes/); when none exists the extractor errors naming what it looked for and what it found, ResourceType stays empty, and the purchase refuses with no machine type on the recommendation. The recommendation is not dropped -- six existing tests depend on operation-less payloads still converting, and a listed-but-unpurchasable recommendation is more visible than a silently discarded one.
  • Accelerator/graphics/storage (MEDIUM): buildInsertRequest sends only VCPU+MEMORY, but GCP requires ACCELERATOR in ACCELERATOR_OPTIMIZED*/GRAPHICS_OPTIMIZED* and LOCAL_SSD in STORAGE_OPTIMIZED_Z3. a2/a3/a4/g2/g4/z3 now take the same fail-loud refusal as m4/x4, naming the missing resource kind. The A3 sub-series map is removed.
  • f1/g1 (LOW): mapped to GENERAL_PURPOSE alongside custom-* so the mapping does not turn a previously-working purchase into a refusal.
  • GroupCommitments + vacuous test (LOW): literals converted to SDK enums; the resource-type test now asserts the literal wire strings "VCPU"/"MEMORY" instead of comparing SDK-derived to SDK-derived (it passed on the pre-fix code).
  • Lint figure corrected: the earlier "684" was the raw output line count, not findings. Re-measured at the CI pin (v2.10.1): 224 findings on providers/gcp/... before, 222 after, 50 in the computeengine package. Zero new; the two-finding drop is pre-existing godot findings on doc comments this PR rewrote.

Not done, deliberately: reading the commitment's own type from the recommender operation value would remove the family-mapping guess entirely, but carrying it to buildInsertRequest needs a new field on pkg/common.Recommendation, a separate module shared by all three providers. Worth a follow-up issue.

Pre-fix failure output for the new commitment-only regression is in the PR description.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Changed files ==='
git diff --name-only HEAD^ HEAD

echo '=== Diff stat ==='
git diff --stat HEAD^ HEAD

echo '=== Relevant symbol locations ==='
rg -n -C 3 \
  'func (extractResourceTypeFromOperations|machineTypeFromResourcePath|commitmentTypeForMachineType|buildInsertRequest|GroupCommitments)\b|extractResourceTypeFromOperations\s*\(|buildInsertRequest\s*\(' \
  providers/gcp 2>/dev/null || true

echo '=== Go file outlines in computeengine ==='
fd -e go . providers/gcp/services/computeengine -x ast-grep outline {} --items all --type function --match 'extractResourceTypeFromOperations|machineTypeFromResourcePath|commitmentTypeForMachineType|buildInsertRequest|GroupCommitments' --view expanded 2>/dev/null || true

echo '=== Updated patch ==='
git diff --find-renames HEAD^ HEAD -- providers/gcp

Length of output: 34365


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Commitment mapping and request construction ==='
sed -n '40,220p' providers/gcp/services/computeengine/client.go
sed -n '720,840p' providers/gcp/services/computeengine/client.go

echo '=== Recommendation conversion and path extraction ==='
sed -n '1040,1255p' providers/gcp/services/computeengine/client.go

echo '=== All extractor and selector call sites ==='
rg -n -C 5 \
  '\bextractResourceTypeFromRecommendation\s*\(|\bmachineTypeFromResourcePath\s*\(|\.ResourceType\b' \
  providers/gcp --glob '*.go'

echo '=== Recommender operation fixtures and resource paths ==='
rg -n -C 5 \
  'OperationGroups|machineTypes|commitments/|ResourceType:\s*"compute.googleapis.com/Commitment"' \
  providers/gcp --glob '*_test.go'

echo '=== Purchase mocks and relevant regression-test range ==='
rg -n -C 4 \
  'type MockCommitmentsService|func \(.*MockCommitmentsService.*Insert|insertReqs|lastInsertReq' \
  providers/gcp/services/computeengine --glob '*_test.go'
sed -n '1580,1970p' providers/gcp/services/computeengine/client_test.go

Length of output: 139120


@cristim I found one blocking issue in the follow-up:

  • High — the supported recommender payload is now universally unpurchasable. extractResourceTypeFromRecommendation (Line 1179) only accepts an operation resource containing /machineTypes/, but the new commitmentOnlyCUDRecommendation fixture (Line 1831) is documented as the expected commitment-recommender shape and has only /commitments/... resources. Conversion consequently leaves ResourceType empty (Line 1102), and buildInsertRequest refuses every such recommendation when it derives the commitment type (Line 755). The regression test currently codifies that refusal rather than preserving a viable purchase path.

    Refusing an ambiguous purchase is correct, but this changes the normal Compute CUD recommender flow from “wrong commitment” to “no commitment can be bought.” Please propagate the commitment operation’s authoritative type through common.Recommendation (or otherwise obtain an authoritative commitment type) before exposing these recommendations as purchasable. Add an end-to-end fixture for the real commitment-only payload that verifies the correct Commitment.Type reaches Insert.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
providers/gcp/services/computeengine/client.go (1)

59-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving the family mapping into its own file.

client.go is well over the 500-line limit, and this change adds about 120 more lines. The mapping tables and commitmentTypeForMachineType form a self-contained unit. Move them to a sibling file such as providers/gcp/services/computeengine/commitment_type.go in the same package. No behavior changes.

As per coding guidelines, "keep files under 500 lines".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/gcp/services/computeengine/client.go` around lines 59 - 145, Move
machineFamilyCommitmentType, familyRequiredResource, and
commitmentTypeForMachineType into a new sibling commitment_type.go file in the
same package. Preserve their existing declarations, comments, visibility, and
behavior without changing lookup or refusal logic; leave client.go focused on
client functionality and under the 500-line limit.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@providers/gcp/services/computeengine/client_test.go`:
- Around line 1932-1933: Update the assertion in the relevant test to check that
err.Error() does not contain the exact commitment name “cud-001” rather than the
generic substring “cud”, preserving the existing intent and failure message.

---

Nitpick comments:
In `@providers/gcp/services/computeengine/client.go`:
- Around line 59-145: Move machineFamilyCommitmentType, familyRequiredResource,
and commitmentTypeForMachineType into a new sibling commitment_type.go file in
the same package. Preserve their existing declarations, comments, visibility,
and behavior without changing lookup or refusal logic; leave client.go focused
on client functionality and under the 500-line limit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9646a39f-7312-46a1-8c50-511c6e64bd3e

📥 Commits

Reviewing files that changed from the base of the PR and between 887d51f and f5e645c.

📒 Files selected for processing (2)
  • providers/gcp/services/computeengine/client.go
  • providers/gcp/services/computeengine/client_test.go

Comment thread providers/gcp/services/computeengine/client_test.go Outdated
cristim added 3 commits August 3, 2026 15:21
buildInsertRequest pinned the GCP commitment Type to the string literal
"GENERAL_PURPOSE" for every purchase. GENERAL_PURPOSE is the N1-only
member of computepb.Commitment_Type, so what the customer requested and
what CUDly bought diverged on every non-N1 recommendation:

  requested n2-standard-16  ->  bought GENERAL_PURPOSE (N1)
  requested c3-highcpu-22   ->  bought GENERAL_PURPOSE (N1)
  requested m3-ultramem-32  ->  bought GENERAL_PURPOSE (N1)
  requested a3-megagpu-8g   ->  bought GENERAL_PURPOSE (N1)

Commitment.Type selects which machine series the discount applies to. An
N1 commitment discounts no N2/C3/M3/A3 instance, so the customer paid the
full 1- or 3-year commitment on top of undimmed on-demand charges while
the purchase was booked as realized savings. The recommended machine type
was already available on rec.ResourceType but was only read into the
Description string.

The Type is now derived from the machine-family segment of
rec.ResourceType and mapped onto the SDK's own computepb.Commitment_Type
constants, so a wrong member is a compile error rather than a wire-level
surprise. Legacy N1 "custom-<vCPUs>-<MB>" names, which carry no family
segment, map to GENERAL_PURPOSE; family-prefixed custom types resolve on
their own family.

An unmappable family returns an explicit error naming the machine type
and refuses the purchase. It does not fall back to GENERAL_PURPOSE:
on a money path a silent default spends real money on a commitment that
discounts nothing and cannot be cancelled, which is strictly worse than
refusing and surfacing the gap. This deliberately covers m4 and x4, whose
commitment types split into size buckets not derivable from the machine
type name, t2a, and any series GCP has not published a Commitment_Type
for.

The sibling ResourceCommitment VCPU/MEMORY wire values now also come from
computepb.ResourceCommitment enum constants rather than string literals,
keeping the issue #1022 "MEMORY_MB" regression compiler-checked.

Regression coverage asserts on the Type carried by the
InsertRegionCommitmentRequest handed to the SDK client, not on an
intermediate struct, and asserts that an unmappable family produces an
error with no Insert reaching GCP at all.

Closes #1538
Review follow-up on the #1538 commitment-type derivation.

Input side. extractResourceTypeFromOperations took the last "/" segment of
the FIRST operation carrying a non-empty Resource, with no check that the
path named a machine type. A CUD recommendation's operations reference the
commitment resource itself
("//compute.googleapis.com/projects/p/regions/r/commitments/cud-001"), so
on a commitment-only payload rec.ResourceType became the commitment NAME
"cud-001", which the new derivation would then reject as machine family
"cud". The purchase refusal was correct but the stated cause was invented.

The machine-type operation is now selected explicitly: only a resource path
containing "/machineTypes/" yields a machine type. When no such operation
exists the extractor returns an error naming the segment it looked for and
the resource paths it actually found. ResourceType is left EMPTY rather
than filled from an unrelated path segment, the recommendation stays
visible, and the purchase refuses with "no machine type on the
recommendation" instead of a fabricated family. The recommendation is not
dropped: six existing tests depend on operation-less payloads still
converting, and a listed-but-unpurchasable recommendation surfaces the
problem where a silently discarded one would not.

Accelerator, graphics and storage families. buildInsertRequest sends only
VCPU and MEMORY resources. GCP requires an ACCELERATOR resource in an
ACCELERATOR_OPTIMIZED*/GRAPHICS_OPTIMIZED* commitment and a LOCAL_SSD
resource in STORAGE_OPTIMIZED_Z3, so mapping a2/a3/a4/g2/g4/z3 newly
DECLARED types whose required resources were not sent. Best case GCP
rejects the insert; worst case it creates a commitment covering the cheap
vCPU and RAM and none of the GPUs or local SSDs that dominate the spend.
Those families now take the same "not derivable, do not guess" refusal as
m4/x4, with an error naming the missing resource kind.

f1-micro and g1-small are legacy shared-core members of the N1 lineage and
resolved correctly before this mapping existed; they are mapped to
GENERAL_PURPOSE alongside custom-* so the mapping does not turn a working
purchase into a refusal.

GroupCommitments now takes its VCPU/MEMORY values from the SDK enum too.
It has no non-test caller, but it is exactly where a future caller would
reintroduce the invalid "MEMORY_MB" spelling of issue #1022.

TestComputeEngineClient_PurchaseCommitment_ResourceTypesUseSDKEnums
compared an SDK-derived expected against an SDK-derived actual, so it
passed on the pre-fix literal-string code and pinned nothing. It now
asserts the literal wire strings "VCPU" and "MEMORY".

Reading the commitment's own type field out of the recommender operation
value would remove the family-mapping guess entirely, but carrying it to
buildInsertRequest requires a new field on pkg/common.Recommendation, a
separate module shared by all three providers. Left out of this fix
deliberately.

Refs #1538
CodeRabbit flagged the commitment-name guard in
TestPurchaseCommitment_CommitmentOnlyRecommendationRefuses as asserting
on the generic substring "cud" instead of the actual commitment name
"cud-001", which a future error-text change containing the lowercase
word "cud" could pass for the wrong reason. Assert on the exact name.

Also drop f1 and g1 (f1-micro, g1-small) from
machineFamilyCommitmentType. The prior comment justified mapping them
to GENERAL_PURPOSE by claiming they "resolved correctly before this
mapping existed" -- but GCP excludes shared-core machine types from
committed use discounts entirely, so no commitment product exists for
either family. Mapping them to GENERAL_PURPOSE would buy a real N1
commitment that discounts nothing, reintroducing the exact issue #1538
failure shape (pay the full multi-year commitment, discount nothing)
inside the PR meant to eliminate it, and violating this file's own
"not derivable, do not guess" principle. f1/g1 now take a dedicated
fail-loud path (familyNoCommitmentAvailable) with an error naming the
reason, and the two test cases that pinned GENERAL_PURPOSE for them
now assert refusal instead.
@cristim
cristim force-pushed the fix/1538-gcp-cud-commitment-type branch from a725cc7 to 928baaf Compare August 3, 2026 13:21
@cristim

cristim commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 30 minutes.

@cristim
cristim merged commit ef73202 into main Aug 3, 2026
19 checks passed
@cristim
cristim deleted the fix/1538-gcp-cud-commitment-type branch August 3, 2026 13:56
@cristim

cristim commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Adversarial review record (merged)

Independent reviewer, distinct from the author. Recording it here because CodeRabbit's status on this PR read success while its description said "Review rate limited" — it did not genuinely review this money-path PR.

No silent fallback — the thing that most mattered. commitmentTypeForMachineType has no value-returning default: arm. Three fail-loud paths (empty machine type, family requiring a resource this client does not send, unmapped family), each returning Commitment_UNDEFINED_TYPE plus an error, with the code explicitly saying "refusing to purchase rather than defaulting to GENERAL_PURPOSE". The caller propagates and PurchaseCommitment returns before ever reaching svc.Insert.

Exhaustive against the real SDK, not from memory. Enumerated all 39 Commitment_Type members from the module cache; every one is mapped or deliberately refused. Two mapping claims independently confirmed against the SDK's own enum doc (MEMORY_OPTIMIZED = M1+M2, GENERAL_PURPOSE = N1).

Threaded to the wire, not computed-and-dropped — traced into the real svc.Insert retry loop, single construction site, no dry-run branch bypassing it. This is the shape another PR in the same batch failed.

The regression test genuinely bites. The pre-fix wiring was rebuilt in isolation and the tests FAIL with expected "COMPUTE_OPTIMIZED_C3" actual "GENERAL_PURPOSE" and expected "MEMORY_OPTIMIZED_M3" actual "GENERAL_PURPOSE", asserting on the actual InsertRegionCommitmentRequest handed to the mock. Controls n1-standard-4 and custom-4-8192 pass pre-fix, so the test discriminates rather than failing indiscriminately, which is exactly why the bug was invisible: N1 was always correct.

It also closes an old hole: sibling "VCPU"/"MEMORY" string literals converted to SDK constants, closing the "MEMORY_MB" reintroduction class.

Finding fixed before merge

f1/g1 were mapped to Commitment_GENERAL_PURPOSE with a comment claiming this preserved purchases that "resolved correctly before this mapping existed". That justification was false — GCP sells no CUD for shared-core machine types at all, so the mapping would have bought a real N1 commitment discounting nothing: issue #1538's exact failure shape, reintroduced inside its own fix, with tests pinning the wrong behaviour. Moved to a dedicated fail-loud arm, the two test cases flipped to expect refusal, and the false comment removed.

Verification of the lint claim

golangci-lint at the CI-pinned v2.10.1 (not the newer local build, which has a different bundled ruleset): 222 issues before and after, identical per-rule breakdown — zero new findings, and no gate-suppression used.

Could not verify

c3-*-lssd and other local-SSD variants: c3 maps to COMPUTE_OPTIMIZED_C3 and the client sends only VCPU+MEMORY. Whether GCP requires a LOCAL_SSD resource for those variants was not confirmed. Low severity — the Type is correct for vCPU/RAM either way. Worth a follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p0 Drop everything; same-day fix severity/critical Major harm when it happens triaged Item has been triaged type/bug Defect urgency/now Drop other things

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(gcp): every CUD is purchased as Type GENERAL_PURPOSE regardless of the recommended machine family

1 participant