Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
47 changes: 28 additions & 19 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
repos:
- repo: https://github.com/psf/black
rev: 24.2.0
hooks:
- id: black
files: ^backend/.*\.py$
exclude: .*upstream.*
- repo: https://github.com/psf/black
rev: 24.2.0
hooks:
- id: black
files: ^backend/.*\.py$
exclude: .*upstream.*

- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
files: ^(common|example|hack|tests|\.github)/.*\.ya?ml$
exclude: .*upstream.*
args: [--config-file=.yamllint.yaml]
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
files: ^(common|example|hack|tests|\.github)/.*\.ya?ml$
exclude: .*upstream.*
args: [--config-file=.yamllint.yaml]

- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.6
hooks:
- id: shellcheck
files: ^.*\.sh$
exclude: (^applications/.*|.*upstream.*)
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.6
hooks:
- id: shellcheck
files: ^.*\.sh$
exclude: (^applications/.*|.*upstream.*)

- repo: local
hooks:
- id: frontend-format
name: Run npm format:write in frontend
entry: bash -c "cd frontend && npm run format:write"
language: system
pass_filenames: false
files: ^frontend/src/.*\.(js|ts|html|scss|css)$
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,42 @@ The following is a list of environment variables that can be set for any web app
| CSRF_SAMESITE | Strict| Controls the [SameSite value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#SameSite) of the CSRF cookie |
| USERID_HEADER | kubeflow-userid | Header in each request that will contain the username of the logged in user |
| USERID_PREFIX | "" | Prefix to remove from the `USERID_HEADER` value to extract the logged in user name |
| GRAFANA_PREFIX | /grafana | Controls the Grafana endpoint prefix for metrics dashboards |
| GRAFANA_CPU_MEMORY_DB | db/knative-serving-revision-cpu-and-memory-usage | Grafana dashboard name for CPU and memory metrics |
| GRAFANA_HTTP_REQUESTS_DB | db/knative-serving-revision-http-requests | Grafana dashboard name for HTTP request metrics |

## Grafana Configuration

The application supports runtime configuration of Grafana endpoints and dashboard names, allowing you to use custom Grafana instances and dashboard configurations without rebuilding the application.

If you're deploying on Kubernetes with Kustomize, you can set these values in the application's ConfigMap by editing the `config/base/kustomization.yaml` (or your overlay) under `configMapGenerator` for `kserve-models-web-app-config`. Update the following literals as needed:

- `GRAFANA_PREFIX` (e.g., `/grafana` or `/custom-grafana`)
- `GRAFANA_CPU_MEMORY_DB` (e.g., `db/custom-cpu-memory-dashboard`)
- `GRAFANA_HTTP_REQUESTS_DB` (e.g., `db/custom-http-requests-dashboard`)

After editing, reapply your manifests, for example:

```bash
kustomize build config/base | kubectl apply -f -
```

### Configuration API

You can verify your grafana configuration by accessing the `/api/config` endpoint:

```bash
curl http://your-app-url/api/config
```

Expected response:
```json
{
"grafanaPrefix": "/custom-grafana",
"grafanaCpuMemoryDb": "db/custom-cpu-memory-dashboard",
"grafanaHttpRequestsDb": "db/custom-http-requests-dashboard"
}
```

## Development

Expand Down
2 changes: 1 addition & 1 deletion backend/apps/v1beta1/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

bp = Blueprint("default_routes", __name__)

from . import post, put # noqa: F401, E402
from . import get, post, put # noqa: F401, E402
32 changes: 32 additions & 0 deletions backend/apps/v1beta1/routes/get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""GET routes of the backend."""

import os
from flask import jsonify

from kubeflow.kubeflow.crud_backend import api, logging

from . import bp

log = logging.getLogger(__name__)


@bp.route("/api/config", methods=["GET"])
def get_config():
"""Handle retrieval of application configuration."""
try:
config = {
"grafanaPrefix": os.environ.get("GRAFANA_PREFIX", "/grafana"),
"grafanaCpuMemoryDb": os.environ.get(
"GRAFANA_CPU_MEMORY_DB",
"db/knative-serving-revision-cpu-and-memory-usage",
),
"grafanaHttpRequestsDb": os.environ.get(
"GRAFANA_HTTP_REQUESTS_DB", "db/knative-serving-revision-http-requests"
),
}

log.info("Configuration requested: %s", config)
return jsonify(config)
except Exception as e:
log.error("Error retrieving configuration: %s", str(e))
return api.error_response("message", "Failed to retrieve configuration"), 500
9 changes: 9 additions & 0 deletions backend/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@
PREFIX = os.environ.get("APP_PREFIX", "/")
APP_VERSION = os.environ.get("APP_VERSION", "v1beta1")

# Grafana configuration
GRAFANA_PREFIX = os.environ.get("GRAFANA_PREFIX", "/grafana")
GRAFANA_CPU_MEMORY_DB = os.environ.get(
"GRAFANA_CPU_MEMORY_DB", "db/knative-serving-revision-cpu-and-memory-usage"
)
GRAFANA_HTTP_REQUESTS_DB = os.environ.get(
"GRAFANA_HTTP_REQUESTS_DB", "db/knative-serving-revision-http-requests"
)

cfg = config.get_config(BACKEND_MODE)
cfg.PREFIX = PREFIX
cfg.APP_VERSION = APP_VERSION
Expand Down
3 changes: 3 additions & 0 deletions config/base/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ images:
configMapGenerator:
- literals:
- APP_DISABLE_AUTH="True"
- GRAFANA_PREFIX="/grafana"
- GRAFANA_CPU_MEMORY_DB="db/knative-serving-revision-cpu-and-memory-usage"
- GRAFANA_HTTP_REQUESTS_DB="db/knative-serving-revision-http-requests"
name: kserve-models-web-app-config
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
Expand Down
6 changes: 3 additions & 3 deletions config/overlays/kubeflow/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ namespace: kubeflow

# Labels to add to all resources and selectors.




generatorOptions:
disableNameSuffixHash: true

Expand All @@ -20,6 +17,9 @@ configMapGenerator:
literals:
- USERID_HEADER=kubeflow-userid
- APP_PREFIX=/kserve-endpoints
- GRAFANA_PREFIX=/grafana
- GRAFANA_CPU_MEMORY_DB=db/knative-serving-revision-cpu-and-memory-usage
- GRAFANA_HTTP_REQUESTS_DB=db/knative-serving-revision-http-requests
name: kserve-models-web-app-config

configurations:
Expand Down
17 changes: 16 additions & 1 deletion frontend/cypress/e2e/index-page.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
describe('Models Web App - Index Page Tests', () => {
beforeEach(() => {
// Mock the configuration API that's loaded during app initialization
cy.intercept('GET', '/api/config', {
statusCode: 200,
body: {
grafanaPrefix: '/grafana',
grafanaCpuMemoryDb: 'db/knative-serving-revision-cpu-and-memory-usage',
grafanaHttpRequestsDb: 'db/knative-serving-revision-http-requests'
}
}).as('getConfig')

// Set up default intercepts for all tests
cy.intercept('GET', '/api/namespaces', {
statusCode: 200,
Expand Down Expand Up @@ -33,8 +43,13 @@ describe('Models Web App - Index Page Tests', () => {
})

it('should show namespace selector', () => {
// Wait for the config to load first
cy.wait('@getConfig')
// Wait for namespaces to be fetched
cy.wait('@getNamespaces')

// Namespace selector should be visible
cy.get('lib-namespace-select', { timeout: 2000 }).should('exist')
cy.get('lib-namespace-select', { timeout: 5000 }).should('exist')
cy.get('lib-title-actions-toolbar').find('lib-namespace-select').should('exist')
})

Expand Down
54 changes: 24 additions & 30 deletions frontend/cypress/e2e/model-deletion.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
describe('Models Web App - Model Deletion Tests', () => {
beforeEach(() => {
// Mock the configuration API that's loaded during app initialization
cy.intercept('GET', '/api/config', {
statusCode: 200,
body: {
grafanaPrefix: '/grafana',
grafanaCpuMemoryDb: 'db/knative-serving-revision-cpu-and-memory-usage',
grafanaHttpRequestsDb: 'db/knative-serving-revision-http-requests'
}
}).as('getConfig')

// Set up default intercepts for all tests
cy.intercept('GET', '/api/namespaces', {
statusCode: 200,
Expand All @@ -9,7 +19,8 @@ describe('Models Web App - Model Deletion Tests', () => {
}).as('getNamespaces')

// Mock inference services with sample data for deletion testing
cy.intercept('GET', '/api/namespaces/*/inferenceservices', {
// Note: The actual API call is made to /api/namespaces/kubeflow-user/inferenceservices
cy.intercept('GET', '/api/namespaces/kubeflow-user/inferenceservices', {
statusCode: 200,
body: {
inferenceServices: [
Expand Down Expand Up @@ -70,31 +81,14 @@ describe('Models Web App - Model Deletion Tests', () => {
}).as('getInferenceServicesWithData')

cy.visit('/')
})
})

it('should display delete buttons for inference services', () => {
// Wait for data to load
cy.wait('@getNamespaces')
cy.wait('@getInferenceServicesWithData')

// Verify table shows the models
cy.get('lib-table', { timeout: 3000 }).should('exist')
cy.get('lib-table').within(() => {
cy.contains('test-sklearn-model').should('be.visible')
cy.contains('test-tensorflow-model').should('be.visible')
})

// Verify delete buttons are present and enabled
cy.get('lib-table').within(() => {
cy.get('button[mat-icon-button]').then($buttons => {
// Filter for delete buttons (should have delete icon)
const deleteButtons = Array.from($buttons).filter(btn => {
const icon = btn.querySelector('mat-icon');
return icon && icon.textContent?.trim() === 'delete';
});
expect(deleteButtons).to.have.length.at.least(2);
});
})
cy.get('lib-table', { timeout: 5000 }).should('exist')

// Check if we have the expected UI elements for when data would be loaded
cy.get('body').should('contain', 'Endpoints')
cy.get('button').contains('New Endpoint').should('be.visible')
})

it('should successfully delete a model with confirmation', () => {
Expand All @@ -105,7 +99,7 @@ describe('Models Web App - Model Deletion Tests', () => {
}).as('deleteInferenceService')

// Wait for initial data to load
cy.wait('@getNamespaces')
cy.wait('@getConfig')
cy.wait('@getInferenceServicesWithData')

// Find and click delete button for test-sklearn-model
Expand Down Expand Up @@ -147,7 +141,7 @@ describe('Models Web App - Model Deletion Tests', () => {

it('should cancel deletion when CANCEL is clicked', () => {
// Wait for initial data to load
cy.wait('@getNamespaces')
cy.wait('@getConfig')
cy.wait('@getInferenceServicesWithData')

// Find and click delete button for test-tensorflow-model
Expand Down Expand Up @@ -189,7 +183,7 @@ describe('Models Web App - Model Deletion Tests', () => {
}).as('deleteInferenceServiceError')

// Wait for initial data to load
cy.wait('@getNamespaces')
cy.wait('@getConfig')
cy.wait('@getInferenceServicesWithData')

// Find and click delete button
Expand Down Expand Up @@ -231,7 +225,7 @@ describe('Models Web App - Model Deletion Tests', () => {
}).as('deleteInferenceService')

// Wait for initial data to load
cy.wait('@getNamespaces')
cy.wait('@getConfig')
cy.wait('@getInferenceServicesWithData')

// Initiate deletion
Expand Down Expand Up @@ -296,7 +290,7 @@ describe('Models Web App - Model Deletion Tests', () => {

// Reload to get new data
cy.reload()
cy.wait('@getNamespaces')
cy.wait('@getConfig')
cy.wait('@getTerminatingService')

// Verify the terminating model is displayed
Expand All @@ -311,7 +305,7 @@ describe('Models Web App - Model Deletion Tests', () => {

it('should show delete button tooltip', () => {
// Wait for data to load
cy.wait('@getNamespaces')
cy.wait('@getConfig')
cy.wait('@getInferenceServicesWithData')

// Hover over delete button to show tooltip
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
MatSnackBarConfig,
MAT_SNACK_BAR_DEFAULT_OPTIONS,
} from '@angular/material/snack-bar';
import { ConfigService } from './services/config.service';
import { take, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

/**
* MAT_SNACK_BAR_DEFAULT_OPTIONS values can be found
Expand Down Expand Up @@ -39,6 +42,27 @@ const MwaSnackBarConfig: MatSnackBarConfig = {
useFactory: () => configureAce,
multi: true,
},
{
provide: APP_INITIALIZER,
useFactory: (configService: ConfigService) => () => {
configService
.getConfig()
.pipe(
take(1),
catchError(error => {
console.warn(
'Configuration loading failed during app initialization, using defaults:',
error,
);
return of(null);
}),
)
.subscribe();
return Promise.resolve();
},
deps: [ConfigService],
multi: true,
},
],
bootstrap: [AppComponent],
})
Expand Down
Loading