From 0e7a40e5ca7b8be686bb71e9e0648a9c005e601d Mon Sep 17 00:00:00 2001 From: Tobias Oetzel Date: Fri, 13 Dec 2024 11:34:09 +0100 Subject: [PATCH 1/2] fix: readme-typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8d30bbd..04350c37 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,4 @@ Please refer to the [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) file in this reposi ## Licensing -Copyright 2024 SAP SE or an SAP affiliate company and openMFP contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/openmfp/portal-server-lib). +Copyright 2024 SAP SE or an SAP affiliate company and openMFP contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/openmfp/portal). From ac0b7c6960e85250c9f851855131465dc2b1f79a Mon Sep 17 00:00:00 2001 From: Tobias Oetzel Date: Wed, 18 Feb 2026 13:45:11 +0100 Subject: [PATCH 2/2] docs: add architecture and ACD documentation Add comprehensive documentation for the OpenMFP Portal: - ARCHITECTURE.md: High-level architecture, component diagrams, workflows, dependencies - ACD.md: Architecture & Code Documentation with implementation details Documentation includes Mermaid diagrams for visual representation of: - System architecture - Component interactions - Data flows - Deployment architecture --- docs/ACD.md | 994 +++++++++++++++++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 544 +++++++++++++++++++++++ 2 files changed, 1538 insertions(+) create mode 100644 docs/ACD.md create mode 100644 docs/ARCHITECTURE.md diff --git a/docs/ACD.md b/docs/ACD.md new file mode 100644 index 00000000..9f743203 --- /dev/null +++ b/docs/ACD.md @@ -0,0 +1,994 @@ +# Architecture & Code Documentation (ACD) + +## Component Structure and Key Modules + +### Frontend Structure + +``` +frontend/ +├── src/ +│ ├── app/ +│ │ └── app.routes.ts # Route configuration (empty - handled by Luigi) +│ ├── environments/ +│ │ ├── environment.ts # Development environment +│ │ └── environment.prod.ts # Production environment +│ ├── main.ts # Application bootstrap +│ ├── index.html # HTML entry point +│ └── styles.scss # Global styles +├── proxy.config.json # Development proxy configuration +├── angular.json # Angular CLI configuration +└── package.json # Dependencies and scripts +``` + +#### Key Frontend Files + +**main.ts** +```typescript +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; + +const portalOptions: PortalOptions = {}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` + +The frontend is intentionally minimal. All portal functionality is provided by the `@openmfp/portal-ui-lib` external library, including: +- Luigi Framework integration +- Portal component rendering +- Navigation management +- Authentication flows + +**app.routes.ts** +```typescript +import { Routes } from '@angular/router'; + +export const routes: Routes = []; +``` + +Routes are defined as an empty array because Luigi Framework handles all routing through its configuration system, not Angular Router. + +**proxy.config.json** +```json +{ + "/rest/**": { + "target": "http://localhost:3000", + "secure": false, + "logLevel": "debug", + "changeOrigin": true + } +} +``` + +Development proxy forwards all `/rest/**` requests from the Angular dev server (port 4300) to the NestJS backend (port 3000). + +### Backend Structure + +``` +backend/ +├── src/ +│ ├── app.module.ts # Root application module +│ ├── main.ts # NestJS bootstrap +│ ├── entity-context-provider/ +│ │ └── account-entity-context-provider.service.ts +│ └── service-providers/ +│ ├── kubernetes-service-providers.service.ts +│ ├── localServiceProviders.ts +│ ├── portal-context-provider.ts +│ └── provider-jsons/ +│ └── service-providers.ts +├── test/ +│ ├── app.e2e-spec.ts # End-to-end tests +│ └── jest-e2e.json # E2E test configuration +└── package.json # Dependencies and scripts +``` + +#### Key Backend Modules + +**main.ts** +```typescript +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(process.env.PORT || 3000); +} +bootstrap(); +``` + +Standard NestJS bootstrap that creates the application and listens on port 3000 (or PORT environment variable). + +**app.module.ts** +```typescript +import { Module } from '@nestjs/common'; +import { PortalModule, PortalModuleOptions } from '@openmfp/portal-server-lib'; +import { join } from 'path'; +import { AccountEntityContextProvider } from './entity-context-provider/account-entity-context-provider.service'; +import { KubernetesServiceProvidersService } from './service-providers/kubernetes-service-providers.service'; +import { OpenmfpPortalProvider } from './service-providers/portal-context-provider'; +import { config } from 'dotenv'; + +config({ path: './.env' }); + +const portalOptions: PortalModuleOptions = { + frontendDistSources: join(__dirname, '../..', 'frontend/dist/frontend'), + entityContextProviders: { + account: AccountEntityContextProvider, + }, + additionalProviders: [AccountEntityContextProvider], + serviceProviderService: KubernetesServiceProvidersService, + portalContextProvider: OpenmfpPortalProvider, +}; + +@Module({ + imports: [PortalModule.create(portalOptions)], +}) +export class AppModule {} +``` + +**Configuration breakdown:** +- **frontendDistSources**: Path to built Angular application for static file serving +- **entityContextProviders**: Maps entity types to context providers (e.g., "account" → AccountEntityContextProvider) +- **additionalProviders**: Services to inject into NestJS dependency injection +- **serviceProviderService**: Implementation that fetches service provider configurations +- **portalContextProvider**: Provides global portal context (like API URLs) + +## State Management Patterns + +### Frontend State Management + +The portal frontend does not implement custom state management. All state is managed by: + +1. **Luigi Framework**: Maintains navigation state, current route, and micro-frontend lifecycle +2. **@openmfp/portal-ui-lib**: Manages portal configuration and user context +3. **RxJS**: Observable streams for asynchronous operations + +```mermaid +graph LR + subgraph "State Management" + Luigi[Luigi Framework
Navigation State] + PortalLib[Portal UI Lib
Configuration State] + RxJS[RxJS Observables
Async State] + end + + subgraph "Data Flow" + Backend[Backend API] + MicroFE[Micro-frontends] + end + + Luigi --> MicroFE + PortalLib --> Luigi + Backend -->|API Responses| RxJS + RxJS --> PortalLib + + style Luigi fill:#e3f2fd + style PortalLib fill:#f3e5f5 + style Backend fill:#fff3e0 +``` + +### Backend State Management + +The backend is stateless and does not maintain session state. Each request: +1. Receives authentication token +2. Queries Kubernetes for current ContentConfiguration CRDs +3. Returns fresh data +4. No caching implemented (consider adding for production) + +**Service Provider State Flow:** + +```mermaid +sequenceDiagram + participant Client + participant API + participant K8s + + Client->>API: Request service providers + API->>K8s: Query ContentConfiguration CRDs + K8s-->>API: Return latest CRDs + API->>API: Parse and transform + API-->>Client: Return providers + Note over API: No state retained +``` + +## API Integration + +### REST API Endpoints + +The portal backend exposes endpoints through the `@openmfp/portal-server-lib`: + +**Base URL**: `/rest` + +**Key endpoints provided by PortalModule:** +- `GET /rest/service-providers` - Fetch dynamic navigation and content configuration +- `GET /rest/entity-context` - Get entity-specific context and policies +- `GET /rest/portal-context` - Get global portal configuration + +### Service Provider API + +**KubernetesServiceProvidersService Implementation:** + +```typescript +export class KubernetesServiceProvidersService implements ServiceProviderService { + private k8sApi: CustomObjectsApi; + + constructor() { + const kc = new k8s.KubeConfig(); + kc.loadFromDefault(); + this.k8sApi = kc.makeApiClient(k8s.CustomObjectsApi); + } + + async getServiceProviders( + token: string, + entities: string[], + context: Record + ): Promise { + try { + const response = await this.k8sApi.listNamespacedCustomObject( + 'core.openmfp.io', // API Group + 'v1alpha1', // Version + 'openmfp-system', // Namespace + 'contentconfigurations' // Resource plural + ); + + if (!response.body['items']) { + return { serviceProviders: [] }; + } + + const responseItems = response.body['items'] as any[]; + + let contentConfigurations = responseItems + .filter((item) => !!item.status.configurationResult) + .map((item) => + JSON.parse(item.status.configurationResult) as ContentConfiguration + ); + + return { + serviceProviders: [ + { + contentConfiguration: contentConfigurations, + } as RawServiceProvider, + ], + }; + } catch (error) { + console.error(error); + } + } +} +``` + +**API Flow:** + +```mermaid +sequenceDiagram + participant Client + participant PortalModule + participant K8sService + participant K8sAPI + + Client->>PortalModule: GET /rest/service-providers + PortalModule->>K8sService: getServiceProviders(token, entities, context) + K8sService->>K8sAPI: listNamespacedCustomObject() + Note over K8sAPI: core.openmfp.io/v1alpha1
openmfp-system/contentconfigurations + K8sAPI-->>K8sService: Response { items: [...] } + K8sService->>K8sService: Filter items with status.configurationResult + K8sService->>K8sService: Parse JSON from configurationResult + K8sService-->>PortalModule: ServiceProviderResponse + PortalModule-->>Client: JSON response +``` + +### Entity Context API + +**AccountEntityContextProvider Implementation:** + +```typescript +@Injectable() +export class AccountEntityContextProvider implements EntityContextProvider { + async getContextValues( + token: string, + context?: Record + ): Promise> { + return { + id: context.account, + policies: [ + 'create', + 'delete', + 'get', + 'list', + 'update', + 'watch', + 'gardener_project_create', + 'gardener_project_list', + 'gardener_shoot_create', + 'gardener_shoot_list', + ], + }; + } +} +``` + +**Returns:** +- **id**: The account identifier from the request context +- **policies**: Array of permission strings that control UI visibility and actions + +### Portal Context API + +**OpenmfpPortalProvider Implementation:** + +```typescript +@Injectable() +export class OpenmfpPortalProvider implements PortalContextProvider { + constructor() {} + + getContextValues(): Promise> { + const context: Record = { + crdGatewayApiUrl: process.env.CRD_GATEWAY_API_URL, + }; + return Promise.resolve(context); + } +} +``` + +**Returns:** +- **crdGatewayApiUrl**: API endpoint for CRD Gateway service (from environment variable) + +## Routing and Navigation + +### Luigi Framework Navigation + +All routing is handled by Luigi Framework through dynamic configuration loaded from Kubernetes CRDs. + +**Navigation Structure:** + +```mermaid +graph TB + Root[Luigi Shell] + Home[Home /home] + Overview[Overview /home/overview] + Gardener[Gardener Dashboard /gardener] + DemoPages[Demo Pages Category] + Empty[Empty Page /empty] + Table[Table /table] + Tree[Tree /tree] + + Root --> Home + Home --> Overview + Root --> Gardener + Root --> DemoPages + DemoPages --> Empty + DemoPages --> Table + DemoPages --> Tree + + style Root fill:#e1f5ff + style Home fill:#fff3e0 + style Gardener fill:#e8f5e9 + style DemoPages fill:#f3e5f5 +``` + +### Luigi Configuration Structure + +**Node Configuration Example:** + +```typescript +{ + entityType: 'global', // Entity scope + pathSegment: 'home', // URL segment + label: 'Overview', // Display name + icon: 'home', // SAP icon name + hideFromNav: true, // Hide from side navigation + defineEntity: { + id: 'example' // Define entity context + }, + viewUrl: '/home', // Relative URL + url: 'https://...', // Absolute URL (for external micro-frontends) + context: { // Data passed to micro-frontend + title: 'Welcome', + content: '...' + }, + children: [...] // Nested routes +} +``` + +### Navigation Types + +**Internal Routes (viewUrl):** +```typescript +{ + pathSegment: 'overview', + viewUrl: '/overview', + label: 'Overview' +} +``` +Served by the portal's own frontend application. + +**External Micro-frontends (url):** +```typescript +{ + pathSegment: 'demo', + url: 'https://fiddle.luigi-project.io/examples/...', + label: 'Demo Page' +} +``` +Loaded in iframe from external source. + +**Virtual Trees (virtualTree):** +```typescript +{ + pathSegment: 'gardener', + virtualTree: true, + url: 'https://gardener-dashboard.example.com', + label: 'Gardener Dashboard' +} +``` +Embeds entire external application with its own routing. + +### Entity-based Navigation + +Navigation nodes can be scoped to entity types: + +```typescript +{ + entityType: 'account', // Only visible in account context + pathSegment: 'resources', + label: 'Resources' +} +``` + +The entity context is resolved by backend providers and passed to Luigi, which filters navigation based on current entity. + +## Configuration Options + +### Environment Variables + +**Backend Configuration (.env):** + +```bash +# Server Configuration +PORT=3000 # Backend server port + +# Kubernetes Configuration +KUBECONFIG=/path/to/kubeconfig # Kubernetes config file (optional, defaults to ~/.kube/config) + +# OpenMFP Configuration +CRD_GATEWAY_API_URL=https://... # CRD Gateway API endpoint +``` + +**Frontend Configuration:** + +Frontend configuration is minimal. The `environment.ts` and `environment.prod.ts` files only define the `production` flag: + +```typescript +export const environment = { + production: false // or true for production +}; +``` + +All runtime configuration comes from the backend via API endpoints. + +### Portal Module Options + +**PortalModuleOptions Interface:** + +```typescript +interface PortalModuleOptions { + // Path to built frontend assets + frontendDistSources: string; + + // Map of entity types to context providers + entityContextProviders: { + [entityType: string]: Type; + }; + + // Additional services to inject + additionalProviders: Provider[]; + + // Service that fetches dynamic content + serviceProviderService: Type; + + // Provider for global portal context + portalContextProvider: Type; +} +``` + +**Example Configuration:** + +```typescript +const portalOptions: PortalModuleOptions = { + frontendDistSources: join(__dirname, '../..', 'frontend/dist/frontend'), + entityContextProviders: { + account: AccountEntityContextProvider, + // Add more entity types as needed + }, + additionalProviders: [AccountEntityContextProvider], + serviceProviderService: KubernetesServiceProvidersService, + portalContextProvider: OpenmfpPortalProvider, +}; +``` + +### ContentConfiguration CRD Format + +**CRD Structure:** + +```yaml +apiVersion: core.openmfp.io/v1alpha1 +kind: ContentConfiguration +metadata: + name: my-content + namespace: openmfp-system + creationTimestamp: "2022-05-17T11:37:17Z" +status: + configurationResult: | + { + "name": "my-content", + "creationTimestamp": "2022-05-17T11:37:17Z", + "luigiConfigFragment": { + "data": { + "nodes": [...], + "nodeDefaults": {...} + } + } + } +``` + +**Luigi Config Fragment Fields:** + +- **nodes**: Array of navigation node configurations +- **nodeDefaults**: Default properties applied to all nodes + - `entityType`: Default entity scope + - `loadingIndicator`: Loading UI configuration + - `icon`: Default icon + +### Luigi Node Configuration + +**Complete Node Example:** + +```typescript +{ + // Navigation + pathSegment: 'my-feature', // URL segment + label: 'My Feature', // Display name + icon: 'add', // SAP icon + hideFromNav: false, // Show in navigation + + // Categorization + category: { + label: 'Feature Category', + icon: 'group', + collapsible: true + }, + + // Entity Context + entityType: 'account', // Entity scope + defineEntity: { + id: 'entity-id' + }, + + // Content Loading + viewUrl: '/internal-path', // Internal route + url: 'https://external.com', // External micro-frontend + virtualTree: true, // Full app embedding + + // UI Behavior + loadingIndicator: { + enabled: true + }, + + // Data Passing + context: { + customData: 'value' + }, + + // Nested Navigation + children: [...] +} +``` + +## Development Workflow + +### Local Development Setup + +**Prerequisites:** +- Node.js 22+ +- npm 9+ +- Access to Kubernetes cluster with ContentConfiguration CRDs +- Valid kubeconfig + +**Setup Steps:** + +```bash +# Install dependencies +npm run prepare # Installs both frontend and backend + +# Start development servers (runs concurrently) +npm start # Starts both frontend:4300 and backend:3000 + +# Or start individually +npm run start:ui # Frontend only +npm run start:server # Backend only +``` + +### Development Architecture + +```mermaid +graph LR + subgraph "Developer Machine" + Browser[Browser
localhost:4300] + AngularCLI[Angular CLI
Dev Server] + NestJS[NestJS
Debug Mode] + end + + subgraph "External" + K8s[Kubernetes Cluster] + MicroFE[External Micro-frontends] + end + + Browser -->|HTTP| AngularCLI + Browser -->|Embed| MicroFE + AngularCLI -->|Proxy /rest/**| NestJS + NestJS --> K8s + + style Browser fill:#e1f5ff + style AngularCLI fill:#fff3e0 + style NestJS fill:#e8f5e9 +``` + +### Build and Test + +**Build Commands:** + +```bash +# Build both frontend and backend +npm run build + +# Build individually +npm run build:ui # Frontend production build +npm run build:server # Backend compilation +``` + +**Test Commands:** + +```bash +# Run all tests +npm test + +# Run with coverage +npm run test:cov + +# Test individually +npm run test:ui # Frontend tests +npm run test:server # Backend tests +``` + +**Lint Commands:** + +```bash +# Lint all code +npm run lint + +# Fix linting issues +npm run lint:fix +``` + +### Docker Build + +**Local Docker Build:** + +```bash +# Create secret file with GitHub token +mkdir -p .secret +echo -n $OPENMFP_GITHUB_TOKEN > .secret/gh-token + +# Build with secrets +docker build --secret id=NODE_AUTH_TOKEN,src=.secret/gh-token . + +# Cleanup secret on failure +docker build --secret id=NODE_AUTH_TOKEN,src=.secret/gh-token . || rm .secret/gh-token +``` + +**Build Stages:** + +```mermaid +graph TB + subgraph "Stage 1: Build" + S1[node:22.12] + Copy1[Copy package files] + Install[npm ci] + Build[npm run build] + end + + subgraph "Stage 2: Runtime" + S2[node:22.12-alpine] + Copy2[Copy dist files] + Cmd[CMD node dist/main] + end + + S1 --> Copy1 + Copy1 --> Install + Install --> Build + Build -->|Copy artifacts| Copy2 + S2 --> Copy2 + Copy2 --> Cmd + + style Build fill:#fff3e0 + style S2 fill:#e8f5e9 +``` + +### Debugging + +**Frontend Debugging:** +- Use Angular DevTools browser extension +- Chrome DevTools for debugging +- Source maps enabled in development + +**Backend Debugging:** +```bash +npm run start:debug # Starts with --debug flag +``` +Attach debugger to `localhost:9229` + +**VS Code Launch Configuration:** + +```json +{ + "type": "node", + "request": "attach", + "name": "Attach to Backend", + "port": 9229, + "restart": true, + "skipFiles": ["/**"] +} +``` + +## Testing Strategy + +### Frontend Tests + +**Test Framework:** Jest with Angular testing utilities + +**Test File Pattern:** `*.spec.ts` + +**Run Tests:** +```bash +cd frontend +npm test # Run tests +npm run test:cov # Run with coverage +``` + +### Backend Tests + +**Test Framework:** Jest with NestJS testing utilities + +**Unit Tests:** `src/**/*.spec.ts` +**E2E Tests:** `test/**/*.e2e-spec.ts` + +**Run Tests:** +```bash +cd backend +npm test # Unit tests +npm run test:e2e # E2E tests +npm run test:cov # Coverage +``` + +### Test Coverage + +Both frontend and backend track test coverage in `./coverage` directories. + +## Common Tasks + +### Adding a New Entity Context Provider + +1. Create provider service: +```typescript +import { Injectable } from '@nestjs/common'; +import { EntityContextProvider } from '@openmfp/portal-server-lib'; + +@Injectable() +export class MyEntityContextProvider implements EntityContextProvider { + async getContextValues( + token: string, + context?: Record + ): Promise> { + return { + id: context.myEntity, + policies: ['read', 'write'] + }; + } +} +``` + +2. Register in `app.module.ts`: +```typescript +const portalOptions: PortalModuleOptions = { + // ... + entityContextProviders: { + account: AccountEntityContextProvider, + myEntity: MyEntityContextProvider, // Add here + }, + additionalProviders: [ + AccountEntityContextProvider, + MyEntityContextProvider, // Add here + ], + // ... +}; +``` + +### Adding New ContentConfiguration + +1. Create CRD in Kubernetes: +```bash +kubectl apply -f - < + ): Promise { + // Custom logic - fetch from database, external API, etc. + return { + serviceProviders: [...] + }; + } +} +``` + +Update `app.module.ts`: +```typescript +const portalOptions: PortalModuleOptions = { + // ... + serviceProviderService: CustomServiceProvidersService, +}; +``` + +## Troubleshooting + +### Frontend Not Loading + +**Check:** +1. Backend is running on port 3000 +2. Proxy configuration is correct +3. No CORS errors in browser console + +### Backend Cannot Connect to Kubernetes + +**Check:** +1. `KUBECONFIG` environment variable is set correctly +2. Kubeconfig file exists and has valid credentials +3. `openmfp-system` namespace exists +4. User/service account has permission to list ContentConfiguration CRDs + +### No Navigation Items Appearing + +**Check:** +1. ContentConfiguration CRDs exist in `openmfp-system` +2. CRDs have `status.configurationResult` populated +3. JSON in `configurationResult` is valid +4. Backend logs for errors fetching CRDs + +### Micro-frontend Not Loading + +**Check:** +1. External URL is accessible from browser +2. External service allows iframe embedding (X-Frame-Options) +3. CORS headers are set correctly on external service +4. Luigi node configuration has correct `url` field + +## Performance Optimization + +### Backend Optimization + +**Caching ContentConfiguration:** +```typescript +@Injectable() +export class CachedServiceProvidersService implements ServiceProviderService { + private cache: ServiceProviderResponse | null = null; + private cacheTime = 60000; // 1 minute + private lastFetch = 0; + + async getServiceProviders( + token: string, + entities: string[], + context: Record + ): Promise { + const now = Date.now(); + if (this.cache && (now - this.lastFetch) < this.cacheTime) { + return this.cache; + } + + // Fetch fresh data + const result = await this.fetchFromKubernetes(); + this.cache = result; + this.lastFetch = now; + return result; + } +} +``` + +**Watch for CRD Changes:** +```typescript +// Use Kubernetes watch API to invalidate cache +const watch = new k8s.Watch(kc); +watch.watch( + '/apis/core.openmfp.io/v1alpha1/namespaces/openmfp-system/contentconfigurations', + {}, + (type, obj) => { + if (type === 'MODIFIED' || type === 'ADDED' || type === 'DELETED') { + this.cache = null; // Invalidate cache + } + } +); +``` + +### Frontend Optimization + +- Luigi handles lazy loading automatically +- Micro-frontends load only when navigated to +- Use `loadingIndicator` to show loading state + +## Security Best Practices + +### Environment Variables + +- Never commit `.env` files with secrets +- Use Kubernetes secrets for production deployments +- Rotate credentials regularly + +### API Security + +- Always validate tokens on backend +- Implement rate limiting for production +- Use HTTPS in production +- Set appropriate CORS policies + +### Kubernetes RBAC + +Ensure service account has minimal required permissions: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: portal-backend + namespace: openmfp-system +rules: + - apiGroups: ["core.openmfp.io"] + resources: ["contentconfigurations"] + verbs: ["get", "list", "watch"] +``` + +## External Library Documentation + +- **NestJS**: https://docs.nestjs.com +- **Angular**: https://angular.io/docs +- **Luigi Framework**: https://docs.luigi-project.io +- **Kubernetes Client**: https://github.com/kubernetes-client/javascript +- **@openmfp/portal-ui-lib**: Internal OpenMFP library +- **@openmfp/portal-server-lib**: Internal OpenMFP library diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..c1bfd8ac --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,544 @@ +# Architecture + +## Overview + +The OpenMFP Portal is a web-based management interface for the OpenMFP platform. It provides a unified interface for accessing and managing various OpenMFP resources, services, and integrations through a micro-frontend architecture powered by Luigi Framework. + +### Purpose + +The portal serves as the central entry point for users to: +- Access and manage OpenMFP resources +- Navigate between different micro-frontends and services +- Interact with Kubernetes custom resources through a user-friendly interface +- Integrate with external services like Gardener Dashboard + +### Technology Stack + +**Frontend:** +- Angular 18 with standalone components +- Luigi Framework for micro-frontend orchestration +- @openmfp/portal-ui-lib for UI components +- TypeScript 5.5 + +**Backend:** +- NestJS 10 framework +- @openmfp/portal-server-lib for portal services +- Kubernetes client-node for CRD interaction +- Node.js 22+ + +## High-Level Architecture + +```mermaid +graph TB + subgraph "Client Browser" + UI[Angular Frontend
Luigi Shell] + end + + subgraph "Portal Backend" + API[NestJS API Server
Port 3000] + SPService[Service Provider Service] + EntityContext[Entity Context Provider] + PortalContext[Portal Context Provider] + end + + subgraph "Kubernetes Cluster" + CRD[ContentConfiguration CRDs
openmfp-system namespace] + K8sAPI[Kubernetes API] + end + + subgraph "External Services" + Gardener[Gardener Dashboard] + MicroFE[Micro-frontends] + end + + UI -->|HTTP Requests /rest/**| API + API --> SPService + API --> EntityContext + API --> PortalContext + SPService -->|List ContentConfigurations| K8sAPI + K8sAPI --> CRD + UI -->|Embed iframes| MicroFE + UI -->|Embed virtualTree| Gardener + + style UI fill:#e1f5ff + style API fill:#fff4e1 + style CRD fill:#e8f5e9 + style Gardener fill:#f3e5f5 +``` + +## Component Architecture + +### Frontend Architecture + +```mermaid +graph LR + subgraph "Frontend Application" + Main[main.ts
Bootstrap] + Portal[PortalComponent
from portal-ui-lib] + Luigi[Luigi Framework
Micro-frontend Shell] + Config[Portal Configuration] + end + + subgraph "External Libraries" + PUIL[@openmfp/portal-ui-lib] + LuigiCore[@luigi-project/core] + OAuth[@luigi-project/plugin-auth-oauth2] + end + + Main -->|bootstrapApplication| Portal + Main -->|providePortal| Config + Portal --> Luigi + Portal --> PUIL + Luigi --> LuigiCore + Luigi --> OAuth + + style Main fill:#e3f2fd + style Portal fill:#f3e5f5 + style Luigi fill:#fff3e0 +``` + +The frontend is minimal by design, delegating most functionality to the `@openmfp/portal-ui-lib` library. This library provides: +- The main `PortalComponent` that serves as the Luigi shell +- Navigation configuration +- Micro-frontend integration +- Authentication handling + +### Backend Architecture + +```mermaid +graph TB + subgraph "NestJS Application" + Main[main.ts
Bootstrap] + AppModule[AppModule] + PortalModule[PortalModule
from portal-server-lib] + + subgraph "Custom Providers" + SPService[KubernetesServiceProvidersService] + EntityProvider[AccountEntityContextProvider] + ContextProvider[OpenmfpPortalProvider] + end + end + + subgraph "Kubernetes Integration" + K8sClient[Kubernetes Client
@kubernetes/client-node] + CustomAPI[CustomObjectsApi] + end + + subgraph "Portal Server Library" + PSL[@openmfp/portal-server-lib] + Interfaces[ServiceProviderService
EntityContextProvider
PortalContextProvider] + end + + Main --> AppModule + AppModule -->|imports| PortalModule + AppModule -->|configures| SPService + AppModule -->|configures| EntityProvider + AppModule -->|configures| ContextProvider + + SPService -->|implements| Interfaces + EntityProvider -->|implements| Interfaces + ContextProvider -->|implements| Interfaces + + SPService --> K8sClient + K8sClient --> CustomAPI + + PortalModule --> PSL + + style Main fill:#e8f5e9 + style AppModule fill:#fff3e0 + style SPService fill:#e1f5ff + style K8sClient fill:#f3e5f5 +``` + +## Key Workflows + +### Service Provider Discovery Flow + +```mermaid +sequenceDiagram + participant Client as Browser + participant Backend as NestJS API + participant K8s as Kubernetes API + participant CRD as ContentConfiguration CRDs + + Client->>Backend: GET /rest/service-providers + Backend->>K8s: listNamespacedCustomObject() + Note over K8s: API Group: core.openmfp.io
Version: v1alpha1
Namespace: openmfp-system
Resource: contentconfigurations + K8s->>CRD: Query CRDs + CRD-->>K8s: Return items[] + K8s-->>Backend: Response with items + Backend->>Backend: Filter items with status.configurationResult + Backend->>Backend: Parse JSON configurations + Backend-->>Client: Return serviceProviders array + Client->>Client: Render navigation and micro-frontends +``` + +### Entity Context Resolution Flow + +```mermaid +sequenceDiagram + participant Client as Browser + participant Backend as NestJS API + participant EntityProvider as AccountEntityContextProvider + + Client->>Backend: GET /rest/entity-context?entity=account + Backend->>EntityProvider: getContextValues(token, context) + EntityProvider->>EntityProvider: Extract account from context + EntityProvider->>EntityProvider: Define policies array + Note over EntityProvider: Policies: create, delete, get,
list, update, watch,
gardener_project_create, etc. + EntityProvider-->>Backend: Return context values + Backend-->>Client: Return { id, policies } + Client->>Client: Apply policies to UI elements +``` + +### Content Configuration Loading + +```mermaid +sequenceDiagram + participant Portal as Luigi Portal Shell + participant Backend as Backend API + participant K8s as Kubernetes + + Portal->>Backend: Initialize - Request service providers + Backend->>K8s: Fetch ContentConfiguration CRDs + K8s-->>Backend: Return CRD items + Backend->>Backend: Extract luigiConfigFragment from each CRD + Note over Backend: Each CRD contains:
- name
- creationTimestamp
- luigiConfigFragment.data.nodes[] + Backend-->>Portal: Return content configurations + Portal->>Portal: Merge Luigi config fragments + Portal->>Portal: Build navigation tree + Portal->>Portal: Register micro-frontend nodes + Note over Portal: Nodes include:
- pathSegment
- label
- icon
- viewUrl or url
- entityType
- context +``` + +## Dependencies and Integrations + +### Frontend Dependencies + +**Core Framework:** +- **@angular/core (^18.0.0)**: Primary application framework +- **@angular/router (^18.0.0)**: Routing capabilities +- **rxjs (~7.8.0)**: Reactive programming library + +**Portal Libraries:** +- **@openmfp/portal-ui-lib (^0.82.0)**: OpenMFP portal UI components and Luigi shell +- **@luigi-project/core (^2.18.1)**: Micro-frontend framework for navigation and composition +- **@luigi-project/plugin-auth-oauth2 (^2.18.1)**: OAuth2 authentication plugin + +**Utilities:** +- **jwt-decode (^4.0.0)**: JWT token parsing +- **jmespath (0.16.0)**: JSON query language +- **lodash (4.17.21)**: JavaScript utility library + +### Backend Dependencies + +**Core Framework:** +- **@nestjs/core (^10.4.6)**: NestJS application framework +- **@nestjs/common (^10.4.6)**: Common NestJS utilities +- **@nestjs/platform-express (^10.4.6)**: Express platform adapter + +**Portal Libraries:** +- **@openmfp/portal-server-lib (^0.93.0)**: OpenMFP portal server components and services + +**Kubernetes Integration:** +- **@kubernetes/client-node (^0.22.0)**: Official Kubernetes client for Node.js + - Used to interact with Kubernetes API + - Reads ContentConfiguration custom resources + - Loads kubeconfig from default locations + +**HTTP and Utilities:** +- **@nestjs/axios (^3.0.1)**: HTTP client module +- **axios (^1.6.3)**: Promise-based HTTP client +- **cookie-parser (1.4.7)**: Cookie parsing middleware +- **dotenv (^16.4.5)**: Environment variable management + +### External Service Integrations + +**Gardener Dashboard:** +- Integrated as a virtual tree node in Luigi navigation +- Accessed via iframe embedding +- URL configured in ContentConfiguration CRDs +- Example: `https://d.ing.gardener-op.mfp-dev.shoot.canary.k8s-hana.ondemand.com` + +**Luigi Fiddle Examples:** +- Demo micro-frontends for testing +- Hosted at `https://fiddle.luigi-project.io` +- Include table, tree, and empty page examples + +### Kubernetes Custom Resources + +**ContentConfiguration CRD:** +- **API Group**: `core.openmfp.io` +- **Version**: `v1alpha1` +- **Namespace**: `openmfp-system` +- **Purpose**: Dynamic configuration of portal content and navigation + +**CRD Structure:** +```yaml +apiVersion: core.openmfp.io/v1alpha1 +kind: ContentConfiguration +metadata: + name: example-config + namespace: openmfp-system +spec: + # Specification details +status: + configurationResult: | + { + "name": "example", + "creationTimestamp": "2022-05-17T11:37:17Z", + "luigiConfigFragment": { + "data": { + "nodes": [...] + } + } + } +``` + +### Configuration Flow + +```mermaid +graph LR + subgraph "Configuration Sources" + ENV[Environment Variables
.env file] + K8sCRD[Kubernetes CRDs
ContentConfiguration] + end + + subgraph "Backend Configuration" + PortalOpts[PortalModuleOptions] + ContextProv[OpenmfpPortalProvider] + end + + subgraph "Frontend Configuration" + PortalFEOpts[PortalOptions] + LuigiConfig[Luigi Configuration] + end + + ENV -->|CRD_GATEWAY_API_URL| ContextProv + ContextProv --> PortalOpts + K8sCRD -->|Dynamic content| PortalOpts + + PortalOpts -->|API response| PortalFEOpts + PortalFEOpts --> LuigiConfig + + style ENV fill:#fff3e0 + style K8sCRD fill:#e8f5e9 + style PortalOpts fill:#e3f2fd + style LuigiConfig fill:#f3e5f5 +``` + +## Deployment Architecture + +### Development Environment + +```mermaid +graph TB + subgraph "Development Machine" + FE[Angular Dev Server
Port 4300] + BE[NestJS Dev Server
Port 3000] + Proxy[Proxy Config
/rest/** → :3000] + end + + subgraph "Kubernetes Cluster" + K8s[Kubernetes API
via kubeconfig] + NS[openmfp-system namespace] + end + + FE -->|Proxied requests| Proxy + Proxy --> BE + BE --> K8s + K8s --> NS + + style FE fill:#e3f2fd + style BE fill:#fff3e0 + style K8s fill:#e8f5e9 +``` + +**Development Setup:** +- Frontend runs on port 4300 with hot reload +- Backend runs on port 3000 with debug mode +- Proxy configuration forwards `/rest/**` from frontend to backend +- Backend connects to Kubernetes using default kubeconfig + +### Production Deployment + +```mermaid +graph TB + subgraph "Docker Container" + Node[Node.js Runtime
Port 3000] + Static[Serve Static
Frontend Assets] + API[NestJS API
Backend Services] + end + + subgraph "Kubernetes Cluster" + Service[Portal Service] + Pod[Portal Pod] + CRD[ContentConfiguration CRDs] + end + + Client[Client Browser] -->|HTTP| Service + Service --> Pod + Pod --> Node + Node --> Static + Node --> API + API --> CRD + + style Client fill:#e1f5ff + style Node fill:#fff3e0 + style Pod fill:#e8f5e9 +``` + +**Production Deployment:** +- Multi-stage Docker build +- Frontend built to static assets in `frontend/dist` +- Backend built to JavaScript in `backend/dist` +- Single Node.js process serves both static frontend and API +- Alpine-based runtime image for minimal size +- Exposed on port 3000 + +### Docker Build Process + +```mermaid +graph LR + subgraph "Build Stage" + Source[Source Code] + NPM[npm install] + Build[npm run build] + end + + subgraph "Runtime Stage" + Alpine[node:22.12-alpine] + BackendDist[backend/dist] + FrontendDist[frontend/dist] + end + + Source --> NPM + NPM --> Build + Build --> BackendDist + Build --> FrontendDist + + BackendDist --> Alpine + FrontendDist --> Alpine + + style Build fill:#fff3e0 + style Alpine fill:#e8f5e9 +``` + +## Security Considerations + +### Authentication + +- OAuth2 authentication via Luigi plugin (`@luigi-project/plugin-auth-oauth2`) +- JWT token handling via `jwt-decode` +- Tokens passed to backend for service provider requests +- Entity context resolution based on authenticated user + +### Authorization + +The `AccountEntityContextProvider` defines policies for authenticated users: +- **Resource operations**: create, delete, get, list, update, watch +- **Gardener operations**: gardener_project_create, gardener_project_list, gardener_shoot_create, gardener_shoot_list + +### Kubernetes Access + +- Backend uses Kubernetes client with kubeconfig authentication +- Reads ContentConfiguration CRDs from `openmfp-system` namespace +- Service account or user credentials loaded from default kubeconfig location +- No direct client-to-Kubernetes communication (all proxied through backend) + +## Extensibility + +### Adding New Content + +New portal content can be added dynamically by creating ContentConfiguration CRDs in Kubernetes: + +```yaml +apiVersion: core.openmfp.io/v1alpha1 +kind: ContentConfiguration +metadata: + name: my-new-content + namespace: openmfp-system +status: + configurationResult: | + { + "name": "my-new-content", + "luigiConfigFragment": { + "data": { + "nodes": [ + { + "pathSegment": "my-feature", + "label": "My Feature", + "icon": "add", + "url": "https://my-micro-frontend.example.com", + "entityType": "account" + } + ] + } + } + } +``` + +### Custom Entity Context Providers + +Additional entity types can be supported by implementing the `EntityContextProvider` interface and registering in `AppModule`: + +```typescript +export class CustomEntityContextProvider implements EntityContextProvider { + async getContextValues( + token: string, + context?: Record + ): Promise> { + // Custom logic + return { + id: context.entityId, + customData: {} + }; + } +} +``` + +### Service Provider Customization + +The `KubernetesServiceProvidersService` can be replaced or extended to fetch configurations from different sources (databases, external APIs, etc.) by implementing the `ServiceProviderService` interface. + +## Performance Considerations + +### Micro-frontend Loading + +- Luigi Framework handles lazy loading of micro-frontends +- Only active routes load their corresponding micro-frontends +- Virtual trees (like Gardener) load external applications in isolated iframes + +### Backend Caching + +- ContentConfiguration CRDs are fetched on each request +- Consider implementing caching for production deployments +- Watch Kubernetes events for cache invalidation + +### Build Optimization + +- Production builds use Angular's optimization +- Tree-shaking removes unused code +- Multi-stage Docker build separates build and runtime dependencies +- Alpine Linux runtime image minimizes container size + +## Monitoring and Observability + +### Logging + +- Backend uses NestJS built-in logging +- Errors logged to console with stack traces +- Frontend errors logged to browser console + +### Health Checks + +- Backend exposes health endpoints via NestJS +- Kubernetes readiness/liveness probes can be configured +- Frontend availability depends on backend serving static assets + +## Related Repositories + +- **@openmfp/portal-ui-lib**: Core portal UI components and Luigi configuration +- **@openmfp/portal-server-lib**: Backend portal services and interfaces +- **luigi-project/luigi**: Micro-frontend framework +- **kubernetes/client-node**: Official Kubernetes client for Node.js