diff --git a/DEPLOYMENT_SCRIPTS.md b/DEPLOYMENT_SCRIPTS.md new file mode 100644 index 0000000..9f85c6b --- /dev/null +++ b/DEPLOYMENT_SCRIPTS.md @@ -0,0 +1,298 @@ +# CF Deployer - Deployment Scripts Documentation + +## Overview + +This repository contains a Spring Boot application for deploying JAR files to Cloud Foundry/Tanzu environments using chunked uploads. There are two deployment scripts designed for different network paths. + +## Architecture + +``` +┌─────────────────────┐ +│ deploy-chunked.sh │──────► nginx ──────► Spring Boot App +│ (Direct to nginx) │ (multipart endpoint) +└─────────────────────┘ + +┌─────────────────────────┐ +│deploy-chunked-simple.sh │──► Java Proxy ──► nginx ──► Spring Boot App +│ (Through Java proxy) │ (adds headers) (base64 endpoint) +└─────────────────────────┘ +``` + +## Deployment Scripts + +### 1. deploy-chunked.sh +**Use when**: Direct access to nginx endpoint with cert/key/headers + +**Features**: +- Sends chunks as multipart form data (`-F` flags) +- Supports client certificates (`--cert`, `--key`) +- Supports custom headers (`X-Forwarded-For`, `My-APIM-KEY`) +- Uses Spring Boot endpoint: `POST /upload/chunk` (multipart) + +**Configuration**: +```bash +# Lines 49-55 +CERT_FILE="/path/to/cert.pem" +KEY_FILE="/path/to/key.pem" +X_FORWARDED_FOR="192.168.1.100" +MY_APIM_KEY="your-api-key" +``` + +**Curl format**: +```bash +curl POST /upload/chunk \ + --cert cert.pem --key key.pem \ + -H "X-Forwarded-For: ..." \ + -H "My-APIM-KEY: ..." \ + -F "uploadSessionId=..." \ + -F "fileType=..." \ + -F "chunk=@chunk_file" +``` + +### 2. deploy-chunked-simple.sh +**Use when**: Going through Java proxy that adds cert/headers automatically + +**Features**: +- Sends chunks as Base64-encoded text (to work with Java proxy) +- Query parameters in URL (for Java proxy's `request.getQueryString()`) +- No cert/key/headers needed (Java proxy adds them) +- Uses Spring Boot endpoint: `POST /upload/chunk` (text/plain, base64) + +**Configuration**: +```bash +# Line 24 +API_BASE="https://myapp.com/v1/utility" +``` + +**Curl format**: +```bash +curl POST /upload/chunk?uploadSessionId=...&fileType=... \ + -H "Content-Type: text/plain" \ + -H "X-Chunk-Encoding: base64" \ + -d @base64_chunk_file +``` + +## Why Two Different Scripts? + +### The Java Proxy Problem + +The Java proxy that sits in front of the Spring Boot app reads the request body as a String: + +```java +@RequestBody(required = false) String body +``` + +**Problem**: Binary multipart data gets corrupted when read as String +**Solution**: Base64 encode chunks as text before sending through the proxy + +### Deploy Script Comparison + +| Feature | deploy-chunked.sh | deploy-chunked-simple.sh | +|---------|-------------------|--------------------------| +| Network Path | Direct to nginx | Through Java proxy | +| Chunk Format | Multipart binary | Base64 text | +| Query Params | No (uses `-F` form fields) | Yes (in URL) | +| Cert/Key | Required in script | Added by Java proxy | +| Headers | Required in script | Added by Java proxy | +| Spring Endpoint | multipart/form-data | text/plain | + +## Spring Boot Endpoints + +The Spring Boot app has **three** chunk upload endpoints: + +### 1. Multipart Endpoint (Original) +```java +@PostMapping("/upload/chunk") +// Consumes: multipart/form-data +// Parameters: All as form fields (-F) +// File: @RequestPart("chunk") MultipartFile +``` +**Used by**: deploy-chunked.sh (direct to nginx) + +### 2. Raw Binary Endpoint +```java +@PostMapping(value = "/upload/chunk", consumes = "application/octet-stream") +// Consumes: application/octet-stream +// Parameters: Query params in URL +// File: @RequestBody byte[] +``` +**Used by**: Not currently used (would fail through Java proxy) + +### 3. Base64 Text Endpoint +```java +@PostMapping(value = "/upload/chunk", consumes = "text/plain") +// Consumes: text/plain +// Parameters: Query params in URL +// File: @RequestBody String (Base64 decoded) +// Header: X-Chunk-Encoding: base64 +``` +**Used by**: deploy-chunked-simple.sh (through Java proxy) + +Spring Boot routes to the correct endpoint based on the `Content-Type` header! + +## Common Configuration (Both Scripts) + +Both scripts share these configuration options: + +```bash +# Files to deploy +JAR_FILE="./app.jar" +MANIFEST_FILE="./manifest.yml" + +# Chunk size (1MB recommended for Tanzu) +CHUNK_SIZE=1048576 + +# Cloud Foundry configuration +CF_API_ENDPOINT="https://api.cf.example.com" +CF_USERNAME="your-username" +CF_PASSWORD="your-password" +CF_ORGANIZATION="your-org" +CF_SPACE="your-space" +CF_APP_NAME="your-app" +CF_SKIP_SSL="false" + +# Polling configuration +POLL_INTERVAL=5 +MAX_WAIT=600 + +# Debug mode +DEBUG_MODE="false" # Set to "true" for verbose output +``` + +## Deployment Flow + +Both scripts follow the same 5-step process: + +1. **Initialize Upload Session**: POST `/upload/init` with CF credentials +2. **Upload JAR Chunks**: POST `/upload/chunk` for each chunk +3. **Upload Manifest Chunks**: POST `/upload/chunk` for manifest.yml +4. **Finalize Upload**: POST `/upload/finalize?uploadSessionId=...&async=true` +5. **Poll Deployment Status**: GET `/deployment/status/{uploadSessionId}` + +## Troubleshooting + +### "Required part 'chunk' is not present" +- **Cause**: Nginx stripped multipart body or wrong Content-Type +- **Solution**: Use deploy-chunked-simple.sh with Base64 encoding + +### "504 Gateway Timeout" on chunk upload +- **Cause**: Java proxy trying to read binary data as String +- **Solution**: Use Base64 encoding (deploy-chunked-simple.sh) + +### "Argument list too long" +- **Cause**: Base64 string passed as command argument instead of file +- **Solution**: Already fixed - script writes Base64 to temp file and uses `-d @file` + +### "Missing uploadSessionId parameter" +- **Cause**: Nginx or proxy stripping query parameters +- **For deploy-chunked.sh**: Parameters should be in form fields (`-F`) +- **For deploy-chunked-simple.sh**: Parameters should be in query string (`?uploadSessionId=...`) + +## Technical Notes + +### Why Not Fix the Java Proxy? + +The Java proxy is shared by multiple services, so modifying it could break other applications. Instead, we adapted the deployment script to work with the proxy's limitations. + +### Why Base64 Encoding? + +When the Java proxy reads binary data as `@RequestBody String body`, it: +- Corrupts binary data (non-UTF8 bytes) +- May hang or timeout on large binary payloads +- Cannot properly forward multipart boundaries + +Base64 encoding converts binary to safe ASCII text that the proxy can handle as a String. + +### Why Query Parameters for Simple Script? + +The Java proxy reconstructs the request using: +```java +String queryParams = request.getQueryString(); +String completeRequest = WSGURL + req; +if (queryParams != null) { + completeRequest = completeRequest + "?" + queryParams; +} +``` + +It only forwards query parameters, not form field parameters, so we must use query strings. + +### Performance Impact of Base64 + +Base64 encoding increases payload size by ~33%: +- 1MB binary chunk → ~1.33MB Base64 text +- Adds CPU overhead for encoding/decoding +- Acceptable tradeoff for proxy compatibility + +## Testing + +### Test deploy-chunked.sh (Direct to nginx) +```bash +# Configure cert/key/headers in script +vim deploy-chunked.sh + +# Run with debug +DEBUG_MODE="true" ./deploy-chunked.sh +``` + +### Test deploy-chunked-simple.sh (Through proxy) +```bash +# Configure API base URL +vim deploy-chunked-simple.sh + +# Run with debug +DEBUG_MODE="true" ./deploy-chunked-simple.sh +``` + +## Dependencies + +### Shell Requirements +- `bash` 4.0+ +- `curl` +- `awk` (replaces `bc` for file size calculation) +- `base64` (for deploy-chunked-simple.sh) +- `mktemp` +- `split` +- `stat` + +### Backend Requirements +- Spring Boot 3.2.0+ +- Java 17+ +- Gradle 8.14 + +## File Reference + +| File | Purpose | +|------|---------| +| `deploy-chunked.sh` | Direct nginx deployment with cert/headers | +| `deploy-chunked-simple.sh` | Java proxy deployment with Base64 | +| `CfDeployController.java` | REST endpoints (3 chunk upload variants) | +| `ChunkedUploadService.java` | Chunk processing (multipart + raw bytes) | +| `AsyncDeploymentService.java` | Background deployment execution | + +## Quick Start + +**For direct nginx access**: +```bash +cp deploy-chunked.sh my-deploy.sh +# Edit configuration +vim my-deploy.sh +# Run +./my-deploy.sh +``` + +**For Java proxy access**: +```bash +cp deploy-chunked-simple.sh my-deploy.sh +# Edit API_BASE +vim my-deploy.sh +# Run +./my-deploy.sh +``` + +## Support + +For issues or questions: +1. Enable `DEBUG_MODE="true"` in the script +2. Check the curl commands and responses +3. Review Spring Boot application logs +4. Verify nginx/proxy logs for request forwarding diff --git a/README.md b/README.md index 03c01a2..70ffe50 100644 --- a/README.md +++ b/README.md @@ -207,12 +207,29 @@ done ``` **For more details, see:** -- `deploy-chunked.sh` - Complete working script +- `deploy-chunked.sh` - Direct nginx deployment with cert/headers +- `deploy-chunked-simple.sh` - Java proxy deployment with Base64 encoding +- **`DEPLOYMENT_SCRIPTS.md`** - **Comprehensive guide for both deployment scripts** ⭐ - `CHUNKED_UPLOAD_GUIDE.md` - Detailed API documentation - `TIMEOUT_SOLUTION.md` - Architecture and design details - `CHUNK_SIZE_GUIDE.md` - Chunk size recommendations - `MEMORY_FIX.md` - JVM memory configuration for Tanzu +### Two Deployment Scripts: Which One to Use? + +We provide **two deployment scripts** for different network configurations: + +| Script | Use When | Features | +|--------|----------|----------| +| **deploy-chunked.sh** | Direct access to nginx | Multipart uploads, cert/key support, custom headers | +| **deploy-chunked-simple.sh** | Through Java proxy | Base64 encoded uploads, proxy adds cert/headers | + +**📖 See [DEPLOYMENT_SCRIPTS.md](DEPLOYMENT_SCRIPTS.md) for detailed comparison, troubleshooting, and technical explanations.** + +**Quick Decision Guide:** +- ✅ Use `deploy-chunked.sh` if you have direct nginx access and need to provide certificates/headers +- ✅ Use `deploy-chunked-simple.sh` if you go through a Java proxy that adds authentication automatically + --- ### 2. List Applications diff --git a/deploy-chunked-simple.sh b/deploy-chunked-simple.sh index 1356181..ac46e6f 100644 --- a/deploy-chunked-simple.sh +++ b/deploy-chunked-simple.sh @@ -1,13 +1,24 @@ #!/bin/bash ############################################################################# -# CF Deployer - Simple Chunked Upload Deployment Script +# CF Deployer - Simple Chunked Upload Deployment Script (Through Java proxy) # # This script deploys a Java application to Cloud Foundry using chunked # uploads to bypass nginx size restrictions and async deployment to avoid # timeout issues. # -# This is a simplified version without cert/key/header authentication. +# USE THIS SCRIPT WHEN: +# - You deploy through a Java proxy that adds certificates/headers +# - The proxy reads request body as @RequestBody String +# - You need to send chunks as Base64-encoded text +# +# For direct nginx deployments with cert/key, use: deploy-chunked.sh +# For detailed documentation, see: DEPLOYMENT_SCRIPTS.md +# +# TECHNICAL NOTES: +# - Chunks are Base64-encoded to prevent corruption in the Java proxy +# - Query parameters are sent in URL (not form fields) +# - Uses Spring Boot endpoint: POST /upload/chunk (Content-Type: text/plain) # # Usage: # ./deploy-chunked-simple.sh diff --git a/deploy-chunked.sh b/deploy-chunked.sh index a14be3d..aa09f68 100644 --- a/deploy-chunked.sh +++ b/deploy-chunked.sh @@ -1,12 +1,20 @@ #!/bin/bash ############################################################################# -# CF Deployer - Chunked Upload Deployment Script +# CF Deployer - Chunked Upload Deployment Script (Direct to nginx) # # This script deploys a Java application to Cloud Foundry using chunked # uploads to bypass nginx size restrictions and async deployment to avoid # timeout issues. # +# USE THIS SCRIPT WHEN: +# - You have direct access to nginx endpoint +# - You need to provide client certificates and custom headers +# - You can send multipart/form-data directly +# +# For deployments through a Java proxy, use: deploy-chunked-simple.sh +# For detailed documentation, see: DEPLOYMENT_SCRIPTS.md +# # Usage: # ./deploy-chunked.sh # diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..da1b7ba --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +dist +.git +.gitignore +README.md +helm diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..fab2275 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,25 @@ +# Node +node_modules/ +npm-debug.log +yarn-error.log + +# Angular +dist/ +.angular/ +.ng_build/ +.ng_pkg_build/ + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Environment +.env +.env.local diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..d43c080 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,33 @@ +# Stage 1: Build Angular app +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build Angular app for production +RUN npm run build:prod + +# Stage 2: Serve with nginx +FROM nginx:alpine + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/nginx.conf.template + +# Copy built Angular app +COPY --from=builder /app/dist/cf-deployer-ui/browser /usr/share/nginx/html + +# Set default backend URL +ENV BACKEND_URL=http://localhost:8080 + +# Replace env variables and start nginx +CMD envsubst '${BACKEND_URL}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;' + +EXPOSE 80 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..914096f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,348 @@ +# Tanzu Deployer UI - Angular Frontend + +A modern, dark-themed Angular 19.1 frontend for deploying applications to Tanzu/Cloud Foundry environments. + +## Features + +- ✅ **Modern Dark Theme**: Sleek GitHub-inspired dark UI with blue/purple gradients +- ✅ **Simple Form Interface**: Easy-to-use deployment form with all Tanzu configuration fields +- ✅ **Modern File Upload**: Beautiful drag-and-drop style file upload buttons with SVG icons +- ✅ **Chunked File Upload**: Handles large JAR files using Base64-encoded chunks (compatible with Java proxy) +- ✅ **Real-time Progress**: Visual progress bar showing upload and deployment status +- ✅ **Live Logs**: Collapsible terminal-style output window displaying deployment logs +- ✅ **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices +- ✅ **Secure**: Password fields are masked, follows Angular security best practices +- ✅ **Configurable**: Environment-based configuration for all settings +- ✅ **Production Ready**: Includes nginx configuration and Helm chart for K8s deployment + +## Configuration + +All application settings are managed via environment files: + +**`src/environments/environment.ts`** (Development): +```typescript +export const environment = { + production: false, + apiBase: '/api/cf', // API endpoint base URL + chunkSize: 1048576, // Chunk size in bytes (1MB) + enableSslValidation: false, // Show/hide SSL validation checkbox + pollInterval: 5000, // Poll interval in milliseconds + maxPollAttempts: 120, // Max polling attempts (10 minutes) + defaultLogsExpanded: true, // Logs expanded by default + showDebugInfo: false // Show debug information +}; +``` + +**`src/environments/environment.prod.ts`** (Production): +- Same structure as development +- Used when building with `--configuration production` + +### Customizing Configuration + +To customize the application behavior: + +1. Edit `src/environments/environment.ts` for development +2. Edit `src/environments/environment.prod.ts` for production +3. Rebuild the application + +**Common Customizations:** + +- **Enable SSL Validation Checkbox**: Set `enableSslValidation: true` +- **Change Chunk Size**: Set `chunkSize: 2097152` (2MB) +- **Increase Poll Time**: Set `maxPollAttempts: 240` (20 minutes) +- **Change API Endpoint**: Set `apiBase: 'https://api.example.com/cf'` + +## Quick Start + +### Local Development + +1. **Install dependencies:** + ```bash + npm install + ``` + +2. **Start development server:** + ```bash + npm start + ``` + +3. **Open browser:** + Navigate to `http://localhost:4200` + +### Build for Production + +```bash +npm run build:prod +``` + +Built files will be in `dist/cf-deployer-ui/browser/` + +## Docker Deployment + +### Build Docker Image + +```bash +docker build -t cf-deployer-ui:latest . +``` + +### Run Docker Container + +```bash +docker run -d \ + -p 8080:80 \ + -e BACKEND_URL=http://cf-deployer-backend:8080 \ + --name cf-deployer-ui \ + cf-deployer-ui:latest +``` + +**Environment Variables:** +- `BACKEND_URL`: URL of the CF Deployer backend API (default: `http://localhost:8080`) + +## Kubernetes Deployment + +### Prerequisites + +- Kubernetes cluster +- Helm 3.x installed +- Docker image pushed to registry + +### Deploy with Helm + +1. **Update values.yaml:** + ```yaml + image: + repository: your-registry/cf-deployer-ui + tag: "1.0.0" + + backend: + url: "http://cf-deployer-backend:8080" + + ingress: + enabled: true + hosts: + - host: cf-deployer.example.com + paths: + - path: / + pathType: Prefix + ``` + +2. **Install the chart:** + ```bash + helm install cf-deployer-ui ./helm + ``` + +3. **Upgrade the chart:** + ```bash + helm upgrade cf-deployer-ui ./helm + ``` + +4. **Uninstall:** + ```bash + helm uninstall cf-deployer-ui + ``` + +### Helm Configuration Options + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `replicaCount` | Number of replicas | `2` | +| `image.repository` | Docker image repository | `cf-deployer-ui` | +| `image.tag` | Docker image tag | `latest` | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | Service port | `80` | +| `backend.url` | Backend API URL | `http://cf-deployer-backend:8080` | +| `ingress.enabled` | Enable ingress | `false` | +| `resources.limits.cpu` | CPU limit | `500m` | +| `resources.limits.memory` | Memory limit | `512Mi` | + +## Usage + +### Deployment Flow + +1. **Fill in Cloud Foundry Details:** + - API Endpoint (e.g., `https://api.cf.example.com`) + - Username and Password + - Organization and Space + - Application Name + - Skip SSL Validation (if needed) + +2. **Select Files:** + - JAR File: Your application JAR + - Manifest File: Cloud Foundry manifest.yml + +3. **Deploy:** + - Click "Deploy to Cloud Foundry" + - Watch progress bar and logs + - Wait for completion + +### Screenshots + +**Main Form:** +- Clean, responsive form with all required fields +- File upload with size display +- SSL validation checkbox + +**Progress Tracking:** +- Visual progress bar (0-100%) +- Current step indicator +- Status badges (IN_PROGRESS, COMPLETED, FAILED) + +**Logs Output:** +- Collapsible terminal-style output +- Timestamped log entries +- Auto-scroll to latest logs + +## Architecture + +### How It Works + +The frontend mimics the behavior of `deploy-chunked-simple.sh`: + +1. **Initialize Upload Session:** + ``` + POST /api/cf/upload/init + → Returns uploadSessionId + ``` + +2. **Upload Files in Chunks:** + - Splits files into 1MB chunks + - Base64 encodes each chunk (for Java proxy compatibility) + - Uploads via: + ``` + POST /api/cf/upload/chunk?uploadSessionId=...&fileType=...&chunkIndex=...&totalChunks=...&fileName=... + Headers: Content-Type: text/plain, X-Chunk-Encoding: base64 + Body: Base64 chunk data + ``` + +3. **Finalize Upload:** + ``` + POST /api/cf/upload/finalize?uploadSessionId=...&async=true + ``` + +4. **Poll Deployment Status:** + ``` + GET /api/cf/deployment/status/{uploadSessionId} + (Every 5 seconds until COMPLETED or FAILED) + ``` + +### Why Base64 Encoding? + +The frontend sends chunks as Base64-encoded text because: +- It goes through a Java proxy that reads `@RequestBody String` +- Binary data gets corrupted when read as String +- Base64 ensures safe text transport through the proxy +- Backend automatically decodes Base64 back to binary + +## Project Structure + +``` +frontend/ +├── src/ +│ ├── app/ +│ │ ├── app.component.ts # Main component with form logic +│ │ ├── app.component.html # Template with form UI +│ │ ├── app.component.css # Component styles +│ │ └── deploy.service.ts # API service +│ ├── index.html # Main HTML file +│ ├── main.ts # Bootstrap file +│ └── styles.css # Global styles +├── helm/ # Helm chart +│ ├── Chart.yaml +│ ├── values.yaml +│ └── templates/ +│ ├── deployment.yaml +│ ├── service.yaml +│ ├── ingress.yaml +│ └── _helpers.tpl +├── nginx.conf # nginx configuration +├── Dockerfile # Multi-stage Docker build +├── angular.json # Angular CLI configuration +├── package.json # NPM dependencies +└── tsconfig.json # TypeScript configuration +``` + +## Development + +### Prerequisites + +- Node.js 20.x or higher +- npm 10.x or higher +- Angular CLI 19.x + +### Install Angular CLI + +```bash +npm install -g @angular/cli@19 +``` + +### Code Structure + +**app.component.ts:** +- Handles form state and validation +- Manages file uploads and chunking +- Polls deployment status +- Updates progress and logs + +**deploy.service.ts:** +- Encapsulates all HTTP API calls +- Returns RxJS Observables converted to Promises +- Handles Base64 encoding headers + +**Styling:** +- Responsive grid layout +- Mobile-first design +- Terminal-style logs with custom scrollbar +- Gradient progress bar + +## Troubleshooting + +### CORS Errors + +If you see CORS errors in the browser console: + +1. **Development:** Configure proxy in `angular.json`: + ```json + { + "serve": { + "options": { + "proxyConfig": "proxy.conf.json" + } + } + } + ``` + + Create `proxy.conf.json`: + ```json + { + "/api": { + "target": "http://localhost:8080", + "secure": false + } + } + ``` + +2. **Production:** nginx handles proxying (already configured) + +### File Upload Fails + +- Check that backend is running and accessible +- Verify `BACKEND_URL` environment variable +- Check browser console for error messages +- Enable DEBUG_MODE in backend to see detailed logs + +### Deployment Timeout + +- Default timeout is 10 minutes (120 attempts × 5 seconds) +- Increase `maxAttempts` in `pollDeploymentStatus()` if needed +- Check backend logs for actual deployment status + +## Browser Support + +- Chrome/Edge (latest) +- Firefox (latest) +- Safari (latest) +- Mobile browsers (iOS Safari, Chrome Android) + +## License + +MIT License - see main project README diff --git a/frontend/angular.json b/frontend/angular.json new file mode 100644 index 0000000..de42938 --- /dev/null +++ b/frontend/angular.json @@ -0,0 +1,76 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "cf-deployer-ui": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/cf-deployer-ui", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.json", + "assets": [], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kB", + "maximumError": "4kB" + } + ], + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "cf-deployer-ui:build:production" + }, + "development": { + "buildTarget": "cf-deployer-ui:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/frontend/helm/Chart.yaml b/frontend/helm/Chart.yaml new file mode 100644 index 0000000..743ab3c --- /dev/null +++ b/frontend/helm/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: cf-deployer-ui +description: Cloud Foundry Deployer UI - Angular frontend +type: application +version: 1.0.0 +appVersion: "1.0.0" diff --git a/frontend/helm/templates/_helpers.tpl b/frontend/helm/templates/_helpers.tpl new file mode 100644 index 0000000..1761f61 --- /dev/null +++ b/frontend/helm/templates/_helpers.tpl @@ -0,0 +1,29 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cf-deployer-ui.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "cf-deployer-ui.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cf-deployer-ui.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} diff --git a/frontend/helm/templates/deployment.yaml b/frontend/helm/templates/deployment.yaml new file mode 100644 index 0000000..1d439c7 --- /dev/null +++ b/frontend/helm/templates/deployment.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "cf-deployer-ui.fullname" . }} + labels: + app: {{ include "cf-deployer-ui.name" . }} + chart: {{ include "cf-deployer-ui.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + app: {{ include "cf-deployer-ui.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ include "cf-deployer-ui.name" . }} + release: {{ .Release.Name }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + env: + - name: BACKEND_URL + value: {{ .Values.backend.url | quote }} + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/frontend/helm/templates/ingress.yaml b/frontend/helm/templates/ingress.yaml new file mode 100644 index 0000000..8fa6712 --- /dev/null +++ b/frontend/helm/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "cf-deployer-ui.fullname" . }} + labels: + app: {{ include "cf-deployer-ui.name" . }} + chart: {{ include "cf-deployer-ui.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "cf-deployer-ui.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/frontend/helm/templates/service.yaml b/frontend/helm/templates/service.yaml new file mode 100644 index 0000000..fdc0c1a --- /dev/null +++ b/frontend/helm/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cf-deployer-ui.fullname" . }} + labels: + app: {{ include "cf-deployer-ui.name" . }} + chart: {{ include "cf-deployer-ui.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ include "cf-deployer-ui.name" . }} + release: {{ .Release.Name }} diff --git a/frontend/helm/values.yaml b/frontend/helm/values.yaml new file mode 100644 index 0000000..6afbad1 --- /dev/null +++ b/frontend/helm/values.yaml @@ -0,0 +1,61 @@ +# Default values for cf-deployer-ui + +replicaCount: 2 + +image: + repository: cf-deployer-ui + pullPolicy: IfNotPresent + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: false + name: "" + +service: + type: ClusterIP + port: 80 + targetPort: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: cf-deployer.example.com + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: cf-deployer-tls + # hosts: + # - cf-deployer.example.com + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + +# Backend service URL +backend: + url: "http://cf-deployer-backend:8080" + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..498d117 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,45 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + # Angular app + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to backend + location /api/ { + proxy_pass ${BACKEND_URL}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Increase timeout for long-running deployments + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + + # Allow large file uploads + client_max_body_size 0; + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..e1678cb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "cf-deployer-ui", + "version": "1.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "build:prod": "ng build --configuration production" + }, + "private": true, + "dependencies": { + "@angular/animations": "^19.1.0", + "@angular/common": "^19.1.0", + "@angular/compiler": "^19.1.0", + "@angular/core": "^19.1.0", + "@angular/forms": "^19.1.0", + "@angular/platform-browser": "^19.1.0", + "@angular/platform-browser-dynamic": "^19.1.0", + "rxjs": "^7.8.0", + "tslib": "^2.6.0", + "zone.js": "^0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.1.0", + "@angular/cli": "^19.1.0", + "@angular/compiler-cli": "^19.1.0", + "typescript": "~5.6.0" + } +} diff --git a/frontend/quick-start.sh b/frontend/quick-start.sh new file mode 100644 index 0000000..6b3389a --- /dev/null +++ b/frontend/quick-start.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +echo "CF Deployer UI - Quick Start" +echo "============================" +echo "" + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "Error: Node.js is not installed" + echo "Please install Node.js 20.x or higher from https://nodejs.org" + exit 1 +fi + +echo "Node.js version: $(node --version)" +echo "npm version: $(npm --version)" +echo "" + +# Check if node_modules exists +if [ ! -d "node_modules" ]; then + echo "Installing dependencies..." + npm install + echo "" +fi + +# Ask for backend URL +read -p "Enter backend URL (default: http://localhost:8080): " BACKEND_URL +BACKEND_URL=${BACKEND_URL:-http://localhost:8080} + +echo "" +echo "Starting development server..." +echo "Backend URL: $BACKEND_URL" +echo "" +echo "The app will be available at: http://localhost:4200" +echo "Press Ctrl+C to stop" +echo "" + +# Start dev server +npm start diff --git a/frontend/src/app/app.component.css b/frontend/src/app/app.component.css new file mode 100644 index 0000000..4d36987 --- /dev/null +++ b/frontend/src/app/app.component.css @@ -0,0 +1,442 @@ +/* Dark Theme Variables */ +:host { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + --bg-hover: #30363d; + --border-color: #30363d; + --text-primary: #c9d1d9; + --text-secondary: #8b949e; + --accent-primary: #58a6ff; + --accent-secondary: #1f6feb; + --success: #3fb950; + --error: #f85149; + --warning: #d29922; +} + +.container { + max-width: 900px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; + padding: 30px; + background: var(--bg-secondary); + color: white; + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); +} + +header h1 { + margin: 0 0 10px 0; + font-size: 2.5em; + font-weight: 700; + background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 25%, #ffaa00 50%, #ff6b35 75%, #f7931e 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + filter: drop-shadow(0 0 20px rgba(255, 107, 53, 0.3)); +} + +header p { + margin: 0; + opacity: 0.9; + font-size: 1.1em; +} + +.card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 30px; + margin-bottom: 20px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.card h2 { + margin: 0 0 20px 0; + color: var(--text-primary); + font-size: 1.5em; +} + +.form-section { + margin-bottom: 30px; +} + +.form-section h3 { + margin: 0 0 20px 0; + color: var(--text-primary); + font-size: 1.2em; + font-weight: 600; + padding-bottom: 10px; + border-bottom: 2px solid var(--border-color); +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: var(--text-primary); + font-size: 14px; +} + +.form-group input[type="text"], +.form-group input[type="password"] { + width: 100%; + padding: 12px 16px; + border: 1px solid var(--border-color); + border-radius: 8px; + font-size: 14px; + box-sizing: border-box; + background: var(--bg-tertiary); + color: var(--text-primary); + transition: all 0.2s ease; +} + +.form-group input[type="text"]:focus, +.form-group input[type="password"]:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1); +} + +.form-group input:disabled { + background-color: var(--bg-primary); + cursor: not-allowed; + opacity: 0.6; +} + +/* Modern File Upload */ +.file-upload-wrapper { + position: relative; + margin-bottom: 10px; +} + +.file-input { + position: absolute; + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + z-index: -1; +} + +.file-upload-btn { + display: inline-flex; + align-items: center; + gap: 24px; + padding: 12px 24px; + background: var(--bg-tertiary); + border: 2px dashed var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + font-size: 14px; +} + +.file-upload-btn:hover { + background: var(--bg-hover); + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +.file-upload-btn.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.file-upload-btn svg { + flex-shrink: 0; +} + +.file-info { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; +} + +.file-info svg { + flex-shrink: 0; + color: var(--accent-primary); +} + +.file-size { + color: var(--text-secondary); + margin-left: auto; +} + +.form-group.checkbox { + display: flex; + align-items: center; +} + +.form-group.checkbox label { + margin: 0; + font-weight: normal; + cursor: pointer; + display: flex; + align-items: center; + color: var(--text-primary); +} + +.form-group.checkbox input { + width: auto; + margin-right: 10px; + cursor: pointer; + width: 18px; + height: 18px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.form-actions { + display: flex; + gap: 12px; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid var(--border-color); + justify-content: center; +} + +.btn { + padding: 14px 28px; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%); + color: white; + box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3); +} + +.btn-primary:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(255, 107, 53, 0.4); +} + +.btn-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--bg-hover); + border-color: var(--accent-primary); +} + +.progress-section { + margin-top: 10px; +} + +.progress-label { + display: flex; + justify-content: space-between; + margin-bottom: 10px; + font-weight: 600; + color: var(--text-primary); + font-size: 15px; +} + +.progress-percent { + color: var(--accent-primary); + font-variant-numeric: tabular-nums; +} + +.progress-bar { + height: 32px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; + position: relative; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #1f6feb 0%, #8957e5 100%); + transition: width 0.3s ease; + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 12px; + color: white; + font-weight: 600; + box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.1); +} + +.status-badge { + margin-top: 20px; + text-align: center; +} + +.badge { + display: inline-block; + padding: 10px 20px; + border-radius: 8px; + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.badge-success { + background: rgba(63, 185, 80, 0.15); + color: var(--success); + border: 1px solid var(--success); +} + +.badge-error { + background: rgba(248, 81, 73, 0.15); + color: var(--error); + border: 1px solid var(--error); +} + +.badge-info { + background: rgba(88, 166, 255, 0.15); + color: var(--accent-primary); + border: 1px solid var(--accent-primary); +} + +.logs-card { + background: var(--bg-primary); + border: 1px solid var(--border-color); +} + +.logs-header { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + user-select: none; + padding: 5px; + border-radius: 6px; + transition: background 0.2s ease; +} + +.logs-header:hover { + background: var(--bg-hover); +} + +.logs-header h2 { + color: var(--text-primary); + margin: 0; + font-size: 1.3em; +} + +.toggle-icon { + font-size: 1.2em; + color: var(--text-secondary); + transition: transform 0.2s ease; +} + +.logs-output { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + margin-top: 16px; + max-height: 500px; + overflow-y: auto; + font-family: 'Courier New', 'Consolas', monospace; + font-size: 13px; + line-height: 1.6; +} + +.log-line { + padding: 4px 0; + color: var(--text-secondary); + border-bottom: 1px solid rgba(48, 54, 61, 0.3); +} + +.log-line:last-child { + border-bottom: none; +} + +/* Scrollbar styling */ +.logs-output::-webkit-scrollbar { + width: 10px; +} + +.logs-output::-webkit-scrollbar-track { + background: var(--bg-primary); + border-radius: 4px; +} + +.logs-output::-webkit-scrollbar-thumb { + background: var(--bg-hover); + border-radius: 4px; +} + +.logs-output::-webkit-scrollbar-thumb:hover { + background: #484f58; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 10px; + } + + header { + padding: 20px; + } + + header h1 { + font-size: 1.8em; + } + + .form-row { + grid-template-columns: 1fr; + } + + .form-actions { + flex-direction: column; + } + + .btn { + width: 100%; + } + + .card { + padding: 20px; + } + + .logs-output { + max-height: 300px; + font-size: 12px; + } +} diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html new file mode 100644 index 0000000..7a9c2b8 --- /dev/null +++ b/frontend/src/app/app.component.html @@ -0,0 +1,217 @@ +
Deploy your applications to Tanzu Platform
+