Compare commits

..

2 Commits

Author SHA1 Message Date
pratik
2c9641ea8a ignore package lock 2025-10-23 08:11:34 -05:00
pratik
c35cd7092a Add frontend 2025-10-23 08:10:15 -05:00
29 changed files with 2244 additions and 4 deletions

1
.gitignore vendored
View File

@@ -76,3 +76,4 @@ src/main/resources/cf-cli/
# Test output
test-output/
target/
frontend/package-lock.json

298
DEPLOYMENT_SCRIPTS.md Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
#

7
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules
npm-debug.log
dist
.git
.gitignore
README.md
helm

25
frontend/.gitignore vendored Normal file
View File

@@ -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

33
frontend/Dockerfile Normal file
View File

@@ -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

348
frontend/README.md Normal file
View File

@@ -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

76
frontend/angular.json Normal file
View File

@@ -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
}
}

6
frontend/helm/Chart.yaml Normal file
View File

@@ -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"

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 }}

61
frontend/helm/values.yaml Normal file
View File

@@ -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: {}

45
frontend/nginx.conf Normal file
View File

@@ -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;
}
}
}

29
frontend/package.json Normal file
View File

@@ -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"
}
}

38
frontend/quick-start.sh Normal file
View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -0,0 +1,217 @@
<div class="container">
<header>
<h1>Tanzu Deployer</h1>
<p>Deploy your applications to Tanzu Platform</p>
</header>
<div class="card">
<form (ngSubmit)="deploy()" #deployForm="ngForm">
<!-- Cloud Foundry Configuration -->
<div class="form-section">
<h3>Tanzu Configuration</h3>
<div class="form-group">
<label for="apiEndpoint">API Endpoint *</label>
<input
type="text"
id="apiEndpoint"
name="apiEndpoint"
[(ngModel)]="apiEndpoint"
placeholder="https://api.cf.example.com"
required
[disabled]="uploading">
</div>
<div class="form-row">
<div class="form-group">
<label for="username">Username *</label>
<input
type="text"
id="username"
name="username"
[(ngModel)]="username"
placeholder="your-username"
required
[disabled]="uploading">
</div>
<div class="form-group">
<label for="password">Password *</label>
<input
type="password"
id="password"
name="password"
[(ngModel)]="password"
placeholder="••••••••"
required
[disabled]="uploading">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="organization">Organization *</label>
<input
type="text"
id="organization"
name="organization"
[(ngModel)]="organization"
placeholder="your-org"
required
[disabled]="uploading">
</div>
<div class="form-group">
<label for="space">Space *</label>
<input
type="text"
id="space"
name="space"
[(ngModel)]="space"
placeholder="dev"
required
[disabled]="uploading">
</div>
</div>
<div class="form-group">
<label for="appName">Application Name *</label>
<input
type="text"
id="appName"
name="appName"
[(ngModel)]="appName"
placeholder="my-app"
required
[disabled]="uploading">
</div>
<div class="form-group checkbox" *ngIf="env.enableSslValidation">
<label>
<input
type="checkbox"
name="skipSsl"
[(ngModel)]="skipSsl"
[disabled]="uploading">
Skip SSL Validation
</label>
</div>
</div>
<!-- Files Upload -->
<div class="form-section">
<h3>Application Files</h3>
<div class="form-group">
<label for="jarFile">JAR File *</label>
<div class="file-upload-wrapper">
<input
type="file"
id="jarFile"
name="jarFile"
class="file-input"
(change)="onJarFileChange($event)"
accept=".jar"
required
[disabled]="uploading">
<label for="jarFile" class="file-upload-btn" [class.disabled]="uploading">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span *ngIf="!jarFile">Choose JAR File</span>
<span *ngIf="jarFile">Change File</span>
</label>
</div>
<div class="file-info" *ngIf="jarFile">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
<polyline points="13 2 13 9 20 9"></polyline>
</svg>
<span>{{ jarFile.name }}</span>
<span class="file-size">({{ (jarFile.size / 1024 / 1024).toFixed(2) }}MB)</span>
</div>
</div>
<div class="form-group">
<label for="manifestFile">Manifest File *</label>
<div class="file-upload-wrapper">
<input
type="file"
id="manifestFile"
name="manifestFile"
class="file-input"
(change)="onManifestFileChange($event)"
accept=".yml,.yaml"
required
[disabled]="uploading">
<label for="manifestFile" class="file-upload-btn" [class.disabled]="uploading">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span *ngIf="!manifestFile">Choose Manifest File</span>
<span *ngIf="manifestFile">Change File</span>
</label>
</div>
<div class="file-info" *ngIf="manifestFile">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
<polyline points="13 2 13 9 20 9"></polyline>
</svg>
<span>{{ manifestFile.name }}</span>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="form-actions">
<button type="submit" class="btn btn-primary" [disabled]="!deployForm.form.valid || uploading">
<span *ngIf="!uploading">Deploy to Tanzu</span>
<span *ngIf="uploading">Deploying...</span>
</button>
<button type="button" class="btn btn-secondary" (click)="clearForm()" [disabled]="uploading">
Clear Form
</button>
</div>
</form>
</div>
<!-- Progress Section -->
<div class="card" *ngIf="uploading || deploymentStatus">
<h2>Deployment Progress</h2>
<div class="progress-section">
<div class="progress-label">
<span>{{ currentStep }}</span>
<span class="progress-percent">{{ uploadProgress }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" [style.width.%]="uploadProgress"></div>
</div>
<div class="status-badge" *ngIf="deploymentStatus">
<span class="badge" [class.badge-success]="deploymentStatus === 'COMPLETED'"
[class.badge-error]="deploymentStatus === 'FAILED'"
[class.badge-info]="deploymentStatus === 'IN_PROGRESS'">
{{ deploymentStatus }}
</span>
</div>
</div>
</div>
<!-- Logs Section -->
<div class="card logs-card" *ngIf="logs.length > 0">
<div class="logs-header" (click)="toggleLogs()">
<h2>Deployment Logs</h2>
<span class="toggle-icon">{{ logsExpanded ? '▼' : '▶' }}</span>
</div>
<div class="logs-output" *ngIf="logsExpanded">
<div class="log-line" *ngFor="let log of logs">{{ log }}</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,217 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DeployService } from './deploy.service';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
// Environment config
env = environment;
// Form fields
apiEndpoint = '';
username = '';
password = '';
organization = '';
space = '';
appName = '';
skipSsl = false;
jarFile: File | null = null;
manifestFile: File | null = null;
// Upload state
uploading = false;
uploadProgress = 0;
currentStep = '';
logs: string[] = [];
logsExpanded = environment.defaultLogsExpanded;
deploymentStatus = '';
sessionId = '';
constructor(private deployService: DeployService) {}
onJarFileChange(event: any) {
this.jarFile = event.target.files[0];
}
onManifestFileChange(event: any) {
this.manifestFile = event.target.files[0];
}
addLog(message: string) {
const timestamp = new Date().toLocaleTimeString();
this.logs.push(`[${timestamp}] ${message}`);
}
async deploy() {
if (!this.jarFile || !this.manifestFile) {
alert('Please select both JAR and Manifest files');
return;
}
this.uploading = true;
this.uploadProgress = 0;
this.logs = [];
this.deploymentStatus = '';
try {
// Step 1: Initialize upload session
this.currentStep = 'Initializing upload session...';
this.addLog('Step 1/5: Initializing upload session');
const config = {
apiEndpoint: this.apiEndpoint,
username: this.username,
password: this.password,
organization: this.organization,
space: this.space,
appName: this.appName,
skipSslValidation: this.skipSsl
};
const initResponse = await this.deployService.initUpload(config);
this.sessionId = initResponse.uploadSessionId;
this.addLog(`✓ Session created: ${this.sessionId}`);
this.uploadProgress = 10;
// Step 2: Upload JAR file
this.currentStep = 'Uploading JAR file...';
this.addLog(`Step 2/5: Uploading JAR file (${(this.jarFile.size / 1024 / 1024).toFixed(2)}MB)`);
await this.uploadFileInChunks(this.jarFile, 'jarFile', 10, 50);
this.addLog('✓ JAR file upload completed');
// Step 3: Upload manifest file
this.currentStep = 'Uploading manifest file...';
this.addLog('Step 3/5: Uploading manifest file');
await this.uploadFileInChunks(this.manifestFile, 'manifest', 50, 60);
this.addLog('✓ Manifest file upload completed');
// Step 4: Finalize and start deployment
this.currentStep = 'Starting deployment...';
this.addLog('Step 4/5: Starting async deployment');
const finalizeResponse = await this.deployService.finalizeUpload(this.sessionId);
this.addLog(`✓ Deployment started (Status: ${finalizeResponse.status})`);
this.uploadProgress = 70;
// Step 5: Poll deployment status
this.currentStep = 'Deploying application...';
this.addLog('Step 5/5: Polling deployment status');
await this.pollDeploymentStatus();
} catch (error: any) {
this.addLog(`✗ ERROR: ${error.message || error}`);
this.currentStep = 'Deployment failed';
this.deploymentStatus = 'FAILED';
this.uploading = false;
}
}
async uploadFileInChunks(file: File, fileType: string, startProgress: number, endProgress: number) {
const CHUNK_SIZE = environment.chunkSize;
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
// Convert chunk to base64
const base64Chunk = await this.fileToBase64(chunk);
await this.deployService.uploadChunk(
this.sessionId,
fileType,
file.name,
chunkIndex,
totalChunks,
base64Chunk
);
const progress = startProgress + ((chunkIndex + 1) / totalChunks) * (endProgress - startProgress);
this.uploadProgress = Math.round(progress);
this.addLog(` Chunk ${chunkIndex + 1}/${totalChunks} uploaded`);
}
}
async fileToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const base64 = (reader.result as string).split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
async pollDeploymentStatus() {
const maxAttempts = environment.maxPollAttempts;
let attempts = 0;
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, environment.pollInterval));
const status = await this.deployService.getDeploymentStatus(this.sessionId);
this.deploymentStatus = status.status;
const progressPercent = 70 + (status.progress || 0) * 0.3;
this.uploadProgress = Math.round(progressPercent);
this.addLog(` Status: ${status.status} - ${status.message || ''}`);
if (status.status === 'COMPLETED') {
this.currentStep = 'Deployment completed successfully!';
this.uploadProgress = 100;
this.uploading = false;
this.addLog('✓ Deployment completed successfully!');
return;
}
if (status.status === 'FAILED') {
this.currentStep = 'Deployment failed';
this.uploading = false;
this.addLog(`✗ Deployment failed: ${status.message}`);
if (status.error) {
this.addLog(` Error: ${status.error}`);
}
throw new Error('Deployment failed');
}
attempts++;
}
throw new Error('Deployment timeout after 10 minutes');
}
toggleLogs() {
this.logsExpanded = !this.logsExpanded;
}
clearForm() {
this.apiEndpoint = '';
this.username = '';
this.password = '';
this.organization = '';
this.space = '';
this.appName = '';
this.skipSsl = false;
this.jarFile = null;
this.manifestFile = null;
this.logs = [];
this.uploadProgress = 0;
this.currentStep = '';
this.deploymentStatus = '';
}
}

View File

@@ -0,0 +1,97 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../environments/environment';
export interface CfConfig {
apiEndpoint: string;
username: string;
password: string;
organization: string;
space: string;
appName: string;
skipSslValidation: boolean;
}
export interface InitUploadResponse {
success: boolean;
uploadSessionId: string;
message: string;
}
export interface ChunkUploadResponse {
success: boolean;
uploadSessionId: string;
message: string;
}
export interface FinalizeResponse {
uploadSessionId: string;
status: string;
message: string;
progress: number;
}
export interface DeploymentStatus {
uploadSessionId: string;
status: string;
message: string;
progress: number;
output?: string;
error?: string;
}
@Injectable({
providedIn: 'root'
})
export class DeployService {
private apiBase = environment.apiBase;
constructor(private http: HttpClient) {}
async initUpload(config: CfConfig): Promise<InitUploadResponse> {
const url = `${this.apiBase}/upload/init`;
return firstValueFrom(
this.http.post<InitUploadResponse>(url, config)
);
}
async uploadChunk(
sessionId: string,
fileType: string,
fileName: string,
chunkIndex: number,
totalChunks: number,
base64Data: string
): Promise<ChunkUploadResponse> {
const url = `${this.apiBase}/upload/chunk?uploadSessionId=${sessionId}&fileType=${fileType}&chunkIndex=${chunkIndex}&totalChunks=${totalChunks}&fileName=${encodeURIComponent(fileName)}`;
const headers = new HttpHeaders({
'Content-Type': 'text/plain',
'X-Chunk-Encoding': 'base64'
});
return firstValueFrom(
this.http.post<ChunkUploadResponse>(url, base64Data, { headers })
);
}
async finalizeUpload(sessionId: string): Promise<FinalizeResponse> {
const url = `${this.apiBase}/upload/finalize?uploadSessionId=${sessionId}&async=true`;
const headers = new HttpHeaders({
'Content-Length': '0'
});
return firstValueFrom(
this.http.post<FinalizeResponse>(url, null, { headers })
);
}
async getDeploymentStatus(sessionId: string): Promise<DeploymentStatus> {
const url = `${this.apiBase}/deployment/status/${sessionId}`;
return firstValueFrom(
this.http.get<DeploymentStatus>(url)
);
}
}

View File

@@ -0,0 +1,20 @@
export const environment = {
production: true,
// API Configuration
apiBase: '/api/cf',
// Upload Configuration
chunkSize: 1048576, // 1MB in bytes
// Feature Flags
enableSslValidation: false, // Show/hide SSL validation checkbox
// Polling Configuration
pollInterval: 5000, // 5 seconds in milliseconds
maxPollAttempts: 120, // 10 minutes total (120 * 5 seconds)
// UI Configuration
defaultLogsExpanded: true,
showDebugInfo: false
};

View File

@@ -0,0 +1,20 @@
export const environment = {
production: false,
// API Configuration
apiBase: '/api/cf',
// Upload Configuration
chunkSize: 1048576, // 1MB in bytes
// Feature Flags
enableSslValidation: false, // Show/hide SSL validation checkbox
// Polling Configuration
pollInterval: 5000, // 5 seconds in milliseconds
maxPollAttempts: 120, // 10 minutes total (120 * 5 seconds)
// UI Configuration
defaultLogsExpanded: true,
showDebugInfo: false
};

12
frontend/src/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CF Deployer</title>
<base href="/">
</head>
<body>
<app-root></app-root>
</body>
</html>

9
frontend/src/main.ts Normal file
View File

@@ -0,0 +1,9 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient()
]
}).catch(err => console.error(err));

17
frontend/src/styles.css Normal file
View File

@@ -0,0 +1,17 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #010409;
min-height: 100vh;
padding: 20px 0;
color: #c9d1d9;
}
* {
box-sizing: border-box;
}

30
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}