Skip to content

Commit 22bdc13

Browse files
authored
Merge pull request #19 from StatCan/notebook-server-validation
Notebook server validation
2 parents 0d174a0 + 9cbb66c commit 22bdc13

11 files changed

+175
-36
lines changed

frontend/.prettierrc

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
{
2-
printWidth: 80,
3-
useTabs: false,
4-
tabWidth: 2,
5-
semi: true,
6-
singleQuote: true,
7-
trailingComma: 'all',
8-
bracketSpacing: true,
9-
arrowParens: 'avoid',
10-
proseWrap: 'preserve',
2+
"arrowParens": "avoid",
3+
"bracketSpacing": false,
4+
"trailingComma": "none"
115
}

frontend/src/app/resource-form/form-data-volumes/form-data-volumes.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ <h3>
2525
[pvcs]="pvcs"
2626
[ephemeral]="false"
2727
[defaultStorageClass]="defaultStorageClass"
28+
[sizes]="['4Gi', '8Gi', '16Gi', '32Gi', '64Gi', '128Gi', '256Gi', '512Gi']"
2829
>
2930
</app-volume>
3031

frontend/src/app/resource-form/form-name/form-name.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ <h3>
1616
formControlName="name"
1717
#name
1818
/>
19-
<mat-error>{{ showNameError() }}</mat-error>
19+
<mat-error>
20+
{{ showNameError() }}
21+
</mat-error>
2022
</mat-form-field>
2123

2224
<mat-form-field appearance="outline">

frontend/src/app/resource-form/form-name/form-name.component.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ export class FormNameComponent implements OnInit, OnDestroy {
2222
constructor(private k8s: KubernetesService, private ns: NamespaceService) {}
2323

2424
ngOnInit() {
25-
// Add the ExistingName validator to the list if it doesn't already exist
25+
// Add validator for notebook name (existing name, length, lowercase alphanumeric and '-')
2626
this.parentForm
2727
.get("name")
28-
.setValidators([Validators.required, this.existingName()]);
29-
28+
.setValidators([Validators.required, this.existingNameValidator(), Validators.pattern(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/), Validators.maxLength(52)]);
29+
3030
// Keep track of the existing Notebooks in the selected Namespace
3131
// Use these names to check if the input name exists
3232
const nsSub = this.ns.getSelectedNamespace().subscribe(ns => {
@@ -45,18 +45,26 @@ export class FormNameComponent implements OnInit, OnDestroy {
4545

4646
showNameError() {
4747
const nameCtrl = this.parentForm.get("name");
48-
48+
49+
if (nameCtrl.value.length==0) {
50+
return `The Notebook Server's name can't be empty`;
51+
}
4952
if (nameCtrl.hasError("existingName")) {
5053
return `Notebook Server "${nameCtrl.value}" already exists`;
51-
} else {
52-
return "The Notebook Server's name can't be empty";
54+
}
55+
if (nameCtrl.hasError("pattern")) {
56+
return `The Notebook Server's name can only contain lowercase alphanumeric characters or '-' and must start and end with an alphanumeric character`;
57+
}
58+
if (nameCtrl.hasError("maxlength")) {
59+
return `The Notebook Server's name can't be more than 52 characters`;
5360
}
5461
}
5562

56-
private existingName(): ValidatorFn {
63+
private existingNameValidator(): ValidatorFn {
5764
return (control: AbstractControl): { [key: string]: any } => {
5865
const exists = this.notebooks.has(control.value);
5966
return exists ? { existingName: true } : null;
6067
};
6168
}
69+
6270
}

frontend/src/app/resource-form/form-specs/form-specs.component.html

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@ <h3>
1212
<div class="inputs-wrapper">
1313
<mat-form-field appearance="outline">
1414
<mat-label>CPU</mat-label>
15-
<input matInput placeholder="# of CPU Cores" formControlName="cpu" />
16-
<mat-error>Please provide the CPU requirements</mat-error>
15+
<input
16+
matInput
17+
placeholder="# of CPU Cores"
18+
formControlName="cpu"
19+
[errorStateMatcher]="parentErrorKeysErrorStateMatcher('maxCpu')"
20+
/>
21+
<mat-error>{{ cpuErrorMessage() }}</mat-error>
1722
</mat-form-field>
1823

1924
<mat-form-field appearance="outline">
2025
<mat-label>Memory</mat-label>
21-
<input matInput placeholder="Amount of Memory" formControlName="memory" />
22-
<mat-error>Please provide the RAM requirements</mat-error>
26+
<input
27+
matInput
28+
placeholder="Amount of Memory"
29+
formControlName="memory"
30+
[errorStateMatcher]="parentErrorKeysErrorStateMatcher('maxRam')"
31+
/>
32+
<mat-error>{{ memoryErrorMessage() }}</mat-error>
2333
</mat-form-field>
2434
</div>
2535
</div>
Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
1-
import { Component, OnInit, Input } from "@angular/core";
2-
import { FormGroup } from "@angular/forms";
1+
import {Component, OnInit, Input} from "@angular/core";
2+
import {
3+
FormGroup,
4+
AbstractControl,
5+
Validators,
6+
ValidatorFn,
7+
ValidationErrors,
8+
FormControl,
9+
FormGroupDirective,
10+
NgForm
11+
} from "@angular/forms";
12+
13+
const MAX_FOR_GPU: ReadonlyMap<number, MaxResourceSpec> = new Map([
14+
[0, {cpu: 15, ram: 96}],
15+
[1, {cpu: 5, ram: 48}]
16+
]);
17+
18+
type MaxResourceSpec = {cpu: number; ram: number};
19+
20+
function resourcesValidator(): ValidatorFn {
21+
return function (control: AbstractControl): ValidationErrors | null {
22+
const gpuNumValue = control.get("gpus").get("num").value;
23+
const gpu = gpuNumValue === "none" ? 0 : parseInt(gpuNumValue, 10) || 0;
24+
const cpu = parseFloat(control.get("cpu").value);
25+
const ram = parseFloat(control.get("memory").value);
26+
const errors = {};
27+
28+
const max = MAX_FOR_GPU.get(gpu);
29+
if (cpu > max.cpu) {
30+
errors["maxCpu"] = {max: max.cpu, gpu};
31+
}
32+
if (ram > max.ram) {
33+
errors["maxRam"] = {max: max.ram, gpu};
34+
}
35+
36+
return Object.entries(errors).length > 0 ? errors : null;
37+
};
38+
}
339

440
@Component({
541
selector: "app-form-specs",
@@ -11,7 +47,70 @@ export class FormSpecsComponent implements OnInit {
1147
@Input() readonlyCPU: boolean;
1248
@Input() readonlyMemory: boolean;
1349

14-
constructor() {}
50+
ngOnInit() {
51+
this.parentForm
52+
.get("cpu")
53+
.setValidators([
54+
Validators.required,
55+
Validators.pattern(/^[0-9]+([.][0-9]+)?$/),
56+
Validators.min(0.5)
57+
]);
58+
this.parentForm
59+
.get("memory")
60+
.setValidators([
61+
Validators.required,
62+
Validators.pattern(/^[0-9]+([.][0-9]+)?(Gi)$/),
63+
Validators.min(1)
64+
]);
65+
this.parentForm.setValidators(resourcesValidator());
66+
}
67+
68+
parentErrorKeysErrorStateMatcher(keys: string | string[]) {
69+
const arrKeys = ([] as string[]).concat(keys);
70+
return {
71+
isErrorState(
72+
control: FormControl,
73+
form: FormGroupDirective | NgForm
74+
): boolean {
75+
return (
76+
(control.dirty && control.invalid) ||
77+
(form.dirty && arrKeys.some(key => form.hasError(key)))
78+
);
79+
}
80+
};
81+
}
82+
83+
cpuErrorMessage(): string {
84+
let e: any;
85+
const errs = this.parentForm.get("cpu").errors || {};
86+
87+
if (errs.required) return "Specify number of CPUs";
88+
if (errs.pattern) return "Must be a number";
89+
if ((e = errs.min)) return `Specify at least ${e.min} CPUs`;
90+
91+
if (this.parentForm.hasError("maxCpu")) {
92+
e = this.parentForm.errors.maxCpu;
93+
return (
94+
`Can't exceed ${e.max} CPUs` +
95+
(e.gpu > 0 ? ` with ${e.gpu} GPU(s) selected` : "")
96+
);
97+
}
98+
}
99+
100+
memoryErrorMessage(): string {
101+
let e: any;
102+
const errs = this.parentForm.get("memory").errors || {};
103+
104+
if (errs.required || errs.pattern)
105+
return "Specify amount of memory (e.g. 2Gi)";
106+
if ((e = errs.min)) return `Specify at least ${e.min}Gi of memory`;
15107

16-
ngOnInit() {}
108+
if (this.parentForm.hasError("maxRam")) {
109+
e = this.parentForm.errors.maxRam;
110+
return (
111+
`Can't exceed ${e.max}Gi of memory` +
112+
(e.gpu > 0 ? ` with ${e.gpu} GPU(s) selected` : "")
113+
);
114+
}
115+
}
17116
}

frontend/src/app/resource-form/form-workspace-volume/form-workspace-volume.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ <h3>
1818
[ephemeral]="parentForm.value.noWorkspace"
1919
[namespace]="parentForm.value.namespace"
2020
[defaultStorageClass]="defaultStorageClass"
21+
[sizes]="['4Gi', '8Gi', '16Gi', '32Gi']"
2122
>
2223
</app-volume>
2324
</div>

frontend/src/app/resource-form/volume/volume.component.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,20 @@
2222
}}</mat-option>
2323
</mat-select>
2424
</ng-template>
25+
<mat-error>
26+
{{ showNameError() }}
27+
</mat-error>
2528
</mat-form-field>
2629

2730
<!-- Size Input -->
2831
<mat-form-field appearance="outline" id="size">
2932
<mat-label>Size</mat-label>
30-
<input matInput formControlName="size" />
31-
</mat-form-field>
33+
<mat-select formControlName="size">
34+
<mat-option *ngFor="let sizeOptions of sizes" [value]="sizeOptions">{{
35+
sizeOptions
36+
}}</mat-option>
37+
</mat-select>
38+
</mat-form-field>
3239

3340
<!-- Mode Input -->
3441
<mat-form-field appearance="outline" id="mode">

frontend/src/app/resource-form/volume/volume.component.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component, OnInit, Input, OnDestroy } from "@angular/core";
2-
import { FormGroup } from "@angular/forms";
2+
import { FormGroup, Validators } from "@angular/forms";
33
import { Volume } from "src/app/utils/types";
44
import { Subscription } from "rxjs";
55

@@ -14,13 +14,14 @@ export class VolumeComponent implements OnInit, OnDestroy {
1414

1515
currentPVC: Volume;
1616
existingPVCs: Set<string> = new Set();
17-
17+
1818
subscriptions = new Subscription();
1919

2020
// ----- @Input Parameters -----
2121
@Input() volume: FormGroup;
2222
@Input() namespace: string;
23-
23+
@Input() sizes: Set<string>;
24+
2425
@Input()
2526
get notebookName() {
2627
return this._notebookName;
@@ -102,8 +103,11 @@ export class VolumeComponent implements OnInit, OnDestroy {
102103
constructor() {}
103104

104105
ngOnInit() {
105-
// type
106-
this.subscriptions.add(
106+
this.volume
107+
.get("name")
108+
.setValidators([Validators.required, Validators.pattern(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/)]);
109+
110+
this.subscriptions.add(
107111
this.volume.get("type").valueChanges.subscribe((type: string) => {
108112
this.setVolumeType(type);
109113
})
@@ -158,4 +162,17 @@ export class VolumeComponent implements OnInit, OnDestroy {
158162
this.volume.controls.name.setValue(this.currentVolName);
159163
}
160164
}
165+
166+
showNameError() {
167+
const volumeName = this.volume.get("name");
168+
169+
if (volumeName.hasError("required")) {
170+
return `The volume name can't be empty`
171+
}
172+
if (volumeName.hasError("pattern")) {
173+
return `The volume name can only contain lowercase alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character`;
174+
}
175+
}
176+
177+
161178
}

frontend/src/app/utils/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function addDataVolume(
8686
value: '{notebook-name}-vol-' + (l + 1),
8787
},
8888
size: {
89-
value: '10Gi',
89+
value: '16Gi',
9090
},
9191
mountPath: {
9292
value: '/home/jovyan/data-vol-' + (l + 1),

samples/spawner_ui_config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ spawnerFormDefaults:
5959
value: 'workspace-{notebook-name}'
6060
size:
6161
# The Size of the Workspace Volume (in Gi)
62-
value: '10Gi'
62+
value: '4Gi'
6363
mountPath:
6464
# The Path that the Workspace Volume will be mounted
6565
value: /home/jovyan
@@ -87,7 +87,7 @@ spawnerFormDefaults:
8787
# name:
8888
# value: '{notebook-name}-vol-1'
8989
# size:
90-
# value: '10Gi'
90+
# value: '16Gi'
9191
# class:
9292
# value: standard
9393
# mountPath:
@@ -102,7 +102,7 @@ spawnerFormDefaults:
102102
# name:
103103
# value: '{notebook-name}-vol-2'
104104
# size:
105-
# value: '10Gi'
105+
# value: '16Gi'
106106
# mountPath:
107107
# value: /home/jovyan/vol-2
108108
# accessModes:

0 commit comments

Comments
 (0)