diff --git a/components/crud-web-apps/common/backend/kubeflow/kubeflow/crud_backend/api/pod.py b/components/crud-web-apps/common/backend/kubeflow/kubeflow/crud_backend/api/pod.py index 57642d66..a5b71bf3 100644 --- a/components/crud-web-apps/common/backend/kubeflow/kubeflow/crud_backend/api/pod.py +++ b/components/crud-web-apps/common/backend/kubeflow/kubeflow/crud_backend/api/pod.py @@ -10,7 +10,7 @@ def list_pods(namespace, auth=True, label_selector = None): def get_pod_logs(namespace, pod, container, auth=True): if auth: - authz.ensure_authorized("read", "", "v1", "pods", namespace, "logs") + authz.ensure_authorized("get", "", "v1", "pods", namespace, "log") return v1_core.read_namespaced_pod_log( namespace=namespace, name=pod, container=container diff --git a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/public-api.ts b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/public-api.ts index 94b1247e..a87d0544 100644 --- a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/public-api.ts +++ b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/public-api.ts @@ -92,6 +92,9 @@ export * from './lib/logs-viewer/logs-viewer.component'; export * from './lib/help-popover/help-popover.component'; export * from './lib/help-popover/help-popover.module'; +export * from './lib/logs-viewer/logs-viewer.module'; +export * from './lib/logs-viewer/logs-viewer.component'; + export * from './lib/content-list-item/content-list-item.component'; export * from './lib/content-list-item/content-list-item.module'; diff --git a/components/crud-web-apps/jupyter/backend/apps/common/routes/get.py b/components/crud-web-apps/jupyter/backend/apps/common/routes/get.py index 97827ea5..479cf9e8 100644 --- a/components/crud-web-apps/jupyter/backend/apps/common/routes/get.py +++ b/components/crud-web-apps/jupyter/backend/apps/common/routes/get.py @@ -78,6 +78,15 @@ def get_notebook_pod(notebook_name, namespace): +@bp.route("/api/namespaces//notebooks//pod//logs") +def get_pod_logs(namespace, notebook_name, pod_name): + container = notebook_name + logs = api.get_pod_logs(namespace, pod_name, container) + return api.success_response( + "logs", logs.split("\n"), + ) + + @bp.route("/api/gpus") def get_gpu_vendors(): diff --git a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.html b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.html new file mode 100644 index 00000000..a2423b6f --- /dev/null +++ b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.html @@ -0,0 +1,18 @@ + + + + + No logs were found for this Notebook. + + Error: {{ loadErrorMsg }} + + + + + + diff --git a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.scss b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.scss new file mode 100644 index 00000000..053f94c3 --- /dev/null +++ b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.scss @@ -0,0 +1,9 @@ +lib-panel { + display: block; + margin-top: 1rem; +} + +.logs-viewer { + margin-bottom: 2rem; + margin-top: 1rem; +} diff --git a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.spec.ts b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.spec.ts new file mode 100644 index 00000000..588e7605 --- /dev/null +++ b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { LoadingSpinnerModule, PollerService } from 'kubeflow'; +import { JWABackendService } from 'src/app/services/backend.service'; +import { of } from 'rxjs'; + +import { LogsComponent } from './logs.component'; +const JWABackendServiceStub: Partial = { + getPodDefaults: () => of(), +}; +const PollerServiceStub: Partial = { + exponential: () => of(), +}; + +describe('LogsComponent', () => { + let component: LogsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LogsComponent], + providers: [ + { provide: JWABackendService, useValue: JWABackendServiceStub }, + { provide: PollerService, useValue: PollerServiceStub }, + ], + imports: [LoadingSpinnerModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LogsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.ts b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.ts new file mode 100644 index 00000000..3e65a2bc --- /dev/null +++ b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.component.ts @@ -0,0 +1,85 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { JWABackendService } from 'src/app/services/backend.service'; +import { Subscription } from 'rxjs'; +import { PollerService } from 'kubeflow'; +import { V1Pod } from '@kubernetes/client-node'; + +@Component({ + selector: 'app-logs', + templateUrl: './logs.component.html', + styleUrls: ['./logs.component.scss'], +}) +export class LogsComponent implements OnInit, OnDestroy { + public goToBottom = true; + public logs: string[]; + public logsRequestCompleted = false; + public loadErrorMsg = ''; + + private pollingSub: Subscription; + private prvPod: V1Pod; + + @Input() podRequestCompleted = false; + + @Input() + set pod(pod: V1Pod) { + this.prvPod = pod; + + if (!pod) { + this.logs = null; + this.logsRequestCompleted = true; + return; + } + + this.poll(pod); + } + get pod() { + return this.prvPod; + } + + private poll(pod: V1Pod) { + if (this.pollingSub) { + this.pollingSub.unsubscribe(); + } + + const request = this.backend.getPodLogs(pod); + this.pollingSub = this.poller.exponential(request).subscribe( + logs => { + this.logs = logs; + this.logsRequestCompleted = true; + this.loadErrorMsg = ''; + }, + error => { + this.logs = null; + this.logsRequestCompleted = true; + this.loadErrorMsg = error; + }, + ); + } + + get logsLoading(): boolean { + if (!this.podRequestCompleted) { + return true; + } else if (this.podRequestCompleted && !this.logsRequestCompleted) { + return true; + } else { + return false; + } + } + + get logsEmpty(): boolean { + return this.logs ? false : true; + } + + constructor( + public backend: JWABackendService, + public poller: PollerService, + ) {} + + ngOnInit() {} + + ngOnDestroy() { + if (this.pollingSub) { + this.pollingSub.unsubscribe(); + } + } +} diff --git a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.module.ts b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.module.ts new file mode 100644 index 00000000..52930929 --- /dev/null +++ b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/logs/logs.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LogsComponent } from './logs.component'; +import { + HeadingSubheadingRowModule, + KubeflowModule, + LogsViewerModule, +} from 'kubeflow'; + +@NgModule({ + declarations: [LogsComponent], + imports: [ + CommonModule, + KubeflowModule, + HeadingSubheadingRowModule, + LogsViewerModule, + ], + exports: [LogsComponent], +}) +export class LogsModule {} diff --git a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/notebook-page.component.html b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/notebook-page.component.html index a4275bed..758821b5 100644 --- a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/notebook-page.component.html +++ b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/notebook-page.component.html @@ -32,6 +32,16 @@ > + + + + + + + diff --git a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/notebook-page.module.ts b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/notebook-page.module.ts index a4fb934e..027627f3 100644 --- a/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/notebook-page.module.ts +++ b/components/crud-web-apps/jupyter/frontend/src/app/pages/notebook-page/notebook-page.module.ts @@ -7,6 +7,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatDividerModule } from '@angular/material/divider'; import { NotebookPageComponent } from './notebook-page.component'; import { OverviewModule } from './overview/overview.module'; +import { LogsModule } from './logs/logs.module'; import { RouterModule } from '@angular/router'; @NgModule({ @@ -19,6 +20,7 @@ import { RouterModule } from '@angular/router'; MatTabsModule, OverviewModule, MatProgressSpinnerModule, + LogsModule, RouterModule, ], }) diff --git a/components/crud-web-apps/jupyter/frontend/src/app/services/backend.service.ts b/components/crud-web-apps/jupyter/frontend/src/app/services/backend.service.ts index 9217049e..22d6e906 100644 --- a/components/crud-web-apps/jupyter/frontend/src/app/services/backend.service.ts +++ b/components/crud-web-apps/jupyter/frontend/src/app/services/backend.service.ts @@ -76,6 +76,17 @@ export class JWABackendService extends BackendService { ); } + public getPodLogs(pod: V1Pod): Observable { + const namespace = pod.metadata.namespace; + const notebookName = pod.metadata.labels['notebook-name']; + const podName = pod.metadata.name; + const url = `api/namespaces/${namespace}/notebooks/${notebookName}/pod/${podName}/logs`; + return this.http.get(url).pipe( + catchError(error => this.handleErrorExtended(error, [404, 400])), + map((resp: JWABackendResponse) => resp.logs), + ); + } + public getConfig(): Observable { const url = `api/config`; diff --git a/components/crud-web-apps/jupyter/frontend/src/app/types/responses.ts b/components/crud-web-apps/jupyter/frontend/src/app/types/responses.ts index 292a748b..a59d667a 100644 --- a/components/crud-web-apps/jupyter/frontend/src/app/types/responses.ts +++ b/components/crud-web-apps/jupyter/frontend/src/app/types/responses.ts @@ -8,6 +8,7 @@ import { PvcResponseObject } from './volume'; export interface JWABackendResponse extends BackendResponse { notebook?: NotebookRawObject; notebooks?: NotebookResponseObject[]; + logs: string[]; pvcs?: PvcResponseObject[]; config?: Config; poddefaults?: PodDefault[];