Skip to content

feat(resourcediscovery): Azure Resource Graph cost-field projection across all supported types (#323) - #327

Merged
NitinKumar004 merged 9 commits into
developmentfrom
feat/323-arg-cost-fields
Aug 5, 2026
Merged

feat(resourcediscovery): Azure Resource Graph cost-field projection across all supported types (#323)#327
NitinKumar004 merged 9 commits into
developmentfrom
feat/323-arg-cost-fields

Conversation

@NitinKumar004

Copy link
Copy Markdown
Collaborator

Summary

Extends the Azure Resource Graph emulation (built in #315/#316) to project the properties and sku cost fields a real armresourcegraph discoverer prices on, across every supported Azure resource type. Addresses #323.

A downstream project drives its real Azure discoverer against cloudemu's Resource Graph endpoint to E2E-verify cost calculation. Previously only disk size, VM SKU, and SQL-MI storage flowed through. This PR closes the field gap so disk IOPS/throughput, VM/VMSS Spot + Hybrid Benefit, SQL zone-redundancy, AKS Spot node pools, storage redundancy, Cosmos serverless, App Service plan tier, and more all round-trip over the real SDK.

What's projected now

Type Fields
microsoft.compute/virtualmachines properties.priority (Spot), licenseType, osType, sku.name, zones
microsoft.compute/disks properties.diskIOPSReadWrite, diskMBpsReadWrite, diskSizeGB, tier, sku.name/sku.tier
microsoft.compute/virtualmachinescalesets (new) sku.name/sku.capacity, nested properties.virtualMachineProfile.{priority,licenseType,storageProfile.osDisk.osType}
microsoft.network/publicipaddresses sku.name (Basic/Standard), properties.publicIPAllocationMethod
microsoft.network/virtualnetworks / subnets properties.addressSpace.addressPrefixes / addressPrefix
microsoft.storage/storageaccounts sku.name (redundancy), kind, properties.accessTier
microsoft.documentdb/databaseaccounts kind, properties.databaseAccountOfferType, capabilities (serverless), enableFreeTier
microsoft.sql/managedinstances properties.storageSizeInGB, storageAccountType
microsoft.sql/servers/databases (new) sku.name, properties.currentSku, zoneRedundant
microsoft.dbformysql/dbforpostgresql/flexibleservers sku.name/sku.tier, properties.version, nested storage.storageSizeGB + highAvailability.mode
microsoft.sql/servers properties.version
microsoft.containerservice/managedclusters sku.tier, properties.powerState.code, kubernetesVersion
.../managedclusters/agentpools sku.name (vmSize), properties.scaleSetPriority (Spot), count, mode/osType
microsoft.databricks/workspaces sku.name/sku.tier, properties.workspaceId/provisioningState
microsoft.web/serverfarms (new) sku.name/sku.tier/sku.capacity, kind

How it's built (per the existing architecture)

  • Foundation: added SKUTier/SKUCapacity slots to the generic Attributes/Resource set so resourceToWire can render sku.tier/sku.capacity; fixed two type-map correctness bugs (NetworkInterface and ElasticIP emitted the wrong Azure type strings).
  • Additive field seeding: widened the portable driver configs where the field can be varied (VolumeConfig.IOPS/Throughput/Tier, InstanceConfig.OSType/Priority/LicenseType/Zones, ManagedInstanceConfig.StorageAccountType, ElasticIPConfig.SKU/AllocationMethod, AKS Tier/ScaleSetPriority), and the walkers/adapters project them.
  • Optional type-asserted capabilities (the NetworkInterfaces pattern) for enrichment that doesn't fit the portable config: BucketAttributes (storage account sku/kind/accessTier) and TableAttributes (Cosmos account kind/offer/capabilities/free-tier). S3/GCS/DynamoDB don't implement them and contribute nothing.
  • New provider-projected discovery capabilities (the relational-DB / Kubernetes walker pattern) for the net-new resources: ScaleSets (VMSS), AppServicePlans (serverfarms), and the Databases CRUD capability implemented on Azure SQL for logical databases.
  • The NodeGroups []string cluster projection became []DiscoveredNodeGroup{Name, Attrs} so agent pools can carry per-pool attributes; the EKS/GKE/AKS adapters were all updated (mirror rule).

All additions are additive and nil-safe — AWS/GCP engines skip the Azure-only walkers, and unimplemented optional capabilities are silently absent.

Tests

  • Real-SDK ARG round-trips (server/azure/resourcegraph/arg_cost_*_test.go) drive the live armresourcegraph client against an httptest server and assert every projected properties/sku field per type.
  • Provider-unit tests for each new mock field / capability (VMSS, App Service plan, SQL databases, storage/Cosmos attributes, AKS agent-pool priority, public-IP sku).
  • go build ./..., go vet ./..., gofmt, full go test ./... (225 packages) and golangci-lint on the changed files are all green.

Docs

docs/services.md §19 gains the full ARG cost-field projection table and a note on the capability patterns used.

…oss all supported types

Emit the properties/sku a real armresourcegraph discoverer prices on — for
VM/disk/VMSS, public IP, VNet/subnet, storage account, Cosmos DB, SQL
MI/database/server + MySQL/Postgres flex, AKS cluster/agent-pool, Databricks,
and App Service plans. Adds SKUTier/SKUCapacity slots to the generic
attribute set and fixes two type-map bugs (NIC, public IP emitted wrong
Azure types).

New driver capabilities: Databases (Azure SQL logical DBs), ScaleSets (VMSS),
AppServicePlans (serverfarms), and the optional BucketAttributes/TableAttributes
capabilities that enrich storage accounts and Cosmos DB. All additive and
nil-safe; AWS/GCP walks are unaffected. Backed by real-SDK ARG round-trip
tests plus provider-unit tests.

Extends #315/#316; addresses #323.
Merge the four parallel-authored arg_cost_*_test.go files into a single
arg_cost_fields_test.go with one shared helper set; assertions unchanged.
…te->discover

Extend the Azure SDK-compat handlers to parse the cost fields off the ARM
create body onto the driver config (they were accepted-and-dropped), and add
create routes for resource types that had none:

- virtualmachines: VM priority/licenseType/osType/zones; new VMSS handler
- azuresql: MI storageAccountType; SQL Database create/get/list -> Databases
- aks: cluster sku.tier; agent-pool scaleSetPriority
- functions: new serverfarms (App Service plan) handler
- storageaccount, cosmosaccount: new Microsoft.Storage/DocumentDB account handlers

Real-SDK create->get round-trip tests per package.
Complete the Microsoft.Storage/storageAccounts and Microsoft.DocumentDB/
databaseAccounts create+get routes so their cost fields (sku/kind/accessTier,
capabilities/offerType/enableFreeTier) survive create->discover.
…bscription

- disks: parse diskIOPSReadWrite/diskMBpsReadWrite/tier onto VolumeConfig; echo sku.tier.
- resourcegraph: stamp the configured subscriptionId on every row instead of
  parsing inconsistent placeholders from each mock's ARN, so subscription-scoped
  ARG queries return the emulator's resources.
Wire Microsoft.Network/publicIPAddresses to AllocateAddress with sku +
allocation method, so public IPs are creatable over the real SDK and their
cost fields surface in Resource Graph.
… match real ARG

VMs projected osType as a flat properties.osType; real Azure Resource Graph
(and the VMSS projection) nest it under properties.storageProfile.osDisk.osType,
so a real discoverer reading that path found it empty. Only Azure VMs set OSType
(AWS/GCP mocks leave it blank), so no cross-provider shape leaks.

@thzgajendra thzgajendra left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review — ARG cost-field projection (in-depth, multi-dimension pass)

Reviewed across correctness, nil-safety, type design, tests, comments/docs, simplification, and architecture-consistency, plus a manual lead pass on the ARG core.

Overall: strong and well-architected. The KQL parser + generic resourceToWire rendering (no per-type branching) are clean, the change is genuinely additive, and both advertised bug fixes are real — the base branch fell through to invalid networking/networkinterface / networking/elasticip type strings; the new portableToAzureTypeMap entries are correct Azure ARG types. The three claimed patterns (NetworkInterfaces optional-capability, relational-DB/K8s walker, and the NodeGroups mirror rule) are applied consistently, and AWS/GCP output is verifiably unaffected (NodeGroupsFromNames shim + zero-value additive fields).

No confirmed correctness blocker. Findings by severity:

Should address

  • Test gap (silent-regression risk): microsoft.sql/serversproperties.version is projected (providers/azure/azure.go:254) but never asserted at the ARG level — TestARGCostFields_SQLDatabase only queries the .../databases row. Add a microsoft.sql/servers round-trip asserting version. It's also missing from the docs/services.md §19 table.
  • Masked test assertion (arg_cost_fields_test.go:345) — see inline; passes even if the projection breaks.
  • Doc error: §19 lists VM osType at properties.osType, but the code nests it at properties.storageProfile.osDisk.osType (which matches real ARG). Fix the documented path. §19 also under-documents the SQL-MI row (missing sku.name, vCores, tier, licenseType).

Consider

  • nonEmptyProps bool / nested-map handling — see inline on azure.go.
  • Swallowed BucketAttributes / TableAttributes errors — see inline on walkers.go:316.
  • flexTier empty-tier for unlisted families — see inline.
  • App Service plans bucketed under ServiceServerless — see inline.
  • Postgres-flex tests assert fewer fields than MySQL-flex (missing sku.tier, version) — worth symmetry.

Nits

  • Stringly-typed OSType / Priority driver configs could be named enum types with consts (small closed domains).
  • No compile-time var _ BucketAttributes = (*Mock)(nil) assertions for the optional capabilities — a typo'd method signature would fail the type-assert silently with no build error.

Nice PR — the field-flow (config → walker → adapter → wire → ARG projection) connects end-to-end for the types I traced, and the units/JSON paths are correct throughout (GB/IOPS/MBps, sku vs properties).

Comment thread providers/azure/azure.go
SKU: db.SKUName,
SKUTier: db.SKUTier,
Properties: nonEmptyProps(map[string]any{
"zoneRedundant": db.ZoneRedundant,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nonEmptyProps (line 442) only strips empty string and intbool and nested map[string]any fall through its type switch untouched. So zoneRedundant: false and the currentSku map are emitted for every SQL database, including ones created without zone-redundancy or an explicit SKU.

Azure SQL genuinely surfaces properties.zoneRedundant as a bool, so emitting false may actually be faithful to what the real armresourcegraph discoverer returns — in which case the inconsistency is the other way (pruning empty strings/ints that real ARG might include). Either way the helper's doc says "drops zero-valued entries" but its behavior only does so for two of the types it receives.

Worth a quick check against the live discoverer (fidelity is the point of this PR): if defaults should be omitted, add case bool { if !val { delete } } + empty-map handling; if ARG-faithfulness is intended, the string/int pruning of real properties is what needs a second look.

Comment thread services/resourcediscovery/walkers.go Outdated
// Optional capability: providers whose buckets carry storage-account
// attributes (Azure) project SKU/kind/access-tier for cost discovery.
if attrer, ok := e.drivers.Storage.(storagedriver.BucketAttributes); ok {
if a, aErr := attrer.BucketAttributes(ctx, b.Name); aErr == nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Swallowed error: if BucketAttributes ever returns a non-nil aErr, the sku/kind/accessTier are silently dropped and the storage account is emitted with no priceable SKU — exactly the "cost field silently absent" failure this PR is closing. Today's impl can't error, so it's latent, but the invariant is load-bearing and undiscoverable if a future/alternate impl breaks it. Suggest logging aErr rather than discarding it. Same pattern at line 362 for TableAttributes.

sku := rowSKU(t, row)
assert.Equal(t, "premium", sku["name"])

if tier, ok := sku["tier"]; ok {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This assertion is masked: if Databricks sku.tier projection regresses to absent, ok is false and the inner assert is skipped — the test still passes green. Since the input sets SKUTier: "premium", make it unconditional so a broken projection actually fails:

assert.Equal(t, "premium", rowSKU(t, row)["tier"])

Comment thread providers/azure/azure.go
// GeneralPurpose / MemoryOptimized) from the SKU name, which encodes it as a
// prefix in both the current ("Standard_B1ms") and legacy ("B_Gen5_1") naming.
// Empty when the name doesn't match a known family.
func flexTier(skuName string) string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

flexTier returns "" for SKU families outside the listed prefixes (e.g. Standard_F* compute-optimized), so sku.tier is silently dropped for those servers and no test catches it. Reasonable as best-effort (a pricing consumer can fall back to sku.name), but worth a short comment noting the mapping is known-incomplete, or a sensible default, so a maintainer doesn't read the empty tier as a bug later.

Comment thread services/resourcediscovery/walkers.go Outdated
}

r := Resource{
Provider: e.provider, Service: ServiceServerless, Type: TypeAppServicePlan,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

App Service plans are bucketed under ServiceServerless (same discriminator as Functions). ARG type-queries still resolve correctly because Type (TypeAppServicePlanmicrosoft.web/serverfarms) disambiguates, so this isn't a functional bug. But serverfarms aren't serverless — following the ServiceRelationalDB precedent, a dedicated service constant for net-new families would be semantically cleaner if anything ever groups by Service.

- nonEmptyProps prunes empty/nested-empty maps and nil (keeps faithful bools);
  doc aligned to behavior.
- Propagate BucketAttributes/TableAttributes errors instead of swallowing them.
- Dedicated ServiceAppService discriminator for serverfarms (wire type unchanged).
- Compile-time capability assertions for the optional discovery interfaces.
- flexTier known-incomplete comment; unmask Databricks sku.tier assertion;
  add SQL-server version + Postgres-flex symmetry tests; §19 doc corrections.
@NitinKumar004

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough pass — all findings addressed in add1050. Gate: go build ./..., go vet, gofmt, and go test ./... all clean.

Should address

  • Masked Databricks assertion — now unconditional (assert.Equal(t, "premium", rowSKU(t, row)["tier"])), so a dropped sku.tier projection fails.
  • microsoft.sql/servers version test gap — added TestARGCostFields_SQLServer (create → ARG query → assert properties.version), plus the §19 row.
  • §19 doc fixes — VM osType corrected to properties.storageProfile.osDisk.osType; SQL-MI row expanded. Note: SQL-MI tier surfaces at properties.tier (not sku.tier) — the mock never sets Attrs.SKUTier for that type, so the doc + test reflect the actual path.

Consider

  • nonEmptyProps now prunes empty/nested-empty maps and nil (kills the empty currentSku map); bools including false are intentionally kept as faithful to real ARG, and the doc comment now states exactly that.
  • Swallowed BucketAttributes/TableAttributes errors — the walk* funcs return error, so both sites now propagate rather than discard.
  • flexTier — added a comment that the prefix list is known-incomplete and "" is an intentional best-effort fallback (sku.name covers the consumer).
  • App Service plans — moved to a dedicated ServiceAppService discriminator (following the ServiceRelationalDB precedent); the emitted wire type is unchanged (microsoft.web/serverfarms).
  • Postgres-flex tests brought to parity with MySQL-flex (sku.tier, version).

Nits

  • Added compile-time var _ driver.BucketAttributes/TableAttributes = (*Mock)(nil) assertions so a signature typo fails the build instead of the runtime type-assert.

@thzgajendra thzgajendra left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review — fixes in add1050fd verified ✅

Re-reviewed the fix(resourcediscovery): address ARG cost-field review findings commit against my prior review. Every finding addressed correctly. Built + go vet + tested the affected packages green locally (resourcegraph, resourcediscovery, blobstorage, cosmosdb).

Finding Status
Swallowed BucketAttributes/TableAttributes errors (walkers.go 316/362) ✅ Now propagate via fmt.Errorf, with a "load-bearing error" comment
Masked Databricks sku.tier assertion ✅ Now unconditional
microsoft.sql/serversversion untested ✅ New TestARGCostFields_SQLServer + §19 doc row
Postgres-flex test asymmetry ✅ New TestARGCostFields_PostgresFlex (sku.tier/version/storage/HA)
Doc: VM osType path + SQL-MI under-documented ✅ §19 corrected; MI sku.name/vCores/tier/licenseType now also asserted in the test
nonEmptyProps bool / nested-map ✅ Recursive-map + nil pruning added; false bools intentionally preserved (faithful to real ARG) and now documented — the right call
App Service plans bucketed under ServiceServerless ✅ Dedicated ServiceAppService / portableAppService applied across kql.go + handler.go + walkers.go
flexTier empty tier for unlisted families (nit) ✅ Documented as intentional best-effort
Compile-time capability assertions (nit) var _ BucketAttributes/TableAttributes = (*Mock)(nil) added to both mocks

The one change with regression risk — renaming the App Service plan service discriminator — is sound: walker dispatch is driver-gated with post-filter q.matches, the query-parse and emit sides both use appservice consistently, and TestARGCostFields_AppServicePlan (microsoft.web/serverfarms round-trip) passes.

No new issues found. Clean, thorough turnaround — LGTM from my side (leaving the merge call to you).

@NitinKumar004
NitinKumar004 merged commit 4bd1336 into development Aug 5, 2026
11 checks passed
@NitinKumar004
NitinKumar004 deleted the feat/323-arg-cost-fields branch August 5, 2026 20:28
@NitinKumar004 NitinKumar004 mentioned this pull request Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants