diff --git a/.github/workflows/linting_bash_python_yaml_files.yaml b/.github/workflows/linting_bash_python_yaml_files.yaml index f64419ce..ccb96859 100644 --- a/.github/workflows/linting_bash_python_yaml_files.yaml +++ b/.github/workflows/linting_bash_python_yaml_files.yaml @@ -131,7 +131,7 @@ jobs: fi - name: Display changed files - if: always() # Always run this step + if: always() # Always run this step run: cat changed_files_in_PR.txt || echo "No bash files have changed in this PR." - name: Run ShellCheck on changed files diff --git a/.github/workflows/oci-release.yml b/.github/workflows/oci-release.yml index f5b6549d..24886ef5 100644 --- a/.github/workflows/oci-release.yml +++ b/.github/workflows/oci-release.yml @@ -4,12 +4,12 @@ on: push: # Publish `master` as `latest` OCI image. branches: - - master - - v* + - master + - v* # Publish `v1.2.3` tags as releases. tags: - - v* + - v* # Run tests for any PRs. pull_request: @@ -22,11 +22,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Run tests - run: | - make docker-build + - name: Run tests + run: | + make docker-build push: needs: test @@ -38,46 +38,46 @@ jobs: contents: read steps: - - name: Check out code - uses: actions/checkout@v4 + - name: Check out code + uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: amd64,ppc64le,arm64 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: amd64,ppc64le,arm64 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Log into GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Log into GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for OCI image - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=schedule - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=sha - # Add 'latest' tag for master branch pushes - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} + - name: Extract metadata (tags, labels) for OCI image + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=sha + # Add 'latest' tag for master branch pushes + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} - - name: Build and push multi-architecture OCI image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - platforms: ${{ env.PLATFORMS }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Build and push multi-architecture OCI image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: ${{ env.PLATFORMS }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 87a49432..6208ff02 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -7,7 +7,7 @@ name: Mark stale issues and pull requests on: schedule: - - cron: '0 0 * * *' # Run every day at midnight + - cron: '0 0 * * *' # Run every day at midnight workflow_dispatch: jobs: diff --git a/.github/workflows/test-node.yaml b/.github/workflows/test-node.yaml index 29beea62..c403f676 100644 --- a/.github/workflows/test-node.yaml +++ b/.github/workflows/test-node.yaml @@ -2,103 +2,103 @@ name: Frontend Test on: pull_request: paths: - - frontend/** + - frontend/** jobs: frontend-format-linting-check: name: Code format and lint runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 16 - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - name: Format code - run: | - npm install prettier@2.8.8 --prefix ./frontend - make prettier-check - - name: Lint code - run: | - cd frontend - npm ci --no-audit - npm run lint-check + - name: Check out code + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 16 + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + - name: Format code + run: | + npm install prettier@2.8.8 --prefix ./frontend + make prettier-check + - name: Lint code + run: | + cd frontend + npm ci --no-audit + npm run lint-check frontend-unit-tests: - name: Frontend Unit Tests - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 16 - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - name: Fetch Kubeflow and Build Common Library - run: | - COMMIT=$(cat frontend/COMMIT) - cd /tmp && git clone https://github.com/kubeflow/kubeflow.git - cd kubeflow - git checkout $COMMIT - cd components/crud-web-apps/common/frontend/kubeflow-common-lib - npm ci --no-audit - npm run build - npm link ./dist/kubeflow - - name: Install Frontend Dependencies and Setup Styles - run: | - cd frontend - npm ci --no-audit - npm link kubeflow - # Copy styles from kubeflow source to local styles directory - mkdir -p ./src/styles/ - cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/styles/* ./src/styles/ - # Also copy to node_modules for the copyCSS script - mkdir -p ./node_modules/kubeflow/styles/ - cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/styles/* ./node_modules/kubeflow/styles/ - - name: Run Unit Tests - run: | - cd frontend - npm run test:jest + name: Frontend Unit Tests + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 16 + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + - name: Fetch Kubeflow and Build Common Library + run: | + COMMIT=$(cat frontend/COMMIT) + cd /tmp && git clone https://github.com/kubeflow/kubeflow.git + cd kubeflow + git checkout $COMMIT + cd components/crud-web-apps/common/frontend/kubeflow-common-lib + npm ci --no-audit + npm run build + npm link ./dist/kubeflow + - name: Install Frontend Dependencies and Setup Styles + run: | + cd frontend + npm ci --no-audit + npm link kubeflow + # Copy styles from kubeflow source to local styles directory + mkdir -p ./src/styles/ + cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/styles/* ./src/styles/ + # Also copy to node_modules for the copyCSS script + mkdir -p ./node_modules/kubeflow/styles/ + cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/styles/* ./node_modules/kubeflow/styles/ + - name: Run Unit Tests + run: | + cd frontend + npm run test:jest frontend-mock-tests: - name: Frontend Mock Tests - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 16 - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - name: Fetch Kubeflow and Build Common Library - run: | - COMMIT=$(cat frontend/COMMIT) - cd /tmp && git clone https://github.com/kubeflow/kubeflow.git - cd kubeflow - git checkout $COMMIT - cd components/crud-web-apps/common/frontend/kubeflow-common-lib - npm ci --no-audit - npm run build - npm link ./dist/kubeflow - - name: Install Frontend Dependencies and Setup Styles - run: | - cd frontend - npm ci --no-audit - npm link kubeflow - # Copy styles from kubeflow source to local styles directory - mkdir -p ./src/styles/ - cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/styles/* ./src/styles/ - # Also copy to node_modules for the copyCSS script - mkdir -p ./node_modules/kubeflow/styles/ - cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/styles/* ./node_modules/kubeflow/styles/ - # Copy assets as well - mkdir -p ./node_modules/kubeflow/assets/ - cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/assets/* ./node_modules/kubeflow/assets/ - - name: Run E2E Tests - run: | - cd frontend - npm run e2e:cypress:ci + name: Frontend Mock Tests + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 16 + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + - name: Fetch Kubeflow and Build Common Library + run: | + COMMIT=$(cat frontend/COMMIT) + cd /tmp && git clone https://github.com/kubeflow/kubeflow.git + cd kubeflow + git checkout $COMMIT + cd components/crud-web-apps/common/frontend/kubeflow-common-lib + npm ci --no-audit + npm run build + npm link ./dist/kubeflow + - name: Install Frontend Dependencies and Setup Styles + run: | + cd frontend + npm ci --no-audit + npm link kubeflow + # Copy styles from kubeflow source to local styles directory + mkdir -p ./src/styles/ + cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/styles/* ./src/styles/ + # Also copy to node_modules for the copyCSS script + mkdir -p ./node_modules/kubeflow/styles/ + cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/styles/* ./node_modules/kubeflow/styles/ + # Copy assets as well + mkdir -p ./node_modules/kubeflow/assets/ + cp -r /tmp/kubeflow/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/assets/* ./node_modules/kubeflow/assets/ + - name: Run E2E Tests + run: | + cd frontend + npm run e2e:cypress:ci diff --git a/README.md b/README.md index e2b6fa0e..2a83fe25 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,30 @@ The following is a list of environment variables that can be set for any web app | 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 | +| ALLOWED_NAMESPACES | "" | Comma-separated list of namespaces to show in the namespace selector. If only one namespace is specified, it will be auto-selected and the dropdown will be hidden. Empty means all namespaces are shown. | + +## Namespace Configuration + +The application supports configuring which namespaces are available in the namespace selector. This is particularly useful for standalone deployments where you want to limit access to specific namespaces. + +### Environment Variable +Set the `ALLOWED_NAMESPACES` environment variable with a comma-separated list of namespaces: + +```bash +# Single namespace (auto-selected, dropdown hidden) +ALLOWED_NAMESPACES="kubeflow-user" + +# Multiple namespaces (dropdown shows only these) +ALLOWED_NAMESPACES="kubeflow,kubeflow-user,production" +``` + +### Behavior +- **No configuration**: All namespaces are shown (default behavior) +- **Single namespace**: Namespace is auto-selected and dropdown is hidden +- **Multiple namespaces**: Dropdown shows only the specified namespaces +- **Error handling**: Falls back to all namespaces if specified ones don't exist + +For detailed configuration options and examples, see [CONFIGURABLE_NAMESPACES.md](./CONFIGURABLE_NAMESPACES.md). ## Grafana Configuration diff --git a/backend/apps/v1beta1/routes/get.py b/backend/apps/v1beta1/routes/get.py index ed294bf1..b603d8e9 100644 --- a/backend/apps/v1beta1/routes/get.py +++ b/backend/apps/v1beta1/routes/get.py @@ -30,3 +30,40 @@ def get_config(): except Exception as e: log.error("Error retrieving configuration: %s", str(e)) return api.error_response("message", "Failed to retrieve configuration"), 500 + + +@bp.route("/api/namespaces", methods=["GET"]) +def get_namespaces(): + """Handle retrieval of allowed namespaces based on ALLOWED_NAMESPACES env var.""" + try: + allowed_namespaces_env = os.environ.get("ALLOWED_NAMESPACES", "") + + if allowed_namespaces_env: + + allowed_list = [ + ns.strip() for ns in allowed_namespaces_env.split(",") if ns.strip() + ] + log.info( + "Filtering namespaces based on ALLOWED_NAMESPACES: %s", allowed_list + ) + all_namespaces = api.list_namespaces() + existing_namespaces = [ns.metadata.name for ns in all_namespaces.items] + filtered_namespaces = [ + ns for ns in allowed_list if ns in existing_namespaces + ] + + if not filtered_namespaces: + log.warning("None of the allowed namespaces exist: %s", allowed_list) + response_namespaces = existing_namespaces + else: + response_namespaces = filtered_namespaces + else: + log.info("No ALLOWED_NAMESPACES specified, returning all namespaces") + all_namespaces = api.list_namespaces() + response_namespaces = [ns.metadata.name for ns in all_namespaces.items] + + log.info("Returning namespaces: %s", response_namespaces) + return api.success_response("namespaces", response_namespaces) + except Exception as e: + log.error("Error retrieving namespaces: %s", str(e)) + return api.error_response("message", "Failed to retrieve namespaces"), 500 diff --git a/frontend/src/app/pages/index/index.component.html b/frontend/src/app/pages/index/index.component.html index 0c564e27..7bb0e054 100644 --- a/frontend/src/app/pages/index/index.component.html +++ b/frontend/src/app/pages/index/index.component.html @@ -1,8 +1,9 @@
- + > + diff --git a/frontend/src/app/services/namespace.service.ts b/frontend/src/app/services/namespace.service.ts new file mode 100644 index 00000000..79af7ba4 --- /dev/null +++ b/frontend/src/app/services/namespace.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { MWABackendService } from './backend.service'; + +@Injectable({ + providedIn: 'root', +}) +export class MWANamespaceService { + private singleNamespaceMode$ = new BehaviorSubject(false); + private allowedNamespacesSubject$ = new BehaviorSubject([]); + + constructor(private backend: MWABackendService) {} + + /** + * Get the filtered namespaces based on ALLOWED_NAMESPACES environment variable + */ + public getFilteredNamespaces(): Observable { + return this.backend.http.get('/api/namespaces').pipe( + map(response => response.namespaces || []), + tap((namespaces: string[]) => { + this.allowedNamespacesSubject$.next(namespaces); + // If only one namespace is available, we're in single namespace mode + this.singleNamespaceMode$.next(namespaces.length === 1); + }), + ); + } + + /** + * Check if we're in single namespace mode (should hide the dropdown) + */ + public get isSingleNamespaceMode$(): Observable { + return this.singleNamespaceMode$.asObservable(); + } + + /** + * Get the current allowed namespaces + */ + public get allowedNamespaces$(): Observable { + return this.allowedNamespacesSubject$.asObservable(); + } + + /** + * Get the single namespace when in single namespace mode + */ + public getSingleNamespace(): string | null { + const namespaces = this.allowedNamespacesSubject$.value; + return namespaces.length === 1 ? namespaces[0] : null; + } +} diff --git a/frontend/src/app/shared/namespace-select/namespace-select.component.ts b/frontend/src/app/shared/namespace-select/namespace-select.component.ts new file mode 100644 index 00000000..1682cbb7 --- /dev/null +++ b/frontend/src/app/shared/namespace-select/namespace-select.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { NamespaceService } from 'kubeflow'; +import { Subscription, Observable } from 'rxjs'; +import { MWANamespaceService } from '../../services/namespace.service'; + +@Component({ + selector: 'app-namespace-select', + template: ` + + + `, +}) +export class NamespaceSelectComponent implements OnInit, OnDestroy { + public isSingleNamespaceMode$: Observable; + private subscription = new Subscription(); + + constructor( + private ns: NamespaceService, + private mwaNs: MWANamespaceService, + ) { + this.isSingleNamespaceMode$ = this.mwaNs.isSingleNamespaceMode$; + } + + ngOnInit(): void { + this.subscription.add( + this.mwaNs.getFilteredNamespaces().subscribe(namespaces => { + if (namespaces.length === 1) { + this.ns.updateSelectedNamespace(namespaces[0]); + } + }), + ); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/frontend/src/app/shared/namespace-select/namespace-select.module.ts b/frontend/src/app/shared/namespace-select/namespace-select.module.ts new file mode 100644 index 00000000..c6ea97b0 --- /dev/null +++ b/frontend/src/app/shared/namespace-select/namespace-select.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { KubeflowModule } from 'kubeflow'; +import { NamespaceSelectComponent } from './namespace-select.component'; + +@NgModule({ + declarations: [NamespaceSelectComponent], + imports: [CommonModule, KubeflowModule], + exports: [NamespaceSelectComponent], +}) +export class NamespaceSelectModule {} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index ef7e83c7..8e77831a 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -2,10 +2,11 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { StorageUriComponent } from './storage-uri/storage-uri.component'; import { StorageUriColumnComponent } from './storage-uri-column/storage-uri-column.component'; +import { NamespaceSelectModule } from './namespace-select/namespace-select.module'; @NgModule({ declarations: [StorageUriComponent, StorageUriColumnComponent], - imports: [CommonModule], - exports: [StorageUriComponent], + imports: [CommonModule, NamespaceSelectModule], + exports: [StorageUriComponent, NamespaceSelectModule], }) export class SharedModule {}