Skip to content

Commit

Permalink
feat(deployments): disable "Scale Up" when environment quotas are rea…
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewazores committed Apr 27, 2018
1 parent 2bf8605 commit d3b1f54
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
</deployments-donut-chart>
<div column class="deployments-donut-scale-controls fade-inline" *ngIf="!isIdled && !mini">
<div>
<a id="scaleUp" (click)="$event.preventDefault(); scaleUp()" [ngClass]="{ disabled: !scalable }" [attr.title]="!scalable ? undefined : 'Scale up'"
[attr.aria-disabled]="!scalable ? 'true' : undefined" role="button">
<a id="scaleUp" (click)="$event.preventDefault(); scaleUp()" [ngClass]="{ disabled: atQuota }" [attr.title]="atQuota ? 'You don\'t have enough resources. Scale down the number of pods on another deployment to make resources available.' : 'Scale up'"
[attr.aria-disabled]="atQuota ? 'true' : undefined" role="button">
<i class="fa fa-chevron-up"></i>
<span class="sr-only">Scale up</span>
</a>
</div>
<div>
<!-- Remove the title when disabled because the not-allowed styled cursor overlaps the tooltip on some browsers. -->
<a id="scaleDown" (click)="$event.preventDefault(); scaleDown()" [ngClass]="{ disabled: !scalable || desiredReplicas === 0 }" [attr.title]="!scalable || desiredReplicas === 0 ? undefined : 'Scale down'"
[attr.aria-disabled]="!scalable || desiredReplicas === 0 ? 'true' : undefined" role="button">
<a id="scaleDown" (click)="$event.preventDefault(); scaleDown()" [ngClass]="{ disabled: desiredReplicas === 0 }" [attr.title]="desiredReplicas === 0 ? undefined : 'Scale down'"
[attr.aria-disabled]="desiredReplicas === 0 ? 'true' : undefined" role="button">
<i class="fa fa-chevron-down"></i>
<span class="sr-only">Scale down</span>
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ import {
import { fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { Observable } from 'rxjs';
import {
Observable,
Subject
} from 'rxjs';
import { createMock } from 'testing/mock';
import {
initContext,
TestContext
} from 'testing/test-context';

import { NotificationsService } from 'app/shared/notifications.service';
import { CpuStat } from '../models/cpu-stat';
import { MemoryStat } from '../models/memory-stat';
import { MemoryUnit } from '../models/memory-unit';
import { PodPhase } from '../models/pod-phase';
import { Pods } from '../models/pods';
import { DeploymentsService } from '../services/deployments.service';
Expand Down Expand Up @@ -42,6 +48,10 @@ describe('DeploymentsDonutComponent', () => {
let notifications: jasmine.SpyObj<NotificationsService> =
jasmine.createSpyObj<NotificationsService>('NotificationsService', ['message']);
let mockSvc: jasmine.SpyObj<DeploymentsService>;

let environmentCpuSubject: Subject<CpuStat> = new Subject<CpuStat>();
let environmentMemSubject: Subject<MemoryStat> = new Subject<MemoryStat>();

beforeEach(fakeAsync(() => {
mockSvc = createMock(DeploymentsService);
mockSvc.scalePods.and.returnValue(
Expand All @@ -50,6 +60,8 @@ describe('DeploymentsDonutComponent', () => {
mockSvc.getPods.and.returnValue(
Observable.of({ pods: [['Running' as PodPhase, 1], ['Terminating' as PodPhase, 1]], total: 2 })
);
mockSvc.getEnvironmentCpuStat.and.returnValue(environmentCpuSubject);
mockSvc.getEnvironmentMemoryStat.and.returnValue(environmentMemSubject);
}));

initContext(DeploymentsDonutComponent, HostComponent,
Expand Down Expand Up @@ -163,6 +175,36 @@ describe('DeploymentsDonutComponent', () => {
expect(mockSvc.scalePods).toHaveBeenCalledWith('space', 'environmentName', 'application', 3);
expect(notifications.message).not.toHaveBeenCalled();
});

describe('atQuota', () => {
it('should default to "false"', function(this: Context) {
expect(this.testedDirective.atQuota).toBeFalsy();
});

it('should be "false" when both stats are below quota', function(this: Context) {
environmentCpuSubject.next({ used: 0, quota: 2 });
environmentMemSubject.next({ used: 0, quota: 2, units: MemoryUnit.GB });
expect(this.testedDirective.atQuota).toBeFalsy();
});

it('should be "true" when CPU usage reaches quota', function(this: Context) {
environmentCpuSubject.next({ used: 2, quota: 2 });
environmentMemSubject.next({ used: 1, quota: 2, units: MemoryUnit.GB });
expect(this.testedDirective.atQuota).toBeTruthy();
});

it('should be "true" when Memory usage reaches quota', function(this: Context) {
environmentCpuSubject.next({ used: 1, quota: 2 });
environmentMemSubject.next({ used: 2, quota: 2, units: MemoryUnit.GB });
expect(this.testedDirective.atQuota).toBeTruthy();
});

it('should be "true" when both stats usage reaches quota', function(this: Context) {
environmentCpuSubject.next({ used: 2, quota: 2 });
environmentMemSubject.next({ used: 2, quota: 2, units: MemoryUnit.GB });
expect(this.testedDirective.atQuota).toBeTruthy();
});
});
});

describe('DeploymentsDonutComponent error handling', () => {
Expand All @@ -179,6 +221,8 @@ describe('DeploymentsDonutComponent error handling', () => {
mockSvc.getPods.and.returnValue(
Observable.of({ pods: [['Running' as PodPhase, 1], ['Terminating' as PodPhase, 1]], total: 2 })
);
mockSvc.getEnvironmentCpuStat.and.returnValue(Observable.never());
mockSvc.getEnvironmentMemoryStat.and.returnValue(Observable.never());
}));

initContext(DeploymentsDonutComponent, HostComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import {
Component,
Input,
OnDestroy,
OnInit,
ViewEncapsulation
} from '@angular/core';

import { debounce } from 'lodash';
import { NotificationType } from 'ngx-base';
import { Observable } from 'rxjs';
import {
Observable,
Subscription
} from 'rxjs';

import { PodPhase } from '../models/pod-phase';

import { NotificationsService } from 'app/shared/notifications.service';
import { Pods } from '../models/pods';
import { Stat } from '../models/stat';
import { DeploymentsService } from '../services/deployments.service';

@Component({
Expand All @@ -28,12 +33,14 @@ export class DeploymentsDonutComponent implements OnInit {
@Input() applicationId: string;
@Input() environment: string;

isIdled = false;
scalable = true;
atQuota: boolean = false;
isIdled: boolean = false;
pods: Observable<Pods>;
desiredReplicas: number = 1;
debounceScale = debounce(this.scale, 650);

private subscriptions: Subscription[] = [];

colors: { [s in PodPhase]: string} = {
'Empty': '#fafafa', // pf-black-100
'Running': '#00b9e4', // pf-light-blue-400
Expand All @@ -57,18 +64,31 @@ export class DeploymentsDonutComponent implements OnInit {

ngOnInit(): void {
this.pods = this.deploymentsService.getPods(this.spaceId, this.environment, this.applicationId);
this.pods.subscribe(pods => {
this.replicas = pods.total;
if (!this.scaleRequestPending) {
this.desiredReplicas = this.replicas;
}
});

this.subscriptions.push(
this.pods.subscribe(pods => {
this.replicas = pods.total;
if (!this.scaleRequestPending) {
this.desiredReplicas = this.replicas;
}
})
);

this.subscriptions.push(
Observable.combineLatest(
this.deploymentsService.getEnvironmentCpuStat(this.spaceId, this.environment),
this.deploymentsService.getEnvironmentMemoryStat(this.spaceId, this.environment)
).subscribe((stats: Stat[]): void => {
this.atQuota = stats.some((stat: Stat): boolean => stat.used >= stat.quota);
})
);
}

ngOnDestroy(): void {
this.subscriptions.forEach((subscription: Subscription): void => subscription.unsubscribe());
}

scaleUp(): void {
if (!this.scalable) {
return;
}
let desired = this.desiredReplicas;
this.desiredReplicas = desired + 1;

Expand All @@ -77,10 +97,6 @@ export class DeploymentsDonutComponent implements OnInit {
}

scaleDown(): void {
if (!this.scalable) {
return;
}

if (this.desiredReplicas === 0) {
return;
}
Expand All @@ -93,19 +109,21 @@ export class DeploymentsDonutComponent implements OnInit {
}

private scale(): void {
this.deploymentsService.scalePods(
this.spaceId, this.environment, this.applicationId, this.desiredReplicas
).first().subscribe(
success => {
this.scaleRequestPending = false;
},
error => {
this.scaleRequestPending = false;
this.notifications.message({
type: NotificationType.WARNING,
message: error
});
}
this.subscriptions.push(
this.deploymentsService.scalePods(
this.spaceId, this.environment, this.applicationId, this.desiredReplicas
).first().subscribe(
success => {
this.scaleRequestPending = false;
},
error => {
this.scaleRequestPending = false;
this.notifications.message({
type: NotificationType.WARNING,
message: error
});
}
)
);
}
}

0 comments on commit d3b1f54

Please sign in to comment.