Skip to content

Commit

Permalink
Merge pull request #27 from StatCan/kubecost-estimates
Browse files Browse the repository at this point in the history
feat: add cost estimate table
  • Loading branch information
brendangadd authored Oct 21, 2020
2 parents 9d02488 + 2284c4d commit 49dea69
Show file tree
Hide file tree
Showing 14 changed files with 276 additions and 21 deletions.
6 changes: 4 additions & 2 deletions frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { SnackBarComponent } from "./services/snack-bar/snack-bar.component";
import { ResourceFormComponent } from "./resource-form/resource-form.component";
import { ConfirmDialogComponent } from "./main-table/confirm-dialog/confirm-dialog.component";
import { VolumeComponent } from "./resource-form/volume/volume.component";
import { CostTableComponent } from "./main-table/cost-table/cost-table.component";
import { FormNameComponent } from "./resource-form/form-name/form-name.component";
import { FormImageComponent } from "./resource-form/form-image/form-image.component";
import { FormSpecsComponent } from "./resource-form/form-specs/form-specs.component";
Expand All @@ -49,7 +50,7 @@ import { RokErrorMsgComponent } from "./uis/rok/rok-error-msg/rok-error-msg.comp
import { FormConfigurationsComponent } from "./resource-form/form-configurations/form-configurations.component";
import { FormGpusComponent } from "./resource-form/form-gpus/form-gpus.component";
import { VolumeTableComponent } from "./main-table/volumes-table/volume-table.component";

import { KubecostService } from './services/kubecost.service';

@NgModule({
declarations: [
Expand Down Expand Up @@ -79,6 +80,7 @@ import { VolumeTableComponent } from "./main-table/volumes-table/volume-table.co
FormConfigurationsComponent,
FormGpusComponent,
VolumeTableComponent,
CostTableComponent
],
imports: [
BrowserModule,
Expand All @@ -90,7 +92,7 @@ import { VolumeTableComponent } from "./main-table/volumes-table/volume-table.co
FormsModule,
ReactiveFormsModule
],
providers: [NamespaceService, KubernetesService, SnackBarService],
providers: [NamespaceService, KubecostService, KubernetesService, SnackBarService],
bootstrap: [AppComponent],
entryComponents: [SnackBarComponent, ConfirmDialogComponent]
})
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/app/main-table/cost-table/cost-table.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="card mat-elevation-z2 mat-typography">
<div class="header">
<mat-icon>attach_money</mat-icon>
<p>Cost</p>
</div>

<p>
Estimated cost per day of notebook server resources – summed values may not
match total due to rounding
</p>

<mat-divider></mat-divider>
<table style="width: auto">
<tr class="mat-header-row" style="text-align: left;">
<th class="mat-header-cell">Compute</th>
<th class="mat-header-cell">GPUs</th>
<th class="mat-header-cell">Storage</th>
<th class="mat-header-cell" width="99%">Total</th>
</tr>
<tr *ngIf="aggregatedCost?.data[currNamespace]; let cost" class="mat-row" style="text-align: left;">
<td class="mat-cell">{{ formatCost(cost.cpuCost + cost.ramCost) }}</td>
<td class="mat-cell">{{ formatCost(cost.gpuCost) }}</td>
<td class="mat-cell">{{ formatCost(cost.pvCost) }}</td>
<td class="mat-cell" style="font-weight: 500;">{{ formatCost(cost.totalCost) }}</td>
</tr>
</table>
</div>
15 changes: 15 additions & 0 deletions frontend/src/app/main-table/cost-table/cost-table.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
p {
padding-left: 24px;
padding-right: 16px;
}

.header p {
padding: 12px 0 0px;
font-weight: 400;
font-size: 20px;
}

.mat-cell, .mat-header-cell, .mat-footer-cell {
padding-left: 12px;
padding-right: 195px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";

import { CostTableComponent } from "./cost-table.component";

describe("CostTableComponent", () => {
let component: CostTableComponent;
let fixture: ComponentFixture<CostTableComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CostTableComponent]
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(CostTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it("should create", () => {
expect(component).toBeTruthy();
});
});
17 changes: 17 additions & 0 deletions frontend/src/app/main-table/cost-table/cost-table.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Component, Input } from "@angular/core";
import { AggregateCostResponse } from 'src/app/services/kubecost.service';

@Component({
selector: "app-cost-table",
templateUrl: "./cost-table.component.html",
styleUrls: ["./cost-table.component.scss", "../main-table.component.scss"]
})
export class CostTableComponent{
@Input() aggregatedCost: AggregateCostResponse;
@Input() currNamespace:string;

formatCost(value: number): string {
return '$' + (value > 0 ? Math.max(value, 0.01) : 0).toFixed(2)
}

}
29 changes: 20 additions & 9 deletions frontend/src/app/main-table/main-table.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@
</div>

<!-- The Table showing the persistent volume claims -->
<div class="parent spacing">
<div class="spacer"></div>
<app-volume-table
[pvcProperties]="pvcProperties"
(deletePvcEvent)="deletePvc($event)"
>
</app-volume-table>
<div class="spacer"></div>
</div>
<div class="parent spacing">
<div class="spacer"></div>
<app-volume-table
[pvcProperties]="pvcProperties"
(deletePvcEvent)="deletePvc($event)"
>
</app-volume-table>
<div class="spacer"></div>
</div>

<!-- The Table showing our Costs-->
<div class="parent spacing">
<div class="spacer"></div>
<app-cost-table
[aggregatedCost]="aggregatedCost"
[currNamespace]="currNamespace"
></app-cost-table>
<div class="spacer"></div>
</div>


4 changes: 4 additions & 0 deletions frontend/src/app/main-table/main-table.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
padding-top: 1.5rem;
}

.spacing:last-of-type {
padding-bottom:1.5rem;
}

.card {
width: 900px;
padding: 0px;
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/app/main-table/main-table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {KubernetesService} from "src/app/services/kubernetes.service";
import {Subscription} from "rxjs";
import {isEqual} from "lodash";
import {first} from "rxjs/operators";
import { KubecostService, AggregateCostResponse } from 'src/app/services/kubecost.service';

import {ExponentialBackoff} from "src/app/utils/polling";
import {Volume, Resource} from "../utils/types";
Expand All @@ -23,12 +24,15 @@ export class MainTableComponent implements OnInit {
pvcs: Volume[] = [];
pvcProperties: PvcWithStatus[] = [];

aggregatedCost: AggregateCostResponse = null;

subscriptions = new Subscription();
poller: ExponentialBackoff;

constructor(
public ns: NamespaceService,
private k8s: KubernetesService,
private kubecostService: KubecostService,
) {}

ngOnInit() {
Expand Down Expand Up @@ -61,6 +65,7 @@ export class MainTableComponent implements OnInit {
const namespaceSub = this.ns.getSelectedNamespace().subscribe(namespace => {
this.currNamespace = namespace;
this.poller.reset();
this.getAggregatedCost();
});

this.subscriptions.add(resourcesSub);
Expand All @@ -84,4 +89,10 @@ export class MainTableComponent implements OnInit {
this.poller.reset();
});
}

getAggregatedCost() {
this.kubecostService.getAggregateCost(this.currNamespace).subscribe(
aggCost => this.aggregatedCost = aggCost
)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from "@angular/core";
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { MatTableDataSource } from "@angular/material/table";
import { Resource } from "src/app/utils/types";
import {MatDialog} from "@angular/material/dialog";
Expand All @@ -10,7 +10,7 @@ import {first} from "rxjs/operators";
templateUrl: "./resource-table.component.html",
styleUrls: ["./resource-table.component.scss", "../main-table.component.scss"]
})
export class ResourceTableComponent implements OnInit {
export class ResourceTableComponent{
@Input() notebooks: Resource[];
@Output() deleteNotebookEvent = new EventEmitter<Resource>();

Expand All @@ -30,10 +30,6 @@ export class ResourceTableComponent implements OnInit {
private dialog: MatDialog
) {}

ngOnInit() { }

ngOnDestroy() { }

// Resource (Notebook) Actions
connectResource(rsrc: Resource): void {
window.open(`/notebook/${rsrc.namespace}/${rsrc.name}/`);
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/app/services/kubecost.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Injectable } from "@angular/core";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";

import { Observable, throwError } from "rxjs";
import { tap, catchError } from "rxjs/operators";
import { environment } from "src/environments/environment";

import {
Resp,
SnackType
} from "../utils/types";
import { SnackBarService } from "./snack-bar.service";

export type AggregateCostResponse = {
code: number,
data: {
[namespace: string]: {
aggregation: string,
environment: string,
cpuAllocationAverage: number,
cpuCost: number,
cpuEfficiency: number,
efficiency: number,
gpuAllocationAverage: number,
gpuCost: number,
ramAllocationAverage: number,
ramCost: number,
ramEfficiency: number,
pvAllocationAverage: number,
pvCost: number,
networkCost: number,
sharedCost: number,
totalCost: number
}
},
message: string
}

@Injectable()
export class KubecostService {

constructor(private http: HttpClient, private snackBar: SnackBarService) { }

getAggregateCost(ns: string): Observable<AggregateCostResponse> {
const url = environment.apiUrl + `/api/namespaces/${ns}/cost/aggregated`

return this.http.get<AggregateCostResponse>(url, {
params: {
aggregation: 'namespace',
namespace: ns,
window: '1d'
}
}).pipe(
tap(res => this.handleBackendError(res)),
catchError(err => this.handleError(err))
);
}


// ---------------------------Error Handling----------------------------------
private handleBackendError(response: { code: number }) {
if (response.code < 200 || response.code >= 300) {
throw response;
}
}

private handleError(error: HttpErrorResponse | Resp): Observable<never> {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
if (error instanceof HttpErrorResponse) {
this.snackBar.show(
`${error.status}: There was an error trying to connect ` +
`to the backend API. ${error.message}`,
SnackType.Error
);
return throwError(error.message);
} else {
// Backend error thrown from handleBackendError
const backendError = error as Resp;
this.snackBar.show(backendError.log, SnackType.Error);
return throwError(backendError.log);
}
}
}
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ k8s.io/apimachinery v0.18.6 h1:RtFHnfGNfd1N0LeSrKCUznz5xtUP1elRGvHJbL3Ntag=
k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0=
k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig=
k8s.io/apimachinery v0.19.2 h1:5Gy9vQpAGTKHPVOh5c4plE274X8D/6cuEiTO2zve7tc=
k8s.io/client-go v0.18.6 h1:I+oWqJbibLSGsZj8Xs8F0aWVXJVIoUHWaaJV3kUN/Zw=
k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q=
k8s.io/code-generator v0.0.0-20200403215918-804a58607501/go.mod h1:UZPlxqFoDEMYYDJksMKLFggA4nK5Y3Nni//sVQggki4=
Expand Down
Loading

0 comments on commit 49dea69

Please sign in to comment.