Skip to content
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

NAS-132131 / 25.04 / Add docker nvidia status #10983

Merged
merged 5 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/app/enums/docker-nvidia-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { marker as T } from '@biesbjerg/ngx-translate-extract-marker';

export enum DockerNvidiaStatus {
Absent = 'ABSENT',
Installed = 'INSTALLED',
Installing = 'INSTALLING',
InstallError = 'INSTALL_ERROR',
NotInstalled = 'NOT_INSTALLED',
}

export const dockerNvidiaStatusLabels = new Map<DockerNvidiaStatus, string>([
[DockerNvidiaStatus.Absent, T('Absent')],
[DockerNvidiaStatus.Installed, T('Installed')],
[DockerNvidiaStatus.Installing, T('Installing')],
[DockerNvidiaStatus.InstallError, T('Error Installing')],
[DockerNvidiaStatus.NotInstalled, T('Not Installed')],
]);

export interface DockerNvidiaStatusResponse {
status: DockerNvidiaStatus;
}
3 changes: 2 additions & 1 deletion src/app/interfaces/api/api-call-directory.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AlertPolicy } from 'app/enums/alert-policy.enum';
import { DatasetRecordSize, DatasetType } from 'app/enums/dataset.enum';
import { DeviceType } from 'app/enums/device-type.enum';
import { DockerConfig, DockerStatusData } from 'app/enums/docker-config.interface';
import { DockerNvidiaStatusResponse } from 'app/enums/docker-nvidia-status.enum';
import { FailoverDisabledReason } from 'app/enums/failover-disabled-reason.enum';
import { FailoverStatus } from 'app/enums/failover-status.enum';
import { OnOff } from 'app/enums/on-off.enum';
Expand Down Expand Up @@ -582,7 +583,7 @@ export interface ApiCallDirectory {
// Docker
'docker.config': { params: void; response: DockerConfig };
'docker.status': { params: void; response: DockerStatusData };
'docker.lacks_nvidia_drivers': { params: void; response: boolean };
'docker.nvidia_status': { params: void; response: DockerNvidiaStatusResponse };

// LDAP
'ldap.config': { params: void; response: LdapConfig };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
mat-hint {
color: var(--fg2) !important;
margin-left: 30px;
margin-top: -8px;
margin-top: 0;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
formControlName="nvidia"
[label]="'Install NVIDIA Drivers' | translate"
[tooltip]="tooltips.install_nvidia_driver| translate"
[hint]="'Current status: {status}' | translate: { status: dockerNvidiaStatus() | mapValue: dockerNvidiaStatusLabels }"
></ix-checkbox>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { fakeSuccessfulJob } from 'app/core/testing/utils/fake-job.utils';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { mockCall, mockJob, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { DockerConfig } from 'app/enums/docker-config.interface';
import { DockerNvidiaStatus } from 'app/enums/docker-nvidia-status.enum';
import { CatalogConfig } 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';
Expand All @@ -23,9 +24,10 @@ 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', () => {
describe('AppsSettingsComponent', () => {
let spectator: Spectator<AppsSettingsComponent>;
let loader: HarnessLoader;

const dockerConfig = {
address_pools: [
{ base: '172.17.0.0/12', size: 12 },
Expand Down Expand Up @@ -63,15 +65,17 @@ describe('CatalogEditFormComponent', () => {
],
});

describe('no docker lacks nvidia drivers', () => {
describe('system has no nvidia card', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
mockProvider(DockerStore, {
nvidiaDriversInstalled$: of(false),
lacksNvidiaDrivers$: of(false),
hasNvidiaCard$: of(false),
dockerConfig$: of(dockerConfig),
dockerNvidiaStatus$: of(DockerNvidiaStatus.NotInstalled),
reloadDockerConfig: jest.fn(() => of({})),
reloadDockerNvidiaStatus: jest.fn(() => of({})),
}),
],
});
Expand Down Expand Up @@ -114,24 +118,26 @@ describe('CatalogEditFormComponent', () => {
});
});

describe('has docker lacks nvidia drivers', () => {
describe('lacksNvidiaDrivers is true', () => {
describe('has docker no nvidia drivers', () => {
describe('hasNvidiaCard is true', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
mockProvider(DockerStore, {
nvidiaDriversInstalled$: of(false),
lacksNvidiaDrivers$: of(true),
setDockerNvidia: jest.fn(() => of(null)),
hasNvidiaCard$: of(true),
dockerConfig$: of(dockerConfig),
dockerNvidiaStatus$: of(DockerNvidiaStatus.NotInstalled),
setDockerNvidia: jest.fn(() => of(null)),
reloadDockerConfig: jest.fn(() => of({})),
reloadDockerNvidiaStatus: jest.fn(() => of({})),
}),
],
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

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

Expand Down Expand Up @@ -160,22 +166,24 @@ describe('CatalogEditFormComponent', () => {
});
});

describe('lacksNvidiaDrivers is false and nvidiaDriversInstalled is true', () => {
describe('hasNvidiaCard is false and nvidiaDriversInstalled is true', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
mockProvider(DockerStore, {
nvidiaDriversInstalled$: of(true),
lacksNvidiaDrivers$: of(false),
hasNvidiaCard$: of(false),
dockerConfig$: of(dockerConfig),
dockerNvidiaStatus$: of(DockerNvidiaStatus.Installed),
reloadDockerConfig: jest.fn(() => of({})),
reloadDockerNvidiaStatus: jest.fn(() => of({})),
}),
],
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('shows Install NVIDIA Drivers checkbox when docker.lacks_nvidia_drivers is true OR when it is checked (so the user can uncheck it)', async () => {
it('shows Install NVIDIA Drivers checkbox when docker.nvidia_status is not Absent OR when it is checked (so the user can uncheck it)', async () => {
const form = await loader.getHarness(IxFormHarness);
const values = await form.getValues();

Expand All @@ -192,9 +200,11 @@ describe('CatalogEditFormComponent', () => {
providers: [
mockProvider(DockerStore, {
nvidiaDriversInstalled$: of(true),
lacksNvidiaDrivers$: of(false),
hasNvidiaCard$: of(true),
dockerConfig$: of(dockerConfig),
dockerNvidiaStatus$: of(DockerNvidiaStatus.Installed),
reloadDockerConfig: jest.fn(() => of({})),
reloadDockerNvidiaStatus: jest.fn(() => of({})),
setDockerNvidia: jest.fn(() => of(null)),
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
take,
} from 'rxjs';
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive';
import { dockerNvidiaStatusLabels } from 'app/enums/docker-nvidia-status.enum';
import { Role } from 'app/enums/role.enum';
import { singleArrayToOptions } from 'app/helpers/operators/options.operators';
import { helptextApps } from 'app/helptext/apps/apps';
Expand All @@ -28,6 +29,7 @@ import { IxListItemComponent } from 'app/modules/forms/ix-forms/components/ix-li
import { IxListComponent } from 'app/modules/forms/ix-forms/components/ix-list/ix-list.component';
import { FormErrorHandlerService } from 'app/modules/forms/ix-forms/services/form-error-handler.service';
import { ipv4or6cidrValidator } from 'app/modules/forms/ix-forms/validators/ip-validation';
import { MapValuePipe } from 'app/modules/pipes/map-value/map-value.pipe';
import { ModalHeaderComponent } from 'app/modules/slide-ins/components/modal-header/modal-header.component';
import { SlideInRef } from 'app/modules/slide-ins/slide-in-ref';
import { TestDirective } from 'app/modules/test-id/test.directive';
Expand Down Expand Up @@ -59,13 +61,16 @@ import { WebSocketService } from 'app/services/ws.service';
RequiresRolesDirective,
TestDirective,
TranslateModule,
MapValuePipe,
],
})
export class AppsSettingsComponent implements OnInit {
protected hasNvidiaCard = toSignal(this.dockerStore.hasNvidiaCard$);
protected nvidiaDriversInstalled = toSignal(this.dockerStore.nvidiaDriversInstalled$);
protected lacksNvidiaDrivers = toSignal(this.dockerStore.lacksNvidiaDrivers$);
protected dockerNvidiaStatus = toSignal(this.dockerStore.dockerNvidiaStatus$);
protected isFormLoading = signal(false);
protected readonly requiredRoles = [Role.AppsWrite, Role.CatalogWrite];
protected readonly dockerNvidiaStatusLabels = dockerNvidiaStatusLabels;

protected form = this.fb.group({
preferred_trains: [[] as string[], Validators.required],
Expand All @@ -81,9 +86,7 @@ export class AppsSettingsComponent implements OnInit {
singleArrayToOptions(),
);

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

readonly tooltips = {
preferred_trains: helptextApps.catalogForm.preferredTrains.tooltip,
Expand Down Expand Up @@ -156,6 +159,7 @@ export class AppsSettingsComponent implements OnInit {
switchMap(() => (values.nvidia !== null ? this.dockerStore.setDockerNvidia(values.nvidia) : of(values.nvidia))),
switchMap(() => forkJoin([
this.dockerStore.reloadDockerConfig(),
this.dockerStore.reloadDockerNvidiaStatus(),
this.appsStore.loadCatalog(),
])),
untilDestroyed(this),
Expand Down
22 changes: 19 additions & 3 deletions src/app/pages/apps/store/docker.store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { MockWebSocketService } from 'app/core/testing/classes/mock-websocket.service';
import { mockCall, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { DockerConfig } from 'app/enums/docker-config.interface';
import { DockerNvidiaStatus } from 'app/enums/docker-nvidia-status.enum';
import { DockerStatus } from 'app/enums/docker-status.enum';
import { DockerStore } from 'app/pages/apps/store/docker.store';
import { WebSocketService } from 'app/services/ws.service';
Expand All @@ -17,7 +18,7 @@ describe('DockerStore', () => {
pool: 'pewl',
nvidia: true,
} as DockerConfig),
mockCall('docker.lacks_nvidia_drivers', true),
mockCall('docker.nvidia_status', { status: DockerNvidiaStatus.Installed }),
mockCall('docker.status', {
status: DockerStatus.Running,
description: 'Docker is running',
Expand All @@ -35,7 +36,7 @@ describe('DockerStore', () => {
spectator.service.initialize();

expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('docker.config');
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('docker.lacks_nvidia_drivers');
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('docker.nvidia_status');
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('docker.status');

expect(spectator.service.state()).toEqual({
Expand All @@ -45,8 +46,8 @@ describe('DockerStore', () => {
pool: 'pewl',
},
isLoading: false,
lacksNvidiaDrivers: true,
nvidiaDriversInstalled: true,
nvidiaStatus: DockerNvidiaStatus.Installed,
statusData: {
description: 'Docker is running',
status: DockerStatus.Running,
Expand All @@ -67,9 +68,24 @@ describe('DockerStore', () => {
mockWebsocket.mockCall('docker.config', newDockerConfig);

spectator.service.reloadDockerConfig().subscribe();
spectator.service.reloadDockerNvidiaStatus().subscribe();

expect(mockWebsocket.call).toHaveBeenCalledWith('docker.config');
expect(spectator.service.state().dockerConfig).toEqual(newDockerConfig);
});
});

describe('reloadDockerNvidiaStatus', () => {
it('reloads docker nvidia status and updates the state', () => {
const mockWebsocket = spectator.inject(MockWebSocketService);
jest.resetAllMocks();
mockWebsocket.mockCall('docker.nvidia_status', { status: DockerNvidiaStatus.Installed });

spectator.service.reloadDockerConfig().subscribe();
spectator.service.reloadDockerNvidiaStatus().subscribe();

expect(mockWebsocket.call).toHaveBeenCalledWith('docker.nvidia_status');
expect(spectator.service.state().nvidiaStatus).toEqual(DockerNvidiaStatus.Installed);
});
});
});
36 changes: 20 additions & 16 deletions src/app/pages/apps/store/docker.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
forkJoin, map, Observable, switchMap, tap,
} from 'rxjs';
import { DockerConfig, DockerStatusData } from 'app/enums/docker-config.interface';
import { DockerNvidiaStatus } from 'app/enums/docker-nvidia-status.enum';
import { DockerStatus } from 'app/enums/docker-status.enum';
import { JobState } from 'app/enums/job-state.enum';
import { Job } from 'app/interfaces/job.interface';
Expand All @@ -17,15 +18,15 @@ export interface DockerConfigState {
isLoading: boolean;
dockerConfig: DockerConfig;
nvidiaDriversInstalled: boolean;
lacksNvidiaDrivers: boolean;
nvidiaStatus: DockerNvidiaStatus;
statusData: DockerStatusData;
}

const initialState: DockerConfigState = {
isLoading: false,
dockerConfig: null,
nvidiaDriversInstalled: false,
lacksNvidiaDrivers: false,
nvidiaStatus: null,
statusData: {
status: null,
description: null,
Expand All @@ -38,7 +39,8 @@ export class DockerStore extends ComponentStore<DockerConfigState> {
readonly dockerConfig$ = this.select((state) => state.dockerConfig);
readonly selectedPool$ = this.select((state) => state.dockerConfig?.pool || null);
readonly nvidiaDriversInstalled$ = this.select((state) => state.nvidiaDriversInstalled);
readonly lacksNvidiaDrivers$ = this.select((state) => state.lacksNvidiaDrivers);
readonly hasNvidiaCard$ = this.select((state) => state.nvidiaStatus !== DockerNvidiaStatus.Absent);
readonly dockerNvidiaStatus$ = this.select((state) => state.nvidiaStatus);
readonly isDockerStarted$ = this.select((state) => {
return state.statusData.status == null ? null : DockerStatus.Running === state.statusData.status;
});
Expand All @@ -57,22 +59,18 @@ export class DockerStore extends ComponentStore<DockerConfigState> {

initialize = this.effect((trigger$: Observable<void>) => {
return trigger$.pipe(
tap(() => {
this.patchState({
isLoading: true,
});
}),
tap(() => this.patchState({ isLoading: true })),
switchMap(() => forkJoin([
this.getDockerConfig(),
this.getDockerStatus(),
this.getLacksNvidiaDrivers(),
this.getDockerNvidiaStatus(),
])),
tap(
([dockerConfig, statusData, lacksNvidiaDrivers]: [DockerConfig, DockerStatusData, boolean]) => {
([dockerConfig, statusData, nvidiaStatus]: [DockerConfig, DockerStatusData, DockerNvidiaStatus]) => {
this.patchState({
dockerConfig,
nvidiaDriversInstalled: dockerConfig.nvidia,
lacksNvidiaDrivers,
nvidiaStatus,
statusData,
isLoading: false,
});
Expand All @@ -85,8 +83,8 @@ export class DockerStore extends ComponentStore<DockerConfigState> {
return this.ws.call('docker.config');
}

private getLacksNvidiaDrivers(): Observable<boolean> {
return this.ws.call('docker.lacks_nvidia_drivers');
private getDockerNvidiaStatus(): Observable<DockerNvidiaStatus> {
return this.ws.call('docker.nvidia_status').pipe(map(({ status }) => status));
}

private getDockerStatus(): Observable<DockerStatusData> {
Expand All @@ -110,6 +108,14 @@ export class DockerStore extends ComponentStore<DockerConfigState> {
);
}

reloadDockerNvidiaStatus(): Observable<DockerNvidiaStatus> {
return this.getDockerNvidiaStatus().pipe(
tap((nvidiaStatus) => {
this.patchState({ nvidiaStatus });
}),
);
}

setDockerNvidia(nvidiaDriversInstalled: boolean): Observable<Job<DockerConfig>> {
return this.dialogService.jobDialog(
this.ws.job('docker.update', [{ nvidia: nvidiaDriversInstalled }]),
Expand All @@ -119,9 +125,7 @@ export class DockerStore extends ComponentStore<DockerConfigState> {
.pipe(
tap((job) => {
if (job.state === JobState.Success) {
this.patchState({
nvidiaDriversInstalled,
});
this.patchState({ nvidiaDriversInstalled });
}
}),
this.errorHandler.catchError(),
Expand Down
Loading
Loading