Files
2026-05-31 16:07:30 +02:00

832 lines
24 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================
# z/OS Web Console — Project Setup Script
# Run this on your Docker/build machine to create all files
# ============================================================
set -e
echo “🚀 Creating z/OS Web Console project…”
# ── Directory structure ──────────────────────────────────────
mkdir -p zos-web/backend
mkdir -p zos-web/frontend/src/components
mkdir -p zos-web/frontend/public
mkdir -p zos-web/k8s
cd zos-web
# ============================================================
# BACKEND
# ============================================================
cat > backend/tn3270_service.py << PYEOF
“””
TN3270 Session Service
Manages a persistent s3270 connection to z/OS
“””
import subprocess
import time
import os
import threading
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(**name**)
class TN3270Session:
def **init**(self):
self.host = os.environ.get(“ZOS_HOST”, “192.168.2.243”)
self.port = os.environ.get(“ZOS_PORT”, “23”)
self.userid = os.environ.get(“ZOS_USERID”, “IBMUSER”)
self.password = os.environ.get(“ZOS_PASSWORD”, “allard”)
self.proc = os.environ.get(“ZOS_PROC”, “DBSPROC9”)
self.acct = os.environ.get(“ZOS_ACCT”, “ACCT#”)
self.size = os.environ.get(“ZOS_SIZE”, “2048000”)
self.s3270 = None
self.lock = threading.Lock()
self.connected = False
```
def _start_s3270(self):
self.s3270 = subprocess.Popen(
["s3270", "-model", "3279-2", f"{self.host}:{self.port}"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
time.sleep(2)
def _send(self, command: str):
self.s3270.stdin.write(command + "\n")
self.s3270.stdin.flush()
return self._read_response()
def _read_response(self):
lines = []
while True:
line = self.s3270.stdout.readline()
if not line:
return False, lines
line = line.rstrip("\n")
if line == "ok":
return True, lines
if line == "error":
return False, lines
lines.append(line)
def _type(self, text: str):
escaped = text.replace("\\", "\\\\").replace(")", "\\)").replace("(", "\\(")
self._send(f"String({escaped})")
def _tab(self): self._send("Tab()")
def _enter(self): self._send("Enter()")
def _pf(self, n): self._send(f"PF({n})")
def connect(self):
logger.info(f"Connecting to {self.host}:{self.port}")
self._start_s3270()
time.sleep(3)
self._send("Home()")
self._type(self.userid); self._tab()
self._type(self.password); self._tab()
self._tab() # New Password
self._tab() # Procedure (pre-filled)
self._tab() # Group Ident
self._type(self.acct); self._tab()
self._tab() # Size (pre-filled)
self._tab() # Perform
self._type("ispf")
self._enter()
time.sleep(5)
self.connected = True
logger.info("Connected to z/OS — ISPF loaded")
def disconnect(self):
try:
self._type("=x"); self._enter(); time.sleep(1)
finally:
self.s3270.terminate()
self.connected = False
def get_screen(self) -> str:
with self.lock:
ok, lines = self._send("Ascii()")
return "\n".join(lines)
def send_command(self, command: str) -> str:
with self.lock:
self._type(command); self._enter()
time.sleep(0.8)
return self.get_screen()
def send_pf(self, key: int) -> str:
with self.lock:
self._pf(key); time.sleep(0.5)
return self.get_screen()
def send_tab(self) -> str:
with self.lock:
self._tab()
return self.get_screen()
def navigate_ispf(self, option: str) -> str:
return self.send_command(option)
def submit_jcl(self, jcl_lines: list) -> str:
with self.lock:
self._type("=6"); self._enter(); time.sleep(1)
return self.get_screen()
def list_datasets(self, hlq: str) -> str:
with self.lock:
self._type("=3.4"); self._enter(); time.sleep(1)
self._send("Home()")
self._type(hlq); self._enter(); time.sleep(1)
return self.get_screen()
def open_sdsf(self) -> str:
with self.lock:
self._type("=m.sdsf"); self._enter(); time.sleep(2)
return self.get_screen()
def sdsf_st(self) -> str:
with self.lock:
self._type("ST"); self._enter(); time.sleep(1)
return self.get_screen()
```
PYEOF
cat > backend/main.py << PYEOF
“””
z/OS Web API — FastAPI Backend
“””
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from contextlib import asynccontextmanager
from tn3270_service import TN3270Session
import asyncio, logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(**name**)
session: TN3270Session = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global session
session = TN3270Session()
try:
session.connect()
logger.info(“✅ z/OS session established”)
except Exception as e:
logger.error(f”❌ Failed to connect: {e})
yield
if session and session.connected:
session.disconnect()
app = FastAPI(title=“z/OS Web API”, version=“1.0.0”, lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=[”*”], allow_methods=[”*”], allow_headers=[”*”])
class CommandRequest(BaseModel): text: str
class JCLRequest(BaseModel): jcl: list[str]
class DatasetRequest(BaseModel): hlq: str
def check():
if not session or not session.connected:
raise HTTPException(503, “Not connected to z/OS”)
@app.get(”/health”)
def health():
return {“status”: “ok”, “connected”: session.connected if session else False}
@app.get(”/screen”)
def get_screen():
check(); return {“screen”: session.get_screen()}
@app.post(”/command”)
def send_command(req: CommandRequest):
check(); return {“screen”: session.send_command(req.text)}
@app.post(”/pf/{key})
def send_pf(key: int):
check()
if not 1 <= key <= 24: raise HTTPException(400, “PF key must be 1-24”)
return {“screen”: session.send_pf(key)}
@app.post(”/tab”)
def send_tab():
check(); return {“screen”: session.send_tab()}
@app.post(”/ispf/navigate”)
def navigate(req: CommandRequest):
check(); return {“screen”: session.navigate_ispf(req.text)}
@app.post(”/jcl/submit”)
def submit_jcl(req: JCLRequest):
check(); return {“screen”: session.submit_jcl(req.jcl)}
@app.post(”/datasets/list”)
def list_datasets(req: DatasetRequest):
check(); return {“screen”: session.list_datasets(req.hlq)}
@app.get(”/sdsf”)
def open_sdsf():
check(); return {“screen”: session.open_sdsf()}
@app.get(”/sdsf/st”)
def sdsf_st():
check(); return {“screen”: session.sdsf_st()}
@app.websocket(”/ws/screen”)
async def ws_screen(websocket: WebSocket):
await websocket.accept()
last_screen = “”
try:
while True:
if session and session.connected:
screen = session.get_screen()
if screen != last_screen:
await websocket.send_json({“screen”: screen})
last_screen = screen
await asyncio.sleep(0.5)
except WebSocketDisconnect:
pass
PYEOF
cat > backend/requirements.txt << EOF
fastapi==0.115.0
uvicorn==0.30.6
pydantic==2.9.2
websockets==13.1
EOF
cat > backend/Dockerfile << EOF
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y python3 python3-pip x3270
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip3 install break-system-packages -r requirements.txt
COPY . .
EXPOSE 8000
CMD [“uvicorn”, “main:app”, “–host”, “0.0.0.0”, “–port”, “8000”, “–log-level”, “info”]
EOF
echo “✅ backend files created”
# ============================================================
# FRONTEND
# ============================================================
cat > frontend/public/index.html << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>z/OS Web Console</title>
<style>* { margin: 0; padding: 0; box-sizing: border-box; } body { background: #0a0f0a; }</style>
</head>
<body><div id="root"></div></body>
</html>
EOF
cat > frontend/src/index.js << EOF
import React from react;
import ReactDOM from react-dom/client;
import App from ./App;
const root = ReactDOM.createRoot(document.getElementById(root));
root.render(<React.StrictMode><App /></React.StrictMode>);
EOF
cat > frontend/src/App.jsx << JSEOF
import { useState, useEffect, useRef, useCallback } from “react”;
const API = “http://192.168.2.100/api”;
const WS = “ws://192.168.2.100/api/ws/screen”;
const C = {
bg: “#0a0f0a”,
bgPanel: “#0d140d”,
green: “#00ff41”,
greenDim: “#00aa2a”,
greenDark: “#004d10”,
amber: “#ffb000”,
red: “#ff3333”,
border: “#1a3a1a”,
};
async function api(path, body) {
const opts = body
? { method: “POST”, headers: { “Content-Type”: “application/json” }, body: JSON.stringify(body) }
: { method: “GET” };
const r = await fetch(API + path, opts);
return r.json();
}
function TerminalScreen({ screen }) {
return (
<pre style={{
fontFamily: “‘Courier New, Courier, monospace”,
fontSize: “13px”, lineHeight: “1.4”,
color: C.green, background: C.bg,
padding: “12px 16px”, margin: 0,
whiteSpace: “pre”, overflowX: “auto”,
minHeight: “340px”,
textShadow: `0 0 8px ${C.green}`,
border: `1px solid ${C.greenDark}`,
borderRadius: “4px”,
}}>
{screen || “Connecting to z/OS…”}
</pre>
);
}
function StatusBar({ connected, message }) {
return (
<div style={{
display: “flex”, alignItems: “center”, gap: “12px”,
padding: “6px 16px”, background: C.bgPanel,
borderBottom: `1px solid ${C.border}`,
fontSize: “11px”, fontFamily: “monospace”, color: C.greenDim,
}}>
<span style={{ color: connected ? C.green : C.red, textShadow: “0 0 6px currentColor” }}>
{connected ? “● ONLINE” : “● OFFLINE”}
</span>
<span>z/OS 192.168.2.243</span>
<span>|</span><span>IBMUSER</span>
<span>|</span><span>DBSPROC9</span>
{message && <><span>|</span><span style={{ color: C.amber }}>{message}</span></>}
</div>
);
}
function PFBar({ onPF }) {
return (
<div style={{
display: “flex”, flexWrap: “wrap”, gap: “4px”,
padding: “8px 12px”, background: C.bgPanel,
borderTop: `1px solid ${C.border}`,
}}>
{[1,2,3,4,5,6,7,8,9,10,11,12].map(k => (
<button key={k} onClick={() => onPF(k)} style={{
background: “transparent”, border: `1px solid ${C.greenDark}`,
color: C.greenDim, fontFamily: “monospace”,
fontSize: “11px”, padding: “2px 8px”, cursor: “pointer”, borderRadius: “2px”,
}}
onMouseEnter={e => { e.target.style.color = C.green; e.target.style.borderColor = C.green; }}
onMouseLeave={e => { e.target.style.color = C.greenDim; e.target.style.borderColor = C.greenDark; }}>
F{k}
</button>
))}
</div>
);
}
function CommandInput({ onSend, disabled }) {
const [cmd, setCmd] = useState(””);
const [history, setHistory] = useState([]);
const [hIdx, setHIdx] = useState(-1);
const submit = () => {
if (!cmd.trim()) return;
setHistory(h => [cmd, …h].slice(0, 50));
setHIdx(-1); onSend(cmd); setCmd(””);
};
const onKey = e => {
if (e.key === “Enter”) submit();
if (e.key === “ArrowUp”) { const i = hIdx+1; if (i < history.length) { setHIdx(i); setCmd(history[i]); } }
if (e.key === “ArrowDown”) { const i = hIdx-1; if (i >= 0) { setHIdx(i); setCmd(history[i]); } else { setHIdx(-1); setCmd(””); } }
};
return (
<div style={{
display: “flex”, alignItems: “center”, gap: “8px”,
padding: “8px 12px”, background: C.bgPanel,
borderTop: `1px solid ${C.border}`,
}}>
<span style={{ color: C.greenDim, fontFamily: “monospace”, fontSize: “13px” }}>COMMAND ===></span>
<input value={cmd} onChange={e => setCmd(e.target.value)} onKeyDown={onKey} disabled={disabled}
style={{
flex: 1, background: “transparent”, border: “none”,
borderBottom: `1px solid ${C.greenDark}`,
color: C.green, fontFamily: “monospace”, fontSize: “13px”,
outline: “none”, padding: “2px 4px”, caretColor: C.green,
}}
placeholder=“Type command or ISPF option (e.g. =3.4)…”
autoFocus
/>
<button onClick={submit} disabled={disabled} style={{
background: “transparent”, border: `1px solid ${C.green}`,
color: C.green, fontFamily: “monospace”,
fontSize: “12px”, padding: “4px 12px”, cursor: “pointer”, borderRadius: “2px”,
}}>ENTER</button>
</div>
);
}
function JCLPanel({ onClose }) {
const [jcl, setJcl] = useState(`//MYJOB JOB (ACCT#),'MY JOB',CLASS=A,MSGCLASS=X\n//STEP1 EXEC PGM=IEFBR14\n//`);
const [result, setResult] = useState(””);
const submit = async () => {
const r = await api(”/jcl/submit”, { jcl: jcl.split(\n) });
setResult(r.screen);
};
return (
<div style={{ position: “fixed”, inset: 0, background: “rgba(0,0,0,0.85)”, display: “flex”, alignItems: “center”, justifyContent: “center”, zIndex: 100 }}>
<div style={{ width: “700px”, background: C.bgPanel, border: `1px solid ${C.green}`, borderRadius: “4px”, padding: “20px”, fontFamily: “monospace” }}>
<div style={{ display: “flex”, justifyContent: “space-between”, marginBottom: “12px” }}>
<span style={{ color: C.green }}>── JCL SUBMIT ──</span>
<button onClick={onClose} style={{ background: “transparent”, border: “none”, color: C.red, cursor: “pointer”, fontSize: “16px” }}>✕</button>
</div>
<textarea value={jcl} onChange={e => setJcl(e.target.value)} rows={12}
style={{ width: “100%”, boxSizing: “border-box”, background: C.bg, color: C.green, border: `1px solid ${C.greenDark}`, fontFamily: “monospace”, fontSize: “13px”, padding: “8px”, resize: “vertical” }} />
<button onClick={submit} style={{ background: “transparent”, border: `1px solid ${C.green}`, color: C.green, fontFamily: “monospace”, padding: “6px 16px”, cursor: “pointer”, marginTop: “8px” }}>SUBMIT JCL</button>
{result && <TerminalScreen screen={result} />}
</div>
</div>
);
}
function DatasetPanel({ onClose }) {
const [hlq, setHlq] = useState(“IBMUSER”);
const browse = async () => { await api(”/datasets/list”, { hlq }); onClose(); };
return (
<div style={{ position: “fixed”, inset: 0, background: “rgba(0,0,0,0.85)”, display: “flex”, alignItems: “center”, justifyContent: “center”, zIndex: 100 }}>
<div style={{ width: “420px”, background: C.bgPanel, border: `1px solid ${C.green}`, borderRadius: “4px”, padding: “20px”, fontFamily: “monospace” }}>
<div style={{ display: “flex”, justifyContent: “space-between”, marginBottom: “12px” }}>
<span style={{ color: C.green }}>── DATASET LIST (ISPF 3.4) ──</span>
<button onClick={onClose} style={{ background: “transparent”, border: “none”, color: C.red, cursor: “pointer” }}>✕</button>
</div>
<div style={{ display: “flex”, alignItems: “center”, gap: “8px”, marginBottom: “12px” }}>
<span style={{ color: C.greenDim }}>HLQ ===></span>
<input value={hlq} onChange={e => setHlq(e.target.value)}
style={{ flex: 1, background: “transparent”, border: “none”, borderBottom: `1px solid ${C.greenDark}`, color: C.green, fontFamily: “monospace”, fontSize: “13px”, outline: “none”, padding: “2px 4px” }} />
</div>
<button onClick={browse} style={{ background: “transparent”, border: `1px solid ${C.green}`, color: C.green, fontFamily: “monospace”, padding: “6px 16px”, cursor: “pointer” }}>BROWSE DATASETS</button>
</div>
</div>
);
}
function Toolbar({ onJCL, onDatasets, onSDSF }) {
const btn = (label, onClick) => (
<button onClick={onClick} style={{
background: “transparent”, border: `1px solid ${C.greenDark}`,
color: C.greenDim, fontFamily: “monospace”,
fontSize: “11px”, padding: “4px 12px”, cursor: “pointer”, borderRadius: “2px”,
}}
onMouseEnter={e => { e.target.style.color = C.green; e.target.style.borderColor = C.green; }}
onMouseLeave={e => { e.target.style.color = C.greenDim; e.target.style.borderColor = C.greenDark; }}>
{label}
</button>
);
return (
<div style={{ display: “flex”, gap: “8px”, padding: “6px 12px”, background: C.bgPanel, borderBottom: `1px solid ${C.border}` }}>
<span style={{ color: C.greenDark, fontSize: “11px”, fontFamily: “monospace” }}>QUICK:</span>
{btn(“3.4 DATASETS”, onDatasets)}
{btn(“SDSF”, onSDSF)}
{btn(“SUBMIT JCL”, onJCL)}
</div>
);
}
export default function App() {
const [screen, setScreen] = useState(””);
const [connected, setConnected] = useState(false);
const [message, setMessage] = useState(“Connecting…”);
const [showJCL, setShowJCL] = useState(false);
const [showDS, setShowDS] = useState(false);
const wsRef = useRef(null);
useEffect(() => {
const check = async () => {
try {
const r = await api(”/health”);
setConnected(r.connected);
setMessage(r.connected ? “” : “Backend disconnected from z/OS”);
} catch { setConnected(false); setMessage(“Cannot reach backend”); }
};
check();
const t = setInterval(check, 10000);
return () => clearInterval(t);
}, []);
useEffect(() => {
const connect = () => {
const ws = new WebSocket(WS);
ws.onmessage = e => { const d = JSON.parse(e.data); if (d.screen) setScreen(d.screen); };
ws.onopen = () => setMessage(””);
ws.onclose = () => setTimeout(connect, 3000);
wsRef.current = ws;
};
connect();
return () => wsRef.current?.close();
}, []);
const sendCommand = useCallback(async cmd => {
const r = await api(”/command”, { text: cmd });
if (r.screen) setScreen(r.screen);
}, []);
const sendPF = useCallback(async key => {
const r = await api(`/pf/${key}`);
if (r.screen) setScreen(r.screen);
}, []);
const openSDSF = async () => {
const r = await api(”/sdsf”);
if (r.screen) setScreen(r.screen);
};
return (
<div style={{ minHeight: “100vh”, background: C.bg, color: C.green, fontFamily: “monospace”, display: “flex”, flexDirection: “column” }}>
<div style={{ padding: “10px 16px”, background: C.bgPanel, borderBottom: `2px solid ${C.greenDark}`, display: “flex”, alignItems: “center”, gap: “16px” }}>
<span style={{ fontSize: “18px”, fontWeight: “bold”, color: C.green, textShadow: `0 0 12px ${C.green}`, letterSpacing: “2px” }}>▋ z/OS WEB CONSOLE</span>
<span style={{ color: C.greenDark, fontSize: “11px” }}>TSO/ISPF • Hercules z/OS • 192.168.2.243</span>
</div>
<StatusBar connected={connected} message={message} />
<Toolbar onJCL={() => setShowJCL(true)} onDatasets={() => setShowDS(true)} onSDSF={openSDSF} />
<div style={{ flex: 1, padding: “12px”, overflow: “auto” }}>
<TerminalScreen screen={screen} />
</div>
<CommandInput onSend={sendCommand} disabled={!connected} />
<PFBar onPF={sendPF} />
{showJCL && <JCLPanel onClose={() => setShowJCL(false)} />}
{showDS && <DatasetPanel onClose={() => setShowDS(false)} />}
<div style={{ position: “fixed”, inset: 0, background: “repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,255,65,0.015) 2px, rgba(0,255,65,0.015) 4px)”, pointerEvents: “none”, zIndex: 999 }} />
</div>
);
}
JSEOF
cat > frontend/package.json << EOF
{
“name”: “zos-frontend”,
“version”: “1.0.0”,
“private”: true,
“dependencies”: {
“react”: “^18.3.1”,
“react-dom”: “^18.3.1”,
“react-scripts”: “5.0.1”
},
“scripts”: {
“start”: “react-scripts start”,
“build”: “react-scripts build”
},
“browserslist”: {
“production”: [”>0.2%”, “not dead”],
“development”: [“last 1 chrome version”]
}
}
EOF
cat > frontend/nginx.conf << EOF
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / { try_files $uri $uri/ /index.html; }
}
EOF
cat > frontend/Dockerfile << EOF
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD [“nginx”, “-g”, “daemon off;]
EOF
echo “✅ frontend files created”
# ============================================================
# KUBERNETES MANIFESTS
# ============================================================
cat > k8s/00-namespace.yaml << EOF
apiVersion: v1
kind: Namespace
metadata:
name: zos-web
EOF
cat > k8s/01-secret.yaml << EOF
apiVersion: v1
kind: Secret
metadata:
name: tso-credentials
namespace: zos-web
type: Opaque
stringData:
ZOS_HOST: “192.168.2.243”
ZOS_PORT: “23”
ZOS_USERID: “IBMUSER”
ZOS_PASSWORD: “allard”
ZOS_PROC: “DBSPROC9”
ZOS_ACCT: “ACCT#”
ZOS_SIZE: “2048000”
EOF
cat > k8s/02-backend-deploy.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: zos-backend
namespace: zos-web
spec:
replicas: 1
selector:
matchLabels:
app: zos-backend
template:
metadata:
labels:
app: zos-backend
spec:
containers:
- name: zos-backend
image: allardkrings/zos-backend:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
envFrom:
- secretRef:
name: tso-credentials
resources:
requests:
memory: “256Mi”
cpu: “250m”
limits:
memory: “512Mi”
cpu: “500m”
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 15
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
EOF
cat > k8s/03-backend-svc.yaml << EOF
apiVersion: v1
kind: Service
metadata:
name: zos-backend
namespace: zos-web
spec:
selector:
app: zos-backend
ports:
- protocol: TCP
port: 8000
targetPort: 8000
EOF
cat > k8s/04-frontend-deploy.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: zos-frontend
namespace: zos-web
spec:
replicas: 2
selector:
matchLabels:
app: zos-frontend
template:
metadata:
labels:
app: zos-frontend
spec:
containers:
- name: zos-frontend
image: allardkrings/zos-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 80
resources:
requests:
memory: “64Mi”
cpu: “50m”
limits:
memory: “128Mi”
cpu: “100m”
EOF
cat > k8s/05-frontend-svc.yaml << EOF
apiVersion: v1
kind: Service
metadata:
name: zos-frontend
namespace: zos-web
spec:
selector:
app: zos-frontend
ports:
- protocol: TCP
port: 80
targetPort: 80
EOF
cat > k8s/06-ingress.yaml << EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: zos-ingress
namespace: zos-web
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-read-timeout: “3600”
nginx.ingress.kubernetes.io/proxy-send-timeout: “3600”
nginx.ingress.kubernetes.io/proxy-http-version: “1.1”
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
spec:
ingressClassName: public
rules:
- http:
paths:
- path: /api(/|$)(.*)
pathType: Prefix
backend:
service:
name: zos-backend
port:
number: 8000
- path: /()(.*)
pathType: Prefix
backend:
service:
name: zos-frontend
port:
number: 80
EOF
echo “✅ kubernetes manifests created”
# ============================================================
# DONE
# ============================================================
echo “”
echo============================================
echo “ ✅ Project created in: $(pwd)
echo============================================
echo “”
echo “Next steps:”
echo “”
echo “ 1. Build & push backend:”
echocd backend”
echo “ docker build -t allardkrings/zos-backend:latest .”
echo “ docker push allardkrings/zos-backend:latest”
echo “”
echo “ 2. Build & push frontend:”
echocd ../frontend”
echo “ docker build -t allardkrings/zos-frontend:latest .”
echo “ docker push allardkrings/zos-frontend:latest”
echo “”
echo “ 3. Deploy to MicroK8s:”
echocd ../k8s”
echo “ microk8s kubectl apply -f .”
echo “”
echo “ 4. Open browser:”
echo “ http://192.168.2.100”
echo “”