Skip to content

Commit

Permalink
NAS-130588 / 25.04 / Docker configuration - install nvidia drivers (#…
Browse files Browse the repository at this point in the history
…10557)

* NAS-130588: Docker configuration - install nvidia drivers

* NAS-130588: Docker configuration - install nvidia drivers

* NAS-130588: PR Update

* NAS-130588: PR Update
  • Loading branch information
AlexKarpov98 authored Aug 27, 2024
1 parent 9af2d12 commit 8c16789
Show file tree
Hide file tree
Showing 98 changed files with 474 additions and 483 deletions.
4 changes: 3 additions & 1 deletion src/app/enums/docker-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ export interface DockerConfig {
pool: string;
dataset: string;
id: number;
nvidia: boolean;
}

export interface DockerConfigUpdate {
pool: string;
pool?: string;
nvidia?: boolean;
}

export interface DockerStatusResponse {
Expand Down
8 changes: 3 additions & 5 deletions src/app/helptext/apps/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,23 +216,21 @@ export const helptextApps = {
title: T('Add Catalog'),
editTitle: T('Edit Catalog'),
name: {
placeholder: T('Catalog Name'),
tooltip: T('Please specify name to be used to lookup catalog.'),
},
forceCreate: {
placeholder: T('Force Create'),
tooltip: T('Add catalog to system even if some trains are unhealthy.'),
},
repository: {
placeholder: T('Repository'),
tooltip: T('Please specify a valid git repository uri.'),
},
preferredTrains: {
placeholder: T('Preferred Trains'),
tooltip: T('Please specify trains from which UI should retrieve available applications for the catalog.'),
},
installNvidiaDriver: {
tooltip: T('Please specify whether to install NVIDIA driver or not.'),
},
branch: {
placeholder: T('Branch'),
tooltip: T('Please specify branch of git repository to use for the catalog.'),
},
},
Expand Down
1 change: 1 addition & 0 deletions src/app/interfaces/api/api-call-directory.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ export interface ApiCallDirectory {
// Docker
'docker.config': { params: void; response: DockerConfig };
'docker.status': { params: void; response: DockerStatusResponse };
'docker.lacks_nvidia_drivers': { params: void; response: boolean };

// LDAP
'ldap.config': { params: void; response: LdapConfig };
Expand Down
1 change: 1 addition & 0 deletions src/app/interfaces/catalog.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface Catalog {

export interface CatalogUpdate {
preferred_trains: string[];
nvidia?: boolean;
}

export type CatalogTrain = Record<string, CatalogApp>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<ix-modal-header
[title]="'Train Settings' | translate"
[title]="'Settings' | translate"
[loading]="isFormLoading()"
[requiredRoles]="requiredRoles"
></ix-modal-header>

<mat-card>
<mat-card-content>
<form class="ix-form-container" [formGroup]="form" (submit)="onSubmit()">
<ix-fieldset>
<ix-fieldset class="trains">
<ix-checkbox-list
formControlName="preferred_trains"
[label]="'Preferred Trains' | translate"
Expand All @@ -17,6 +17,16 @@
></ix-checkbox-list>
</ix-fieldset>

@if (showNvidiaCheckbox()) {
<ix-fieldset class="nvidia">
<ix-checkbox
formControlName="nvidia"
[label]="'Install NDIVIA Drivers' | translate"
[tooltip]="tooltips.install_nvidia_driver| translate"
></ix-checkbox>
</ix-fieldset>
}

<ix-form-actions>
<button
*ixRequiresRoles="requiredRoles"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
:host ::ng-deep {
.trains {
fieldset {
padding: 0 0 10px;
}

ix-label {
padding-left: 10px;
}
}
}

.nvidia {
border-top: 1px solid var(--lines);
display: block;
margin-bottom: 15px;
margin-top: 5px;
padding-top: 10px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonHarness } from '@angular/material/button/testing';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { mockCall, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { Catalog } from 'app/interfaces/catalog.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxCheckboxListHarness } from 'app/modules/forms/ix-forms/components/ix-checkbox-list/ix-checkbox-list.harness';
import { IxSlideInRef } from 'app/modules/forms/ix-forms/components/ix-slide-in/ix-slide-in-ref';
import { IxFormsModule } from 'app/modules/forms/ix-forms/ix-forms.module';
import { FormErrorHandlerService } from 'app/modules/forms/ix-forms/services/form-error-handler.service';
import { IxFormHarness } from 'app/modules/forms/ix-forms/testing/ix-form.harness';
import { CatalogSettingsComponent } from 'app/pages/apps/components/catalog-settings/catalog-settings.component';
import { AppsStore } from 'app/pages/apps/store/apps-store.service';
import { DockerStore } from 'app/pages/apps/store/docker.store';
import { WebSocketService } from 'app/services/ws.service';

describe('CatalogEditFormComponent', () => {
Expand All @@ -33,50 +36,133 @@ describe('CatalogEditFormComponent', () => {
preferred_trains: ['test'],
} as Catalog),
]),
mockProvider(DialogService, {
jobDialog: jest.fn(() => ({
afterClosed: () => of(null),
})),
}),
mockProvider(AppsStore),
mockProvider(IxSlideInRef),
mockProvider(FormErrorHandlerService),
mockAuth(),
],
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});
describe('no docker lacks nvidia drivers', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
mockProvider(DockerStore, {
nvidiaDriversInstalled$: of(false),
lacksNvidiaDrivers$: of(false),
}),
],
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('loads list of available trains and shows them', async () => {
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('catalog.trains');
it('loads list of available trains and shows them', async () => {
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('catalog.trains');

const checkboxList = await loader.getHarness(IxCheckboxListHarness);
const checkboxes = await checkboxList.getCheckboxes();
expect(checkboxes).toHaveLength(3);
expect(await checkboxes[0].getLabelText()).toBe('stable');
expect(await checkboxes[1].getLabelText()).toBe('community');
expect(await checkboxes[2].getLabelText()).toBe('test');
});
const checkboxList = await loader.getHarness(IxCheckboxListHarness);
const checkboxes = await checkboxList.getCheckboxes();
expect(checkboxes).toHaveLength(3);
expect(await checkboxes[0].getLabelText()).toBe('stable');
expect(await checkboxes[1].getLabelText()).toBe('community');
expect(await checkboxes[2].getLabelText()).toBe('test');
});

it('shows preferred trains when catalog is open for editing', async () => {
const form = await loader.getHarness(IxFormHarness);
const values = await form.getValues();

expect(values).toEqual({
'Preferred Trains': ['test'],
});
});

it('saves catalog updates and reloads catalog apps when form is saved', async () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
'Preferred Trains': ['stable', 'community'],
});

it('shows preferred trains when catalog is open for editing', async () => {
const form = await loader.getHarness(IxFormHarness);
const values = await form.getValues();
const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await saveButton.click();

expect(values).toEqual({
'Preferred Trains': ['test'],
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('catalog.update', [
{ preferred_trains: ['stable', 'community'] },
]);
expect(spectator.inject(AppsStore).loadCatalog).toHaveBeenCalled();
});
});

it('saves catalog updates and reloads catalog apps when form is saved', async () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
'Preferred Trains': ['stable', 'community'],
describe('has docker lacks nvidia drivers', () => {
describe('lacksNvidiaDrivers is true', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
mockProvider(DockerStore, {
nvidiaDriversInstalled$: of(false),
lacksNvidiaDrivers$: of(true),
setDockerNvidia: jest.fn(() => of(null)),
}),
],
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('shows Install NDIVIA Drivers checkbox when lacksNvidiaDrivers is true', async () => {
const form = await loader.getHarness(IxFormHarness);
const values = await form.getValues();

expect(values).toEqual({
'Install NDIVIA Drivers': false,
'Preferred Trains': ['test'],
});
});

it('saves catalog updates and nvidia settings', async () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
'Preferred Trains': ['stable'],
'Install NDIVIA Drivers': true,
});

const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await saveButton.click();

expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('catalog.update', [
{ preferred_trains: ['stable'] },
]);

expect(spectator.inject(DockerStore).setDockerNvidia).toHaveBeenCalled();
expect(spectator.inject(AppsStore).loadCatalog).toHaveBeenCalled();
});
});

const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await saveButton.click();
describe('lacksNvidiaDrivers is false and nvidiaDriversInstalled is true', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
mockProvider(DockerStore, {
nvidiaDriversInstalled$: of(true),
lacksNvidiaDrivers$: of(false),
}),
],
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('catalog.update', [
{ preferred_trains: ['stable', 'community'] },
]);
expect(spectator.inject(AppsStore).loadCatalog).toHaveBeenCalled();
it('shows Install NDIVIA Drivers checkbox when docker.lacks_nvidia_drivers is true OR when it is checked (so the user can uncheck it)', async () => {
const form = await loader.getHarness(IxFormHarness);
const values = await form.getValues();

expect(values).toEqual({
'Install NDIVIA Drivers': true,
'Preferred Trains': ['test'],
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,41 +1,53 @@
import {
ChangeDetectionStrategy, Component, OnInit, signal,
ChangeDetectionStrategy, Component, computed, OnInit, signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, Validators } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { switchMap } from 'rxjs';
import { of, switchMap } from 'rxjs';
import { Role } from 'app/enums/role.enum';
import { singleArrayToOptions } from 'app/helpers/operators/options.operators';
import { helptextApps } from 'app/helptext/apps/apps';
import { Catalog, CatalogUpdate } from 'app/interfaces/catalog.interface';
import { IxSlideInRef } from 'app/modules/forms/ix-forms/components/ix-slide-in/ix-slide-in-ref';
import { FormErrorHandlerService } from 'app/modules/forms/ix-forms/services/form-error-handler.service';
import { AppsStore } from 'app/pages/apps/store/apps-store.service';
import { DockerStore } from 'app/pages/apps/store/docker.store';
import { WebSocketService } from 'app/services/ws.service';

@UntilDestroy()
@Component({
selector: 'ix-catalog-settings',
templateUrl: './catalog-settings.component.html',
styleUrls: ['./catalog-settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CatalogSettingsComponent implements OnInit {
protected nvidiaDriversInstalled = toSignal(this.dockerStore.nvidiaDriversInstalled$);
protected lacksNvidiaDrivers = toSignal(this.dockerStore.lacksNvidiaDrivers$);
protected isFormLoading = signal(false);
protected readonly requiredRoles = [Role.AppsWrite, Role.CatalogWrite];

protected form = this.fb.group({
preferred_trains: [[] as string[], Validators.required],
nvidia: [null as boolean],
});

protected allTrains$ = this.ws.call('catalog.trains').pipe(
singleArrayToOptions(),
);

protected showNvidiaCheckbox = computed(() => {
return this.nvidiaDriversInstalled() || this.lacksNvidiaDrivers();
});

readonly tooltips = {
preferred_trains: helptextApps.catalogForm.preferredTrains.tooltip,
install_nvidia_driver: helptextApps.catalogForm.installNvidiaDriver.tooltip,
};

constructor(
protected dockerStore: DockerStore,
private ws: WebSocketService,
private slideInRef: IxSlideInRef<CatalogSettingsComponent>,
private errorHandler: FormErrorHandlerService,
Expand All @@ -57,14 +69,21 @@ export class CatalogSettingsComponent implements OnInit {
});
},
});

if (this.nvidiaDriversInstalled()) {
this.form.patchValue({
nvidia: this.nvidiaDriversInstalled(),
});
}
}

onSubmit(): void {
const { preferred_trains: preferredTrains } = this.form.value;
const { preferred_trains: preferredTrains, nvidia } = this.form.value;

this.isFormLoading.set(true);
this.ws.call('catalog.update', [{ preferred_trains: preferredTrains } as CatalogUpdate])
.pipe(
switchMap(() => (nvidia !== null ? this.dockerStore.setDockerNvidia(nvidia) : of(nvidia))),
switchMap(() => this.appsStore.loadCatalog()),
untilDestroyed(this),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[matMenuTriggerFor]="menu"
[ixUiSearch]="searchableElements.elements.settings"
>
{{ 'Settings' | translate }}
{{ 'Configuration' | translate }}
<ix-icon name="mdi-menu-down" class="menu-caret"></ix-icon>
</button>

Expand Down Expand Up @@ -37,6 +37,6 @@
ixTest="update-preferred-trains"
(click)="manageCatalog()"
>
{{ 'Train Settings' | translate }}
{{ 'Settings' | translate }}
</button>
</mat-menu>
Loading

1 comment on commit 8c16789

@timocapa
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good ol' NDIVIA drivers

Please sign in to comment.