-
Notifications
You must be signed in to change notification settings - Fork 5
Adding committed resource usage API #614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f4a445
9f45167
29ce3ed
d5e79e2
e6df6fd
63a1e18
613305e
50bb782
e549095
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,21 @@ type migration struct { | |
| DestCompute string `json:"dest_compute"` | ||
| } | ||
|
|
||
| // ServerDetail contains extended server information for usage reporting. | ||
| type ServerDetail struct { | ||
| ID string `json:"id"` | ||
| Name string `json:"name"` | ||
| Status string `json:"status"` | ||
| TenantID string `json:"tenant_id"` | ||
| Created string `json:"created"` | ||
| AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` | ||
| Hypervisor string `json:"OS-EXT-SRV-ATTR:hypervisor_hostname"` | ||
| FlavorName string // Populated from nested flavor.original_name | ||
| FlavorRAM uint64 // Populated from nested flavor.ram | ||
| FlavorVCPUs uint64 // Populated from nested flavor.vcpus | ||
| FlavorDisk uint64 // Populated from nested flavor.disk | ||
| } | ||
|
|
||
| type NovaClient interface { | ||
| // Initialize the Nova API with the Keystone authentication. | ||
| Init(ctx context.Context, client client.Client, conf NovaClientConfig) error | ||
|
|
@@ -47,6 +62,8 @@ type NovaClient interface { | |
| LiveMigrate(ctx context.Context, id string) error | ||
| // Get migrations for a server by ID. | ||
| GetServerMigrations(ctx context.Context, id string) ([]migration, error) | ||
| // List all servers for a project with detailed info. | ||
| ListProjectServers(ctx context.Context, projectID string) ([]ServerDetail, error) | ||
| } | ||
|
|
||
| type novaClient struct { | ||
|
|
@@ -160,3 +177,86 @@ func (api *novaClient) GetServerMigrations(ctx context.Context, id string) ([]mi | |
| slog.Info("fetched migrations for vm", "id", id, "count", len(migrations)) | ||
| return migrations, nil | ||
| } | ||
|
|
||
| // ListProjectServers retrieves all servers for a project with detailed info. | ||
| func (api *novaClient) ListProjectServers(ctx context.Context, projectID string) ([]ServerDetail, error) { | ||
| // Build URL with pagination support | ||
| initialURL := api.sc.Endpoint + "servers/detail?all_tenants=true&tenant_id=" + projectID | ||
| var nextURL = &initialURL | ||
| var result []ServerDetail | ||
|
|
||
| for nextURL != nil { | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, *nextURL, http.NoBody) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| req.Header.Set("X-Auth-Token", api.sc.Token()) | ||
| req.Header.Set("X-OpenStack-Nova-API-Version", api.sc.Microversion) | ||
|
|
||
| resp, err := api.sc.HTTPClient.Do(req) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() | ||
|
Comment on lines
+196
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resource leak: Using 🐛 Proposed fix: close body immediately after use resp, err := api.sc.HTTPClient.Do(req)
if err != nil {
return nil, err
}
- defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
+ resp.Body.Close()
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
// ... decode logic ...
if err := json.NewDecoder(resp.Body).Decode(&list); err != nil {
+ resp.Body.Close()
return nil, err
}
+ resp.Body.Close()
// Convert to ServerDetailAlternatively, extract the HTTP request logic into a helper function where 🤖 Prompt for AI Agents |
||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) | ||
| } | ||
|
|
||
| // Response structure with nested flavor | ||
| var list struct { | ||
| Servers []struct { | ||
| ID string `json:"id"` | ||
| Name string `json:"name"` | ||
| Status string `json:"status"` | ||
| TenantID string `json:"tenant_id"` | ||
| Created string `json:"created"` | ||
| AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` | ||
| Hypervisor string `json:"OS-EXT-SRV-ATTR:hypervisor_hostname"` | ||
| Flavor struct { | ||
| OriginalName string `json:"original_name"` | ||
| RAM uint64 `json:"ram"` | ||
| VCPUs uint64 `json:"vcpus"` | ||
| Disk uint64 `json:"disk"` | ||
| } `json:"flavor"` | ||
| } `json:"servers"` | ||
| Links []struct { | ||
| Rel string `json:"rel"` | ||
| Href string `json:"href"` | ||
| } `json:"servers_links"` | ||
| } | ||
|
|
||
| if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Convert to ServerDetail | ||
| for _, s := range list.Servers { | ||
| result = append(result, ServerDetail{ | ||
| ID: s.ID, | ||
| Name: s.Name, | ||
| Status: s.Status, | ||
| TenantID: s.TenantID, | ||
| Created: s.Created, | ||
| AvailabilityZone: s.AvailabilityZone, | ||
| Hypervisor: s.Hypervisor, | ||
| FlavorName: s.Flavor.OriginalName, | ||
| FlavorRAM: s.Flavor.RAM, | ||
| FlavorVCPUs: s.Flavor.VCPUs, | ||
| FlavorDisk: s.Flavor.Disk, | ||
| }) | ||
| } | ||
|
|
||
| // Check for next page | ||
| nextURL = nil | ||
| for _, link := range list.Links { | ||
| if link.Rel == "next" { | ||
| nextURL = &link.Href | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
|
||
| slog.Info("fetched servers for project", "projectID", projectID, "count", len(result)) | ||
| return result, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,37 +4,54 @@ | |
| package commitments | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "sync" | ||
|
|
||
| "github.com/cobaltcore-dev/cortex/internal/scheduling/nova" | ||
| "github.com/prometheus/client_golang/prometheus" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| ) | ||
|
|
||
| // UsageNovaClient is a minimal interface for the Nova client needed by the usage API. | ||
| // This allows for easy mocking in tests without implementing the full NovaClient interface. | ||
| type UsageNovaClient interface { | ||
| ListProjectServers(ctx context.Context, projectID string) ([]nova.ServerDetail, error) | ||
| } | ||
|
|
||
| // HTTPAPI implements Limes LIQUID commitment validation endpoints. | ||
| type HTTPAPI struct { | ||
| client client.Client | ||
| config Config | ||
| monitor ChangeCommitmentsAPIMonitor | ||
| client client.Client | ||
| config Config | ||
| novaClient UsageNovaClient | ||
| monitor ChangeCommitmentsAPIMonitor | ||
| usageMonitor ReportUsageAPIMonitor | ||
| capacityMonitor ReportCapacityAPIMonitor | ||
| // Mutex to serialize change-commitments requests | ||
| changeMutex sync.Mutex | ||
| } | ||
|
|
||
| func NewAPI(client client.Client) *HTTPAPI { | ||
| return NewAPIWithConfig(client, DefaultConfig()) | ||
| return NewAPIWithConfig(client, DefaultConfig(), nil) | ||
| } | ||
|
Comment on lines
34
to
36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t publish Line 35 still passes Minimal guard func (api *HTTPAPI) Init(mux *http.ServeMux, registry prometheus.Registerer) {
registry.MustRegister(&api.monitor)
registry.MustRegister(&api.usageMonitor)
registry.MustRegister(&api.capacityMonitor)
mux.HandleFunc("/v1/commitments/change-commitments", api.HandleChangeCommitments)
mux.HandleFunc("/v1/commitments/report-capacity", api.HandleReportCapacity)
mux.HandleFunc("/v1/commitments/info", api.HandleInfo)
- mux.HandleFunc("/v1/commitments/projects/", api.HandleReportUsage) // matches /v1/commitments/projects/:project_id/report-usage
+ if api.novaClient != nil {
+ mux.HandleFunc("/v1/commitments/projects/", api.HandleReportUsage) // matches /v1/commitments/projects/:project_id/report-usage
+ }
}A stronger fix would be to make Also applies to: 49-56 🤖 Prompt for AI Agents |
||
|
|
||
| func NewAPIWithConfig(client client.Client, config Config) *HTTPAPI { | ||
| func NewAPIWithConfig(client client.Client, config Config, novaClient UsageNovaClient) *HTTPAPI { | ||
| return &HTTPAPI{ | ||
| client: client, | ||
| config: config, | ||
| monitor: NewChangeCommitmentsAPIMonitor(), | ||
| client: client, | ||
| config: config, | ||
| novaClient: novaClient, | ||
| monitor: NewChangeCommitmentsAPIMonitor(), | ||
| usageMonitor: NewReportUsageAPIMonitor(), | ||
| capacityMonitor: NewReportCapacityAPIMonitor(), | ||
| } | ||
| } | ||
|
|
||
| func (api *HTTPAPI) Init(mux *http.ServeMux, registry prometheus.Registerer) { | ||
| registry.MustRegister(&api.monitor) | ||
| registry.MustRegister(&api.usageMonitor) | ||
| registry.MustRegister(&api.capacityMonitor) | ||
| mux.HandleFunc("/v1/commitments/change-commitments", api.HandleChangeCommitments) | ||
| mux.HandleFunc("/v1/commitments/report-capacity", api.HandleReportCapacity) | ||
| mux.HandleFunc("/v1/commitments/info", api.HandleInfo) | ||
| mux.HandleFunc("/v1/commitments/projects/", api.HandleReportUsage) // matches /v1/commitments/projects/:project_id/report-usage | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't gate the commitments HTTP API behind
nova-pipeline-controllers.This is the only
commitmentsAPI.Init()incmd/main.go, so deployments that do not enable that controller will stop serving/v1/commitments/...entirely. It also makes the Nova pipeline controller path hard-requirecommitments.ConfigviaGetConfigOrDie(), even though the usage path already tolerates a nil Nova client.Suggested direction
🤖 Prompt for AI Agents