From 1434463b632de590fed55bc9a14d0cde5663e648 Mon Sep 17 00:00:00 2001 From: Varsius <42544122+Varsius@users.noreply.github.com> Date: Tue, 5 Nov 2024 08:51:16 +0100 Subject: [PATCH] Add api endpoint to render request payloads for querying the LIQUID api --- docs/users/api-spec-resources.md | 10 ++ internal/api/core.go | 3 + internal/api/liquid.go | 121 +++++++++++++++++ internal/api/liquid_test.go | 158 ++++++++++++++++++++++ internal/core/plugin.go | 9 ++ internal/plugins/capacity_liquid.go | 43 +++--- internal/plugins/capacity_manual.go | 4 + internal/plugins/capacity_nova.go | 4 + internal/plugins/capacity_prometheus.go | 4 + internal/plugins/capacity_sapcc_ironic.go | 4 + internal/plugins/liquid.go | 16 ++- internal/plugins/nova.go | 4 + internal/test/plugins/capacity_static.go | 4 + internal/test/plugins/quota_generic.go | 4 + internal/test/plugins/quota_noop.go | 4 + 15 files changed, 371 insertions(+), 21 deletions(-) create mode 100644 internal/api/liquid.go create mode 100644 internal/api/liquid_test.go diff --git a/docs/users/api-spec-resources.md b/docs/users/api-spec-resources.md index 1a0afd372..2892e5247 100644 --- a/docs/users/api-spec-resources.md +++ b/docs/users/api-spec-resources.md @@ -798,3 +798,13 @@ The objects at `scrape_errors[]` may contain the following fields: | `service_type` | string | Type name of the service where this resource scrape error was observed. | | `checked_at` | integer | UNIX timestamp of the instant when this resource scrape error was observed in the specified project and service. | | `message` | string | The exact error message that was observed. | + +### GET /admin/liquid/service-capacity-request + +Generates the request body payload for querying the LIQUID API endpoint /v1/report-capacity of a specific service. +Requires the `?service_type` query parameter. + +### GET /admin/liquid/service-usage-request + +Generates the request body payload for querying the LIQUID API endpoint /v1/projects/:uuid/report-usage of a specific service and project. +Requires the `?service_type` and `?project_id` query parameters. \ No newline at end of file diff --git a/internal/api/core.go b/internal/api/core.go index 4d8a00a04..94e2e80fb 100644 --- a/internal/api/core.go +++ b/internal/api/core.go @@ -168,6 +168,9 @@ func (p *v1Provider) AddTo(r *mux.Router) { r.Methods("GET").Path("/v1/commitment-conversion/{service_type}/{resource_name}").HandlerFunc(p.GetCommitmentConversions) r.Methods("POST").Path("/v1/domains/{domain_id}/projects/{project_id}/commitments/{commitment_id}/convert").HandlerFunc(p.ConvertCommitment) r.Methods("POST").Path("/v1/domains/{domain_id}/projects/{project_id}/commitments/{commitment_id}/update-duration").HandlerFunc(p.UpdateCommitmentDuration) + + r.Methods("GET").Path("/admin/liquid/service-capacity-request").HandlerFunc(p.GetServiceCapacityRequest) + r.Methods("GET").Path("/admin/liquid/service-usage-request").HandlerFunc(p.GetServiceUsageRequest) } // RequireJSON will parse the request body into the given data structure, or diff --git a/internal/api/liquid.go b/internal/api/liquid.go new file mode 100644 index 000000000..13814ee82 --- /dev/null +++ b/internal/api/liquid.go @@ -0,0 +1,121 @@ +/****************************************************************************** +* +* 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 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 ( + "database/sql" + "errors" + "net/http" + + "github.com/sapcc/go-bits/httpapi" + "github.com/sapcc/go-bits/respondwith" + + "github.com/sapcc/limes/internal/core" + "github.com/sapcc/limes/internal/datamodel" + "github.com/sapcc/limes/internal/db" +) + +// GetServiceCapacityRequest handles GET /admin/liquid/service-capacity-request?service_type=:type. +func (p *v1Provider) GetServiceCapacityRequest(w http.ResponseWriter, r *http.Request) { + httpapi.IdentifyEndpoint(r, "/admin/liquid/service-capacity-request") + token := p.CheckToken(r) + if !token.Require(w, "cluster:show") { + return + } + + serviceType := r.URL.Query().Get("service_type") + if serviceType == "" { + http.Error(w, "missing required parameter: service_type", http.StatusBadRequest) + return + } + + plugin, ok := p.Cluster.CapacityPlugins[serviceType] + if !ok { + http.Error(w, "invalid service type", http.StatusBadRequest) + return + } + + backchannel := datamodel.NewCapacityPluginBackchannel(p.Cluster, p.DB) + serviceCapacityRequest, err := plugin.BuildServiceCapacityRequest(backchannel, p.Cluster.Config.AvailabilityZones) + if respondwith.ErrorText(w, err) { + return + } + if serviceCapacityRequest == nil { + http.Error(w, "capacity plugin does not support LIQUID requests", http.StatusNotImplemented) + return + } + + respondwith.JSON(w, http.StatusOK, serviceCapacityRequest) +} + +// p.GetServiceUsageRequest handles GET /admin/liquid/service-usage-request?service_type=:type&project_id=:id. +func (p *v1Provider) GetServiceUsageRequest(w http.ResponseWriter, r *http.Request) { + httpapi.IdentifyEndpoint(r, "/admin/liquid/service-usage-request") + token := p.CheckToken(r) + if !token.Require(w, "cluster:show") { + return + } + + serviceType := r.URL.Query().Get("service_type") + if serviceType == "" { + http.Error(w, "missing required parameter: service_type", http.StatusBadRequest) + return + } + + plugin, ok := p.Cluster.QuotaPlugins[db.ServiceType(serviceType)] + if !ok { + http.Error(w, "invalid service type", http.StatusBadRequest) + return + } + + projectID := r.URL.Query().Get("project_id") + if projectID == "" { + http.Error(w, "missing required parameter: project_id", http.StatusBadRequest) + return + } + + var dbProject db.Project + err := p.DB.SelectOne(&dbProject, `SELECT * FROM projects WHERE id = $1`, projectID) + if errors.Is(err, sql.ErrNoRows) { + http.Error(w, "project not found", http.StatusNotFound) + return + } else if respondwith.ErrorText(w, err) { + return + } + + var dbDomain db.Domain + err = p.DB.SelectOne(&dbDomain, `SELECT * FROM domains WHERE id = $1`, dbProject.DomainID) + if respondwith.ErrorText(w, err) { + return + } + + domain := core.KeystoneDomainFromDB(dbDomain) + project := core.KeystoneProjectFromDB(dbProject, domain) + + serviceUsageRequest, err := plugin.BuildServiceUsageRequest(project, p.Cluster.Config.AvailabilityZones) + if respondwith.ErrorText(w, err) { + return + } + if serviceUsageRequest == nil { + http.Error(w, "quota plugin does not support LIQUID requests", http.StatusNotImplemented) + return + } + + respondwith.JSON(w, http.StatusOK, serviceUsageRequest) +} diff --git a/internal/api/liquid_test.go b/internal/api/liquid_test.go new file mode 100644 index 000000000..57214ecbd --- /dev/null +++ b/internal/api/liquid_test.go @@ -0,0 +1,158 @@ +/******************************************************************************* +* +* 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 ( + "net/http" + "testing" + + "github.com/sapcc/go-bits/assert" + + "github.com/sapcc/limes/internal/test" +) + +const ( + liquidQuotaTestConfigYAML = ` + availability_zones: [ az-one, az-two ] + discovery: + method: --test-static + params: + domains: + - { name: germany, id: uuid-for-germany } + projects: + uuid-for-germany: + - { name: berlin, id: uuid-for-berlin, parent_id: uuid-for-germany } + services: + - service_type: unittest + type: --test-generic + ` + liquidCapacityTestConfigYAML = ` + availability_zones: [ az-one, az-two ] + discovery: + method: --test-static + services: + - service_type: unittest + type: --test-generic + capacitors: + - id: unittest + type: --test-static + ` +) + +func TestGetServiceCapacityRequest(t *testing.T) { + t.Helper() + s := test.NewSetup(t, + test.WithConfig(liquidCapacityTestConfigYAML), + test.WithAPIHandler(NewV1API), + ) + + // endpoint requires cluster show permissions + s.TokenValidator.Enforcer.AllowView = false + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-capacity-request?service_type=unittest", + ExpectStatus: http.StatusForbidden, + }.Check(t, s.Handler) + s.TokenValidator.Enforcer.AllowView = true + + // expect error when service type is missing + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-capacity-request", + ExpectStatus: http.StatusBadRequest, + ExpectBody: assert.StringData("missing required parameter: service_type\n"), + }.Check(t, s.Handler) + + // expect error for invalid service type + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-capacity-request?service_type=invalid_service_type", + ExpectStatus: http.StatusBadRequest, + ExpectBody: assert.StringData("invalid service type\n"), + }.Check(t, s.Handler) + + // TODO: Implement happy path test for liquid plugins + // Expect not implemented error for now + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-capacity-request?service_type=unittest", + ExpectStatus: 501, + ExpectBody: assert.StringData("capacity plugin does not support LIQUID requests\n"), + }.Check(t, s.Handler) +} + +func TestServiceUsageRequest(t *testing.T) { + t.Helper() + s := test.NewSetup(t, + test.WithConfig(liquidQuotaTestConfigYAML), + test.WithAPIHandler(NewV1API), + test.WithDBFixtureFile("fixtures/start-data.sql"), + ) + + // endpoint requires cluster show permissions + s.TokenValidator.Enforcer.AllowView = false + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-usage-request?service_type=unittest&project_id=1", + ExpectStatus: http.StatusForbidden, + }.Check(t, s.Handler) + s.TokenValidator.Enforcer.AllowView = true + + // expect error when service type is missing + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-usage-request?project_id=1", + ExpectStatus: http.StatusBadRequest, + ExpectBody: assert.StringData("missing required parameter: service_type\n"), + }.Check(t, s.Handler) + + // expect error when project_id is missing + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-usage-request?service_type=unittest", + ExpectStatus: http.StatusBadRequest, + ExpectBody: assert.StringData("missing required parameter: project_id\n"), + }.Check(t, s.Handler) + + // expect error for invalid service type + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-usage-request?service_type=invalid_service_type&project_id=1", + ExpectStatus: http.StatusBadRequest, + ExpectBody: assert.StringData("invalid service type\n"), + }.Check(t, s.Handler) + + // expect error for invalid project_id + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-usage-request?service_type=unittest&project_id=-1", + ExpectStatus: http.StatusNotFound, + ExpectBody: assert.StringData("project not found\n"), + }.Check(t, s.Handler) + + // TODO: Implement happy path test for liquid plugins + // Expect not implemented error for now + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-usage-request?service_type=unittest&project_id=1", + ExpectStatus: 501, + ExpectBody: assert.StringData("quota plugin does not support LIQUID requests\n"), + }.Check(t, s.Handler) +} diff --git a/internal/core/plugin.go b/internal/core/plugin.go index 1b1591718..e3f692112 100644 --- a/internal/core/plugin.go +++ b/internal/core/plugin.go @@ -132,6 +132,11 @@ type QuotaPlugin interface { // The `serializedMetrics` return value is persisted in the Limes DB and // supplied to all subsequent RenderMetrics calls. Scrape(ctx context.Context, project KeystoneProject, allAZs []limes.AvailabilityZone) (result map[liquid.ResourceName]ResourceData, serializedMetrics []byte, err error) + + // BuildServiceUsageRequest generates the request body payload for querying + // the LIQUID API endpoint /v1/projects/:uuid/report-usage + BuildServiceUsageRequest(project KeystoneProject, allAZs []limes.AvailabilityZone) (*liquid.ServiceUsageRequest, error) + // SetQuota updates the backend service's quotas for the given project in the // given domain to the values specified here. The map is guaranteed to contain // values for all resources defined by Resources(). @@ -233,6 +238,10 @@ type CapacityPlugin interface { // supplied to all subsequent RenderMetrics calls. Scrape(ctx context.Context, backchannel CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (result map[db.ServiceType]map[liquid.ResourceName]PerAZ[CapacityData], serializedMetrics []byte, err error) + // BuildServiceCapacityRequest generates the request body payload for querying + // the LIQUID API endpoint /v1/report-capacity + BuildServiceCapacityRequest(backchannel CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (*liquid.ServiceCapacityRequest, error) + // DescribeMetrics is called when Prometheus is scraping metrics from // limes-collect, to provide an opportunity to the plugin to emit its own // metrics. diff --git a/internal/plugins/capacity_liquid.go b/internal/plugins/capacity_liquid.go index 9bd272e41..f823b89bb 100644 --- a/internal/plugins/capacity_liquid.go +++ b/internal/plugins/capacity_liquid.go @@ -73,25 +73,12 @@ func (p *liquidCapacityPlugin) Init(ctx context.Context, client *gophercloud.Pro // Scrape implements the core.QuotaPlugin interface. func (p *liquidCapacityPlugin) Scrape(ctx context.Context, backchannel core.CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (result map[db.ServiceType]map[liquid.ResourceName]core.PerAZ[core.CapacityData], serializedMetrics []byte, err error) { - req := liquid.ServiceCapacityRequest{ - AllAZs: allAZs, - DemandByResource: make(map[liquid.ResourceName]liquid.ResourceDemand, len(p.LiquidServiceInfo.Resources)), - } - - for resName, resInfo := range p.LiquidServiceInfo.Resources { - if !resInfo.HasCapacity { - continue - } - if !resInfo.NeedsResourceDemand { - continue - } - req.DemandByResource[resName], err = backchannel.GetResourceDemand(p.ServiceType, resName) - if err != nil { - return nil, nil, fmt.Errorf("while getting resource demand for %s/%s: %w", p.ServiceType, resName, err) - } + req, err := p.BuildServiceCapacityRequest(backchannel, allAZs) + if err != nil { + return nil, nil, err } - resp, err := p.LiquidClient.GetCapacityReport(ctx, req) + resp, err := p.LiquidClient.GetCapacityReport(ctx, *req) if err != nil { return nil, nil, err } @@ -131,6 +118,28 @@ func (p *liquidCapacityPlugin) Scrape(ctx context.Context, backchannel core.Capa return result, serializedMetrics, nil } +func (p *liquidCapacityPlugin) BuildServiceCapacityRequest(backchannel core.CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (*liquid.ServiceCapacityRequest, error) { + req := &liquid.ServiceCapacityRequest{ + AllAZs: allAZs, + DemandByResource: make(map[liquid.ResourceName]liquid.ResourceDemand, len(p.LiquidServiceInfo.Resources)), + } + + var err error + for resName, resInfo := range p.LiquidServiceInfo.Resources { + if !resInfo.HasCapacity { + continue + } + if !resInfo.NeedsResourceDemand { + continue + } + req.DemandByResource[resName], err = backchannel.GetResourceDemand(p.ServiceType, resName) + if err != nil { + return nil, fmt.Errorf("while getting resource demand for %s/%s: %w", p.ServiceType, resName, err) + } + } + return req, nil +} + // DescribeMetrics implements the core.QuotaPlugin interface. func (p *liquidCapacityPlugin) DescribeMetrics(ch chan<- *prometheus.Desc) { liquidDescribeMetrics(ch, p.LiquidServiceInfo.CapacityMetricFamilies, nil) diff --git a/internal/plugins/capacity_manual.go b/internal/plugins/capacity_manual.go index 5dca28fea..89c24bafe 100644 --- a/internal/plugins/capacity_manual.go +++ b/internal/plugins/capacity_manual.go @@ -71,6 +71,10 @@ func (p *capacityManualPlugin) Scrape(ctx context.Context, _ core.CapacityPlugin return result, nil, nil } +func (p *capacityManualPlugin) BuildServiceCapacityRequest(backchannel core.CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (*liquid.ServiceCapacityRequest, error) { + return nil, nil +} + // DescribeMetrics implements the core.CapacityPlugin interface. func (p *capacityManualPlugin) DescribeMetrics(ch chan<- *prometheus.Desc) { // not used by this plugin diff --git a/internal/plugins/capacity_nova.go b/internal/plugins/capacity_nova.go index eb5299a44..2fef851b1 100644 --- a/internal/plugins/capacity_nova.go +++ b/internal/plugins/capacity_nova.go @@ -518,6 +518,10 @@ func (p *capacityNovaPlugin) Scrape(ctx context.Context, backchannel core.Capaci return map[db.ServiceType]map[liquid.ResourceName]core.PerAZ[core.CapacityData]{"compute": capacities}, serializedMetrics, err } +func (p *capacityNovaPlugin) BuildServiceCapacityRequest(backchannel core.CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (*liquid.ServiceCapacityRequest, error) { + return nil, nil +} + var novaHypervisorWellformedGauge = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "limes_nova_hypervisor_is_wellformed", diff --git a/internal/plugins/capacity_prometheus.go b/internal/plugins/capacity_prometheus.go index f23261389..a9081c642 100644 --- a/internal/plugins/capacity_prometheus.go +++ b/internal/plugins/capacity_prometheus.go @@ -134,6 +134,10 @@ func (p *capacityPrometheusPlugin) scrapeOneResource(ctx context.Context, client return result, nil } +func (p *capacityPrometheusPlugin) BuildServiceCapacityRequest(backchannel core.CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (*liquid.ServiceCapacityRequest, error) { + return nil, nil +} + // DescribeMetrics implements the core.CapacityPlugin interface. func (p *capacityPrometheusPlugin) DescribeMetrics(ch chan<- *prometheus.Desc) { // not used by this plugin diff --git a/internal/plugins/capacity_sapcc_ironic.go b/internal/plugins/capacity_sapcc_ironic.go index 2a1dae076..30c64fe6c 100644 --- a/internal/plugins/capacity_sapcc_ironic.go +++ b/internal/plugins/capacity_sapcc_ironic.go @@ -310,6 +310,10 @@ func (p *capacitySapccIronicPlugin) Scrape(ctx context.Context, _ core.CapacityP return map[db.ServiceType]map[liquid.ResourceName]core.PerAZ[core.CapacityData]{"compute": resultCompute}, serializedMetrics, err } +func (p *capacitySapccIronicPlugin) BuildServiceCapacityRequest(backchannel core.CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (*liquid.ServiceCapacityRequest, error) { + return nil, nil +} + var ( ironicUnmatchedNodesGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "limes_unmatched_ironic_nodes", diff --git a/internal/plugins/liquid.go b/internal/plugins/liquid.go index 40cdf7e44..3c6e26c7c 100644 --- a/internal/plugins/liquid.go +++ b/internal/plugins/liquid.go @@ -102,12 +102,12 @@ func (p *liquidQuotaPlugin) Scrape(ctx context.Context, project core.KeystonePro return nil, nil, nil } - req := liquid.ServiceUsageRequest{AllAZs: allAZs} - if p.LiquidServiceInfo.UsageReportNeedsProjectMetadata { - req.ProjectMetadata = project.ForLiquid() + req, err := p.BuildServiceUsageRequest(project, allAZs) + if err != nil { + return nil, nil, err } - resp, err := p.LiquidClient.GetUsageReport(ctx, project.UUID, req) + resp, err := p.LiquidClient.GetUsageReport(ctx, project.UUID, *req) if err != nil { return nil, nil, err } @@ -150,6 +150,14 @@ func (p *liquidQuotaPlugin) Scrape(ctx context.Context, project core.KeystonePro return result, serializedMetrics, nil } +func (p *liquidQuotaPlugin) BuildServiceUsageRequest(project core.KeystoneProject, allAZs []limes.AvailabilityZone) (*liquid.ServiceUsageRequest, error) { + req := &liquid.ServiceUsageRequest{AllAZs: allAZs} + if p.LiquidServiceInfo.UsageReportNeedsProjectMetadata { + req.ProjectMetadata = project.ForLiquid() + } + return req, nil +} + func castSliceToAny[T any](input []T) (output []any) { if input == nil { return nil diff --git a/internal/plugins/nova.go b/internal/plugins/nova.go index c828e708f..318a2b2fa 100644 --- a/internal/plugins/nova.go +++ b/internal/plugins/nova.go @@ -348,6 +348,10 @@ func (p *novaPlugin) Scrape(ctx context.Context, project core.KeystoneProject, a return result, serializedMetrics, err } +func (p *novaPlugin) BuildServiceUsageRequest(project core.KeystoneProject, allAZs []limes.AvailabilityZone) (*liquid.ServiceUsageRequest, error) { + return nil, nil +} + func (p *novaPlugin) pooledResourceName(hwVersion string, base liquid.ResourceName) liquid.ResourceName { // `base` is one of "cores", "instances" or "ram" if hwVersion == "" { diff --git a/internal/test/plugins/capacity_static.go b/internal/test/plugins/capacity_static.go index 3b406a8d8..738111462 100644 --- a/internal/test/plugins/capacity_static.go +++ b/internal/test/plugins/capacity_static.go @@ -111,6 +111,10 @@ func (p *StaticCapacityPlugin) Scrape(ctx context.Context, _ core.CapacityPlugin return result, serializedMetrics, nil } +func (p *StaticCapacityPlugin) BuildServiceCapacityRequest(backchannel core.CapacityPluginBackchannel, allAZs []limes.AvailabilityZone) (*liquid.ServiceCapacityRequest, error) { + return nil, nil +} + var ( unittestCapacitySmallerHalfMetric = prometheus.NewGauge( prometheus.GaugeOpts{Name: "limes_unittest_capacity_smaller_half"}, diff --git a/internal/test/plugins/quota_generic.go b/internal/test/plugins/quota_generic.go index d20428c6f..2681c19d8 100644 --- a/internal/test/plugins/quota_generic.go +++ b/internal/test/plugins/quota_generic.go @@ -136,6 +136,10 @@ func (p *GenericQuotaPlugin) ScrapeRates(ctx context.Context, project core.Keyst return result, string(serializedStateBytes), nil } +func (p *GenericQuotaPlugin) BuildServiceUsageRequest(project core.KeystoneProject, allAZs []limes.AvailabilityZone) (*liquid.ServiceUsageRequest, error) { + return nil, nil +} + // Scrape implements the core.QuotaPlugin interface. func (p *GenericQuotaPlugin) Scrape(ctx context.Context, project core.KeystoneProject, allAZs []limes.AvailabilityZone) (result map[liquid.ResourceName]core.ResourceData, serializedMetrics []byte, err error) { if p.ScrapeFails { diff --git a/internal/test/plugins/quota_noop.go b/internal/test/plugins/quota_noop.go index 05e1962e1..2b1345358 100644 --- a/internal/test/plugins/quota_noop.go +++ b/internal/test/plugins/quota_noop.go @@ -115,6 +115,10 @@ func (p *NoopQuotaPlugin) Scrape(ctx context.Context, project core.KeystoneProje return result, nil, nil } +func (p *NoopQuotaPlugin) BuildServiceUsageRequest(project core.KeystoneProject, allAZs []limes.AvailabilityZone) (*liquid.ServiceUsageRequest, error) { + return nil, nil +} + // SetQuota implements the core.QuotaPlugin interface. func (p *NoopQuotaPlugin) SetQuota(ctx context.Context, project core.KeystoneProject, quotas map[liquid.ResourceName]uint64) error { return nil