From 8258908b038d20d4a62b0504ba7cf84868fee365 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Mon, 9 Sep 2024 15:13:46 +0200 Subject: [PATCH] add subresource translation layer for v1 API backwards compatibility (#550) We all forgot the note that I left about this in the previous PR. :) --- internal/api/fixtures/start-data-small.sql | 35 +++ internal/api/translation_test.go | 256 +++++++++++++++++++++ internal/core/resource_behavior.go | 4 + internal/core/translation_rule.go | 201 ++++++++++++++++ internal/liquids/cinder/liquid.go | 3 - internal/liquids/cinder/usage.go | 10 +- internal/liquids/manila/liquid.go | 3 - internal/reports/cluster.go | 23 +- internal/reports/project.go | 8 + 9 files changed, 526 insertions(+), 17 deletions(-) create mode 100644 internal/api/fixtures/start-data-small.sql create mode 100644 internal/api/translation_test.go create mode 100644 internal/core/translation_rule.go diff --git a/internal/api/fixtures/start-data-small.sql b/internal/api/fixtures/start-data-small.sql new file mode 100644 index 000000000..c5d30550b --- /dev/null +++ b/internal/api/fixtures/start-data-small.sql @@ -0,0 +1,35 @@ +-- This start-data contains exactly one project and one service, with all capacity/quota/usage values at 0. +-- It can be used as a base to set up isolated tests for individual reporting features. + +CREATE OR REPLACE FUNCTION unix(i integer) RETURNS timestamp AS $$ SELECT TO_TIMESTAMP(i) AT TIME ZONE 'Etc/UTC' $$ LANGUAGE SQL; + +INSERT INTO cluster_capacitors (capacitor_id, scraped_at, next_scrape_at) VALUES ('first', UNIX(1000), UNIX(2000)); + +INSERT INTO cluster_services (id, type) VALUES (1, 'first'); + +INSERT INTO cluster_resources (id, service_id, name, capacitor_id) VALUES (1, 1, 'things', 'first'); +INSERT INTO cluster_resources (id, service_id, name, capacitor_id) VALUES (2, 1, 'capacity', 'first'); + +-- "capacity" is modeled as AZ-aware, "things" is not +INSERT INTO cluster_az_resources (id, resource_id, az, raw_capacity, usage, subcapacities) VALUES (1, 1, 'any', 0, 0, ''); +INSERT INTO cluster_az_resources (id, resource_id, az, raw_capacity, usage, subcapacities) VALUES (2, 2, 'az-one', 0, 0, ''); +INSERT INTO cluster_az_resources (id, resource_id, az, raw_capacity, usage, subcapacities) VALUES (3, 2, 'az-two', 0, 0, ''); + +INSERT INTO domains (id, name, uuid) VALUES (1, 'domainone', 'uuid-for-domainone'); + +INSERT INTO projects (id, domain_id, name, uuid, parent_uuid) VALUES (1, 1, 'projectone', 'uuid-for-projectone', 'uuid-for-domainone'); + +INSERT INTO project_services (id, project_id, type, scraped_at, rates_scraped_at, checked_at, rates_checked_at) VALUES (1, 1, 'first', UNIX(11), UNIX(12), UNIX(11), UNIX(12)); + +INSERT INTO project_resources (id, service_id, name, quota, backend_quota) VALUES (1, 1, 'things', 0, 0); +INSERT INTO project_resources (id, service_id, name, quota, backend_quota) VALUES (2, 1, 'capacity', 0, 0); +INSERT INTO project_resources (id, service_id, name, quota, backend_quota) VALUES (3, 1, 'capacity_portion', NULL, NULL); + +-- "capacity" and "capacity_portion" are modeled as AZ-aware, "things" is not +INSERT INTO project_az_resources (id, resource_id, az, quota, usage, physical_usage, subresources) VALUES (1, 1, 'any', 0, 0, NULL, ''); +INSERT INTO project_az_resources (id, resource_id, az, quota, usage, physical_usage, subresources) VALUES (2, 2, 'any', 0, 0, NULL, ''); +INSERT INTO project_az_resources (id, resource_id, az, quota, usage, physical_usage, subresources) VALUES (3, 2, 'az-one', 0, 0, NULL, ''); +INSERT INTO project_az_resources (id, resource_id, az, quota, usage, physical_usage, subresources) VALUES (4, 2, 'az-two', 0, 0, NULL, ''); +INSERT INTO project_az_resources (id, resource_id, az, quota, usage, physical_usage, subresources) VALUES (5, 3, 'any', NULL, 0, NULL, ''); +INSERT INTO project_az_resources (id, resource_id, az, quota, usage, physical_usage, subresources) VALUES (6, 3, 'az-one', NULL, 0, NULL, ''); +INSERT INTO project_az_resources (id, resource_id, az, quota, usage, physical_usage, subresources) VALUES (7, 3, 'az-two', NULL, 0, NULL, ''); diff --git a/internal/api/translation_test.go b/internal/api/translation_test.go new file mode 100644 index 000000000..683b1bef7 --- /dev/null +++ b/internal/api/translation_test.go @@ -0,0 +1,256 @@ +/******************************************************************************* +* +* Copyright 2024 SAP SE +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You should have received a copy of the License along with this +* program. If not, you may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*******************************************************************************/ + +package api + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/sapcc/go-bits/assert" + "github.com/sapcc/go-bits/must" + + "github.com/sapcc/limes/internal/core" + "github.com/sapcc/limes/internal/test" +) + +const ( + testSmallConfigYAML = ` + availability_zones: [ az-one, az-two ] + discovery: + method: --test-static + services: + - service_type: first + type: --test-generic + ` +) + +func TestTranslateManilaSubcapacities(t *testing.T) { + s := test.NewSetup(t, + test.WithDBFixtureFile("fixtures/start-data-small.sql"), + test.WithConfig(testSmallConfigYAML), + test.WithAPIHandler(NewV1API), + ) + s.Cluster.Config.ResourceBehaviors = []core.ResourceBehavior{{ + FullResourceNameRx: "first/capacity", + TranslationRuleInV1API: must.Return(core.NewTranslationRule("manila")), + }} + + // this is what liquid-manila (or liquid-cinder) writes into the DB + newFormatSubcapacities := []assert.JSONObject{ + { + "name": "pool1", + "capacity": 520, + "usage": 520, + "attributes": assert.JSONObject{ + "exclusion_reason": "hardware_state = in_decom", + "real_capacity": 15360, + }, + }, + { + "name": "pool2", + "capacity": 15360, + "usage": 10, + "attributes": assert.JSONObject{}, + }, + } + _, err := s.DB.Exec(`UPDATE cluster_az_resources SET subcapacities = $1 WHERE id = 2`, + string(must.Return(json.Marshal(newFormatSubcapacities))), + ) + if err != nil { + t.Fatal(err.Error()) + } + + // this is what we expect to be reported on the API + oldFormatSubcapacities := []assert.JSONObject{ + { + "pool_name": "pool1", + "az": "az-one", + "capacity_gib": 15360, + "usage_gib": 520, + "exclusion_reason": "hardware_state = in_decom", + }, + { + "pool_name": "pool2", + "az": "az-one", + "capacity_gib": 15360, + "usage_gib": 10, + "exclusion_reason": "", + }, + } + + assert.HTTPRequest{ + Method: "GET", + Path: "/v1/clusters/current?resource=capacity&detail", + Header: map[string]string{"X-Limes-V2-API-Preview": "per-az"}, + ExpectStatus: http.StatusOK, + ExpectBody: assert.JSONObject{ + "cluster": assert.JSONObject{ + "id": "current", + "min_scraped_at": 1000, + "max_scraped_at": 1000, + "services": []assert.JSONObject{{ + "type": "first", + "area": "first", + "min_scraped_at": 11, + "max_scraped_at": 11, + "resources": []assert.JSONObject{{ + "name": "capacity", + "unit": "B", + "capacity": 0, + "domains_quota": 0, + "usage": 0, + "per_availability_zone": []assert.JSONObject{ + {"capacity": 0, "name": "az-one"}, + {"capacity": 0, "name": "az-two"}, + }, + "per_az": assert.JSONObject{ + "az-one": assert.JSONObject{"capacity": 0, "usage": 0, "subcapacities": oldFormatSubcapacities}, + "az-two": assert.JSONObject{"capacity": 0, "usage": 0}, + }, + "quota_distribution_model": "autogrow", + "subcapacities": oldFormatSubcapacities, + }}, + }}, + }, + }, + }.Check(t, s.Handler) +} + +func TestTranslateCinderVolumeSubresources(t *testing.T) { + subresourcesInLiquidFormat := []assert.JSONObject{ + { + "id": "6dfbbce3-078d-4c64-8f88-8145b8d44183", + "name": "volume1", + "attributes": assert.JSONObject{ + "size_gib": 21, + "status": "error", + }, + }, + { + "id": "a33bae62-e14e-47a6-a019-5faf89c20dc7", + "name": "volume2", + "attributes": assert.JSONObject{ + "size_gib": 1, + "status": "available", + }, + }, + } + + subresourcesInLegacyFormat := []assert.JSONObject{ + { + "id": "6dfbbce3-078d-4c64-8f88-8145b8d44183", + "name": "volume1", + "status": "error", + "size": assert.JSONObject{"value": 21, "unit": "GiB"}, + "availability_zone": "az-one", + }, + { + "id": "a33bae62-e14e-47a6-a019-5faf89c20dc7", + "name": "volume2", + "status": "available", + "size": assert.JSONObject{"value": 1, "unit": "GiB"}, + "availability_zone": "az-one", + }, + } + + testSubresourceTranslation(t, "cinder-volumes", subresourcesInLiquidFormat, subresourcesInLegacyFormat) +} + +func TestTranslateCinderSnapshotSubresources(t *testing.T) { + subresourcesInLiquidFormat := []assert.JSONObject{ + { + "id": "260da0ee-4816-48af-8784-1717cb76c0cd", + "name": "snapshot1-of-volume2", + "attributes": assert.JSONObject{ + "size_gib": 1, + "status": "available", + "volume_id": "a33bae62-e14e-47a6-a019-5faf89c20dc7", + }, + }, + } + + subresourcesInLegacyFormat := []assert.JSONObject{ + { + "id": "260da0ee-4816-48af-8784-1717cb76c0cd", + "name": "snapshot1-of-volume2", + "status": "available", + "size": assert.JSONObject{"value": 1, "unit": "GiB"}, + "volume_id": "a33bae62-e14e-47a6-a019-5faf89c20dc7", + }, + } + + testSubresourceTranslation(t, "cinder-snapshots", subresourcesInLiquidFormat, subresourcesInLegacyFormat) +} + +func testSubresourceTranslation(t *testing.T, ruleID string, subresourcesInLiquidFormat, subresourcesInLegacyFormat []assert.JSONObject) { + s := test.NewSetup(t, + test.WithDBFixtureFile("fixtures/start-data-small.sql"), + test.WithConfig(testSmallConfigYAML), + test.WithAPIHandler(NewV1API), + ) + s.Cluster.Config.ResourceBehaviors = []core.ResourceBehavior{{ + FullResourceNameRx: "first/capacity", + TranslationRuleInV1API: must.Return(core.NewTranslationRule(ruleID)), + }} + + _, err := s.DB.Exec(`UPDATE project_az_resources SET subresources = $1 WHERE id = 3`, + string(must.Return(json.Marshal(subresourcesInLiquidFormat))), + ) + if err != nil { + t.Fatal(err.Error()) + } + + assert.HTTPRequest{ + Method: "GET", + Path: "/v1/domains/uuid-for-domainone/projects/uuid-for-projectone?resource=capacity&detail", + Header: map[string]string{"X-Limes-V2-API-Preview": "per-az"}, + ExpectStatus: http.StatusOK, + ExpectBody: assert.JSONObject{ + "project": assert.JSONObject{ + "id": "uuid-for-projectone", + "name": "projectone", + "parent_id": "uuid-for-domainone", + "services": []assert.JSONObject{ + { + "type": "first", + "area": "first", + "scraped_at": 11, + "resources": []assert.JSONObject{ + { + "name": "capacity", + "unit": "B", + "quota": 0, + "usable_quota": 0, + "usage": 0, + "per_az": assert.JSONObject{ + "az-one": assert.JSONObject{"quota": 0, "usage": 0, "subresources": subresourcesInLegacyFormat}, + "az-two": assert.JSONObject{"quota": 0, "usage": 0}, + }, + "quota_distribution_model": "autogrow", + "subresources": subresourcesInLegacyFormat, + }, + }, + }, + }, + }, + }, + }.Check(t, s.Handler) +} diff --git a/internal/core/resource_behavior.go b/internal/core/resource_behavior.go index 9d17b2bae..4ad6eb51f 100644 --- a/internal/core/resource_behavior.go +++ b/internal/core/resource_behavior.go @@ -42,6 +42,7 @@ type ResourceBehavior struct { CommitmentUntilPercent *float64 `yaml:"commitment_until_percent"` CommitmentConversion CommitmentConversion `yaml:"commitment_conversion"` IdentityInV1API ResourceRef `yaml:"identity_in_v1_api"` + TranslationRuleInV1API TranslationRule `yaml:"translation_rule_in_v1_api"` Category string `yaml:"category"` } @@ -115,6 +116,9 @@ func (b *ResourceBehavior) Merge(other ResourceBehavior) { if other.IdentityInV1API != (ResourceRef{}) { b.IdentityInV1API = other.IdentityInV1API } + if !other.TranslationRuleInV1API.IsEmpty() { + b.TranslationRuleInV1API = other.TranslationRuleInV1API + } if other.Category != "" { b.Category = other.Category } diff --git a/internal/core/translation_rule.go b/internal/core/translation_rule.go new file mode 100644 index 000000000..1a48d0209 --- /dev/null +++ b/internal/core/translation_rule.go @@ -0,0 +1,201 @@ +/******************************************************************************* +* +* Copyright 2024 SAP SE +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You should have received a copy of the License along with this +* program. If not, you may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*******************************************************************************/ + +package core + +import ( + "encoding/json" + "fmt" + + "github.com/sapcc/go-api-declarations/limes" +) + +// TranslationRule appears in type ResourceBehavior. +// +// It provides a backwards compatibility mechanism to format subcapacities or +// subresources provided by a LIQUID implementation back into the old format +// that was generated by the respective CapacityPlugin or QuotaPlugin. +type TranslationRule struct { + // If not nil, reports need to pass all `subcapacities` strings through this handler. + TranslateSubcapacities func(string, limes.AvailabilityZone) (string, error) + // If not nil, reports need to pass all `subresources` strings through this handler. + TranslateSubresources func(string, limes.AvailabilityZone) (string, error) +} + +// NewTranslationRule returns the TranslationRule for the given ID, or an error if the ID is unknown. +func NewTranslationRule(id string) (TranslationRule, error) { + switch id { + case "": + // the default is to not do any translation + return TranslationRule{nil, nil}, nil + case "cinder-volumes": + return TranslationRule{translateCinderOrManilaSubcapacities, translateCinderVolumeSubresources}, nil + case "cinder-snapshots": + return TranslationRule{translateCinderOrManilaSubcapacities, translateCinderSnapshotSubresources}, nil + case "manila": + return TranslationRule{translateCinderOrManilaSubcapacities, nil}, nil + default: + return TranslationRule{}, fmt.Errorf("no such TranslationRule: %q", id) + } +} + +// IsEmpty returns whether this translation rule contains only nil members. +func (r TranslationRule) IsEmpty() bool { + return r.TranslateSubcapacities == nil && r.TranslateSubresources == nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (r *TranslationRule) UnmarshalYAML(unmarshal func(any) error) error { + var id string + err := unmarshal(&id) + if err != nil { + return err + } + + *r, err = NewTranslationRule(id) + return err +} + +func translateCinderOrManilaSubcapacities(input string, az limes.AvailabilityZone) (string, error) { + if input == "" || input == "[]" { + return input, nil + } + + type newFormat struct { + Name string `json:"name"` + Capacity uint64 `json:"capacity"` + Usage *uint64 `json:"usage"` + Attributes struct { + ExclusionReason string `json:"exclusion_reason"` + RealCapacity uint64 `json:"real_capacity"` + } `json:"attributes"` + } + var inputs []newFormat + err := json.Unmarshal([]byte(input), &inputs) + if err != nil { + return "", err + } + + type oldFormat struct { + PoolName string `json:"pool_name"` + AvailabilityZone limes.AvailabilityZone `json:"az"` + CapacityGiB uint64 `json:"capacity_gib"` + UsageGiB uint64 `json:"usage_gib"` + ExclusionReason string `json:"exclusion_reason"` + } + outputs := make([]oldFormat, len(inputs)) + for idx, in := range inputs { + if in.Usage == nil { + return "", fmt.Errorf("no usage in subcapacity: %#v", in) + } + outputs[idx] = oldFormat{ + PoolName: in.Name, + AvailabilityZone: az, + CapacityGiB: in.Capacity, + UsageGiB: *in.Usage, + ExclusionReason: in.Attributes.ExclusionReason, + } + if in.Attributes.ExclusionReason != "" { + outputs[idx].CapacityGiB = in.Attributes.RealCapacity + } + } + buf, err := json.Marshal(outputs) + return string(buf), err +} + +func translateCinderVolumeSubresources(input string, az limes.AvailabilityZone) (string, error) { + if input == "" || input == "[]" { + return input, nil + } + + type newFormat struct { + ID string `json:"id"` + Name string `json:"name"` + Attributes struct { + SizeGiB uint64 `json:"size_gib"` + Status string `json:"status"` + } `json:"attributes"` + } + var inputs []newFormat + err := json.Unmarshal([]byte(input), &inputs) + if err != nil { + return "", err + } + + type oldFormat struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Size limes.ValueWithUnit `json:"size"` + AvailabilityZone limes.AvailabilityZone `json:"availability_zone"` + } + outputs := make([]oldFormat, len(inputs)) + for idx, in := range inputs { + outputs[idx] = oldFormat{ + ID: in.ID, + Name: in.Name, + Status: in.Attributes.Status, + Size: limes.ValueWithUnit{Value: in.Attributes.SizeGiB, Unit: limes.UnitGibibytes}, + AvailabilityZone: az, + } + } + buf, err := json.Marshal(outputs) + return string(buf), err +} + +func translateCinderSnapshotSubresources(input string, az limes.AvailabilityZone) (string, error) { + if input == "" || input == "[]" { + return input, nil + } + + type newFormat struct { + ID string `json:"id"` + Name string `json:"name"` + Attributes struct { + SizeGiB uint64 `json:"size_gib"` + Status string `json:"status"` + VolumeID string `json:"volume_id"` + } `json:"attributes"` + } + var inputs []newFormat + err := json.Unmarshal([]byte(input), &inputs) + if err != nil { + return "", err + } + + type oldFormat struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Size limes.ValueWithUnit `json:"size"` + VolumeID string `json:"volume_id"` + } + outputs := make([]oldFormat, len(inputs)) + for idx, in := range inputs { + outputs[idx] = oldFormat{ + ID: in.ID, + Name: in.Name, + Status: in.Attributes.Status, + Size: limes.ValueWithUnit{Value: in.Attributes.SizeGiB, Unit: limes.UnitGibibytes}, + VolumeID: in.Attributes.VolumeID, + } + } + buf, err := json.Marshal(outputs) + return string(buf), err +} diff --git a/internal/liquids/cinder/liquid.go b/internal/liquids/cinder/liquid.go index be2a14f6b..f87e84a72 100644 --- a/internal/liquids/cinder/liquid.go +++ b/internal/liquids/cinder/liquid.go @@ -31,9 +31,6 @@ import ( "github.com/sapcc/limes/internal/liquids" ) -// NOTE: this will render subresources and subcapacities in the LIQUID format - to maintain compatibility within the v1 API, we will use a translation layer on the API level to translate subresources and subcapacities back into the known formats -// TODO: if you see this comment, reject the PR review - type Logic struct { // configuration WithSubcapacities bool `json:"with_subcapacities"` diff --git a/internal/liquids/cinder/usage.go b/internal/liquids/cinder/usage.go index 0a8097d94..f98da3541 100644 --- a/internal/liquids/cinder/usage.go +++ b/internal/liquids/cinder/usage.go @@ -154,8 +154,9 @@ func (l *Logic) collectSnapshotSubresources(ctx context.Context, projectUUID str ID: snapshot.ID, Name: snapshot.Name, Attributes: SnapshotAttributes{ - SizeGiB: uint64(snapshot.Size), - Status: snapshot.Status, + SizeGiB: uint64(snapshot.Size), + Status: snapshot.Status, + VolumeID: snapshot.VolumeID, }, }.Finalize() if err != nil { @@ -186,8 +187,9 @@ type VolumeAttributes struct { // SnapshotAttributes is the Attributes payload for a Cinder snapshot subresource. type SnapshotAttributes struct { - SizeGiB uint64 `json:"size_gib"` - Status string `json:"status"` + SizeGiB uint64 `json:"size_gib"` + Status string `json:"status"` + VolumeID string `json:"volume_id"` } //////////////////////////////////////////////////////////////////////////////// diff --git a/internal/liquids/manila/liquid.go b/internal/liquids/manila/liquid.go index 682f0ee59..3625b5f7b 100644 --- a/internal/liquids/manila/liquid.go +++ b/internal/liquids/manila/liquid.go @@ -39,9 +39,6 @@ import ( "github.com/sapcc/limes/internal/liquids" ) -// NOTE: this will render subresources and subcapacities in the LIQUID format - to maintain compatibility within the v1 API, we will use a translation layer on the API level to translate subresources and subcapacities back into the known formats -// TODO: if you see this comment, reject the PR review - type Logic struct { // configuration CapacityCalculation struct { diff --git a/internal/reports/cluster.go b/internal/reports/cluster.go index 704a0d665..2c3ca3620 100644 --- a/internal/reports/cluster.go +++ b/internal/reports/cluster.go @@ -22,6 +22,7 @@ package reports import ( "database/sql" "encoding/json" + "fmt" "strings" "time" @@ -133,7 +134,7 @@ func GetClusterResources(cluster *core.Cluster, now time.Time, dbi db.Interface, if !filter.Includes[dbServiceType][dbResourceName] { return nil } - service, resource := findInClusterReport(cluster, report, dbServiceType, dbResourceName, now) + service, resource, _ := findInClusterReport(cluster, report, dbServiceType, dbResourceName, now) service.MaxScrapedAt = mergeMaxTime(service.MaxScrapedAt, maxScrapedAt) service.MinScrapedAt = mergeMinTime(service.MinScrapedAt, minScrapedAt) @@ -188,7 +189,7 @@ func GetClusterResources(cluster *core.Cluster, now time.Time, dbi db.Interface, if !filter.Includes[dbServiceType][dbResourceName] { return nil } - _, resource := findInClusterReport(cluster, report, dbServiceType, dbResourceName, now) + _, resource, _ := findInClusterReport(cluster, report, dbServiceType, dbResourceName, now) if quota != nil && !resource.NoQuota { // NOTE: This is called "DomainsQuota" for historical reasons, but it is actually @@ -225,7 +226,7 @@ func GetClusterResources(cluster *core.Cluster, now time.Time, dbi db.Interface, if !filter.Includes[dbServiceType][dbResourceName] { return nil } - _, resource := findInClusterReport(cluster, report, dbServiceType, dbResourceName, now) + _, resource, behavior := findInClusterReport(cluster, report, dbServiceType, dbResourceName, now) //NOTE: resource.Capacity is computed from this below once data for all AZs was ingested if resource.RawCapacity == nil { @@ -234,6 +235,14 @@ func GetClusterResources(cluster *core.Cluster, now time.Time, dbi db.Interface, resource.RawCapacity = pointerTo(*resource.RawCapacity + *rawCapacityInAZ) } if subcapacitiesInAZ != nil && *subcapacitiesInAZ != "" && filter.IsSubcapacityAllowed(dbServiceType, dbResourceName) { + translate := behavior.TranslationRuleInV1API.TranslateSubcapacities + if translate != nil { + *subcapacitiesInAZ, err = translate(*subcapacitiesInAZ, *availabilityZone) + if err != nil { + return fmt.Errorf("could not apply TranslationRule to subcapacities in %s/%s/%s: %w", + dbServiceType, dbResourceName, *availabilityZone, err) + } + } mergeJSONListInto(&resource.Subcapacities, *subcapacitiesInAZ) } @@ -242,7 +251,7 @@ func GetClusterResources(cluster *core.Cluster, now time.Time, dbi db.Interface, Name: *availabilityZone, Usage: unwrapOrDefault(usageInAZ, 0), } - overcommitFactor := cluster.BehaviorForResource(dbServiceType, dbResourceName).OvercommitFactor + overcommitFactor := behavior.OvercommitFactor azReport.Capacity = overcommitFactor.ApplyTo(*rawCapacityInAZ) if azReport.Capacity != *rawCapacityInAZ { azReport.RawCapacity = *rawCapacityInAZ @@ -301,7 +310,7 @@ func GetClusterResources(cluster *core.Cluster, now time.Time, dbi db.Interface, if !filter.Includes[dbServiceType][dbResourceName] { return nil } - _, resource := findInClusterReport(cluster, report, dbServiceType, dbResourceName, now) + _, resource, _ := findInClusterReport(cluster, report, dbServiceType, dbResourceName, now) azReport := resource.PerAZ[az] if azReport == nil { @@ -445,7 +454,7 @@ func GetClusterRates(cluster *core.Cluster, dbi db.Interface, filter Filter) (*l return report, nil } -func findInClusterReport(cluster *core.Cluster, report *limesresources.ClusterReport, dbServiceType db.ServiceType, dbResourceName liquid.ResourceName, now time.Time) (*limesresources.ClusterServiceReport, *limesresources.ClusterResourceReport) { +func findInClusterReport(cluster *core.Cluster, report *limesresources.ClusterReport, dbServiceType db.ServiceType, dbResourceName liquid.ResourceName, now time.Time) (*limesresources.ClusterServiceReport, *limesresources.ClusterResourceReport, core.ResourceBehavior) { behavior := cluster.BehaviorForResource(dbServiceType, dbResourceName) apiIdentity := behavior.IdentityInV1API @@ -477,7 +486,7 @@ func findInClusterReport(cluster *core.Cluster, report *limesresources.ClusterRe service.Resources[apiIdentity.ResourceName] = resource } - return service, resource + return service, resource, behavior } func skipAZBreakdown(azReports limesresources.ClusterAvailabilityZoneReports) bool { diff --git a/internal/reports/project.go b/internal/reports/project.go index bd5e7c5d2..4005d4484 100644 --- a/internal/reports/project.go +++ b/internal/reports/project.go @@ -241,6 +241,14 @@ func GetProjectResources(cluster *core.Cluster, domain db.Domain, project *db.Pr resReport.PhysicalUsage = &sum } if azSubresources != nil { + translate := behavior.TranslationRuleInV1API.TranslateSubresources + if translate != nil { + *azSubresources, err = translate(*azSubresources, *az) + if err != nil { + return fmt.Errorf("could not apply TranslationRule to subresources in %s/%s/%s of project %d: %w", + dbServiceType, dbResourceName, *az, currentProjectID, err) + } + } mergeJSONListInto(&resReport.Subresources, *azSubresources) }