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..e8811a444 --- /dev/null +++ b/internal/api/liquid.go @@ -0,0 +1,123 @@ +/****************************************************************************** +* +* 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" +) + +var errNotALiquid = errors.New("this plugin is not a liquid") + +// 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 errors.Is(err, errNotALiquid) { + http.Error(w, "capacity plugin does not support LIQUID requests", http.StatusNotImplemented) + return + } + if respondwith.ErrorText(w, err) { + 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 errors.Is(err, errNotALiquid) { + http.Error(w, "quota plugin does not support LIQUID requests", http.StatusNotImplemented) + return + } + if respondwith.ErrorText(w, err) { + 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..e4305e204 --- /dev/null +++ b/internal/api/liquid_test.go @@ -0,0 +1,180 @@ +/******************************************************************************* +* +* 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) + + // happy path + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-capacity-request?service_type=unittest", + ExpectStatus: 200, + ExpectBody: assert.JSONObject{ + "allAZs": []string{"az-one", "az-two"}, + "demandByResource": assert.JSONObject{ + "capacity": assert.JSONObject{ + "overcommitFactor": 1.5, + "perAZ": assert.JSONObject{ + "any": assert.JSONObject{ + "usage": 10, + "unusedCommitments": 0, + "pendingCommitments": 0, + }, + }, + }, + }, + }, + }.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) + + // happy path + assert.HTTPRequest{ + Method: "GET", + Path: "/admin/liquid/service-usage-request?service_type=unittest&project_id=1", + ExpectStatus: 200, + ExpectBody: assert.JSONObject{ + "allAZs": []string{"az-one", "az-two"}, + "projectMetadata": assert.JSONObject{ + "uuid": "uuid-for-berlin", + "name": "berlin", + "domain": assert.JSONObject{ + "uuid": "uuid-for-germany", + "name": "germany", + }, + }, + }, + }.Check(t, s.Handler) +} diff --git a/internal/core/plugin.go b/internal/core/plugin.go index 1b1591718..35da8d307 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..6d8250c91 100644 --- a/internal/plugins/capacity_liquid.go +++ b/internal/plugins/capacity_liquid.go @@ -73,22 +73,9 @@ 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) @@ -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 liquid.ServiceCapacityRequest{}, 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..d92096080 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 liquid.ServiceCapacityRequest{}, errors.New("this plugin is not a liquid") +} + // 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..fe2075fa5 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 liquid.ServiceCapacityRequest{}, errors.New("this plugin is not a liquid") +} + 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..93716ab7c 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 liquid.ServiceCapacityRequest{}, errors.New("this plugin is not a liquid") +} + // 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..125c4e9be 100644 --- a/internal/plugins/capacity_sapcc_ironic.go +++ b/internal/plugins/capacity_sapcc_ironic.go @@ -22,6 +22,7 @@ package plugins import ( "context" "encoding/json" + "errors" "regexp" "strconv" @@ -310,6 +311,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 liquid.ServiceCapacityRequest{}, errors.New("this plugin is not a liquid") +} + 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..501f5dbd4 100644 --- a/internal/plugins/liquid.go +++ b/internal/plugins/liquid.go @@ -102,9 +102,9 @@ 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) @@ -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..deb1c8135 100644 --- a/internal/plugins/nova.go +++ b/internal/plugins/nova.go @@ -22,6 +22,7 @@ package plugins import ( "context" "encoding/json" + "errors" "fmt" "maps" "math/big" @@ -348,6 +349,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 liquid.ServiceUsageRequest{}, errors.New("this plugin is not a liquid") +} + 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..67eb91eea 100644 --- a/internal/test/plugins/capacity_static.go +++ b/internal/test/plugins/capacity_static.go @@ -111,6 +111,24 @@ 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 liquid.ServiceCapacityRequest{ + AllAZs: []liquid.AvailabilityZone{"az-one", "az-two"}, + DemandByResource: map[liquid.ResourceName]liquid.ResourceDemand{ + "capacity": { + OvercommitFactor: 1.5, + PerAZ: map[liquid.AvailabilityZone]liquid.ResourceDemandInAZ{ + liquid.AvailabilityZoneAny: { + Usage: 10, + UnusedCommitments: 0, + PendingCommitments: 0, + }, + }, + }, + }, + }, 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..f4801f276 100644 --- a/internal/test/plugins/quota_generic.go +++ b/internal/test/plugins/quota_generic.go @@ -136,6 +136,13 @@ 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 liquid.ServiceUsageRequest{ + AllAZs: []liquid.AvailabilityZone{"az-one", "az-two"}, + ProjectMetadata: project.ForLiquid(), + }, 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..355933695 100644 --- a/internal/test/plugins/quota_noop.go +++ b/internal/test/plugins/quota_noop.go @@ -21,6 +21,7 @@ package plugins import ( "context" + "errors" "math/big" "github.com/gophercloud/gophercloud/v2" @@ -115,6 +116,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 liquid.ServiceUsageRequest{}, errors.New("this plugin is not a liquid") +} + // SetQuota implements the core.QuotaPlugin interface. func (p *NoopQuotaPlugin) SetQuota(ctx context.Context, project core.KeystoneProject, quotas map[liquid.ResourceName]uint64) error { return nil