Add Linux App Store Web Portal with Kubernetes deployment

- React-style static web portal with Tailwind CSS
- OS detection (Debian/Ubuntu, Fedora, Arch, Generic)
- Dynamic install commands (apt://, appstream://, flatpak)
- 9 pre-configured applications
- Kubernetes: 2 replicas, LoadBalancer service, Nginx ingress
- GitLab Container Registry (git.giaco.net/capitano/webapplinux)
- Namespace: linuxwebapp
- Added Dockerfile, nginx.conf, deploy.sh, docker-compose.yml
- Updated README.md with deployment instructions
This commit is contained in:
capitano
2026-03-18 22:20:20 +01:00
parent d13597e6b2
commit 533cbed8d3
13 changed files with 560 additions and 19 deletions

9
Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM nginx:alpine
COPY frontend/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

9
Dockerfile.simple Normal file
View File

@@ -0,0 +1,9 @@
FROM nginx:alpine
COPY frontend/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -4,58 +4,74 @@
### Prerequisiti
- Cluster Kubernetes attivo
- Cluster Kubernetes attivo (es. `10.66.200.x/24`)
- kubectl configurato
- Container registry accessibile (Docker Hub o privato)
- Container registry privato (GitLab: `git.giaco.net`)
- Personal Access Token per il registry
### Build e Deploy
```bash
# Build l'immagine
docker build -t linux-app-store:latest .
# Login al registry GitLab
echo "<GITLAB_TOKEN>" | docker login git.giaco.net -u capitano --password-stdin
# Tagga per il push
docker tag linux-app-store:latest <username>/linux-app-store:latest
# Build l'immagine
docker build -t git.giaco.net/capitano/webapplinux:latest .
# Push su registry
docker push <username>/linux-app-store:latest
docker push git.giaco.net/capitano/webapplinux:latest
# Deploy su Kubernetes
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/ingress.yaml
# Verifica
kubectl get pods
kubectl get svc
kubectl get ingress
kubectl get pods -n linuxwebapp
kubectl get svc -n linuxwebapp
kubectl get ingress -n linuxwebapp
# Log
kubectl logs -l app=linux-app-store -f
kubectl logs -l app=linux-app-store -n linuxwebapp -f
```
### Accesso al portale
Il portale è accessibile tramite ingress nginx all'indirizzo:
- **URL**: `http://apps.local/`
Per raggiungerlo localmente, aggiungi una entry nel file `/etc/hosts`:
```
10.66.200.211 apps.local
```
(sostituisci `10.66.200.211` con l'IP del tuo Ingress controller)
### File struttura
```
infrastructure/
├── Dockerfile # Dockerfile multi-stage (Nginx)
├── Dockerfile.simple # Dockerfile semplificato
webapplinux/
├── Dockerfile # Dockerfile Nginx
├── docker-compose.yml # Compose per sviluppo locale
├── nginx.conf # Configurazione Nginx
├── README.md # Questo file
├── frontend/
│ ├── index.html # Pagina principale
│ ├── script.js # Logica dell'app
│ ├── script.js # Logica OS detection & install
│ └── apps.json # Database applicazioni
└── k8s/
├── deployment.yaml # Kubernetes Deployment (2.repliche)
├── namespace.yaml # Namespace linuxwebapp
├── deployment.yaml # Deployment (2 repliche, git registry)
├── service.yaml # Service (ClusterIP)
└── ingress.yaml # Ingress (dominio apps.local)
```
### Customizzazione
1. Modificare `frontend/apps.json` per aggiungere rimuovere applicazioni
2. Aggiornare `nginx.conf` per personalizzare le regole di routing
3. Modificare `k8s/deployment.yaml` per aggiornare le risorse o le immagini
1. **Applicazioni**: Modifica `frontend/apps.json` per aggiungere/rimuovere app
2. **Nginx**: Aggiorna `nginx.conf` per routing personalizzato
3. **Risorse**: Modifica `k8s/deployment.yaml` per cpu/memory
4. **Immagine**: Cambia `image` nel deployment per usare un altro registry
### Rimozione
@@ -63,4 +79,12 @@ infrastructure/
kubectl delete -f k8s/ingress.yaml
kubectl delete -f k8s/service.yaml
kubectl delete -f k8s/deployment.yaml
kubectl delete -f k8s/namespace.yaml
```
### CI/CD
Usa il file `.gitlab-ci.yml` (da creare) per automatizzare:
1. Build dell'immagine
2. Push su GitLab registry
3. Deploy automatico in Kubernetes

18
deploy.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
REGISTRY="git.giaco.net"
IMAGE_NAME="capitano/webapplinux"
TAG="latest"
echo "Building image..."
docker build -t $REGISTRY/$IMAGE_NAME:$TAG /home/capitano/kubernetes/infrastructure/webapplinux/
echo "Pushing to registry..."
docker push $REGISTRY/$IMAGE_NAME:$TAG
echo "Deploying to Kubernetes..."
kubectl apply -f k8s/namespace.yaml 2>/dev/null || true
kubectl apply -f k8s/
echo "Check status with: kubectl get pods -n linuxwebapp"

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
web:
build: .
ports:
- "8080:80"
volumes:
- ./frontend:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
environment:
- NGINX_HOST=apps.local
- NGINX_PORT=80

112
frontend/apps.json Normal file
View File

@@ -0,0 +1,112 @@
{
"apps": [
{
"id": "vlc",
"name": "VLC Media Player",
"description": "The most complete media player ever created. Play all your favorite media formats.",
"icon": "fa-film",
"colors": "from-purple-500 to-indigo-600",
"packages": {
"apt": "vlc",
"appstream": "org.videolan.VLC",
"flatpak": "https://flathub.org-repo/vlc.flatpakref"
}
},
{
"id": "gimp",
"name": "GIMP",
"description": "GNU Image Manipulation Program - professional-grade photo editing tool.",
"icon": "fa-image",
"colors": "from-blue-500 to-cyan-600",
"packages": {
"apt": "gimp",
"appstream": "org.gimp.GIMP",
"flatpak": "https://flathub.org-repo/gimp.flatpakref"
}
},
{
"id": "vscode",
"name": "VS Code",
"description": "Visual Studio Code - the code editor redefined for modern development.",
"icon": "fa-code",
"colors": "from-blue-600 to-blue-800",
"packages": {
"apt": "code",
"appstream": "com.visualstudio.code",
"flatpak": "https://flathub.org-repo/code.flatpakref"
}
},
{
"id": "thunderbird",
"name": "Thunderbird",
"description": "Email client that puts control back in your hands with powerful features.",
"icon": "fa-envelope",
"colors": "from-orange-500 to-red-600",
"packages": {
"apt": "thunderbird",
"appstream": "org.mozilla.thunderbird",
"flatpak": "https://flathub.org-repo/thunderbird.flatpakref"
}
},
{
"id": "libreoffice",
"name": "LibreOffice",
"description": "The-free-and-open-source productivity suite for documents, spreadsheets, and presentations.",
"icon": "fa-file-alt",
"colors": "from-teal-500 to-green-600",
"packages": {
"apt": "libreoffice",
"appstream": "org.libreoffice.LibreOffice",
"flatpak": "https://flathub.org-repo/libreoffice.flatpakref"
}
},
{
"id": "firefox",
"name": "Firefox",
"description": "The web browser built for privacy, speed, and customization.",
"icon": "fa-globe",
"colors": "from-orange-600 to-red-700",
"packages": {
"apt": "firefox",
"appstream": "org.mozilla.firefox",
"flatpak": "https://flathub.org-repo/firefox.flatpakref"
}
},
{
"id": "inkscape",
"name": "Inkscape",
"description": "Professional vector graphics editor for illustrations, diagrams, and logos.",
"icon": "fa-pen-nib",
"colors": "from-blue-400 to-indigo-500",
"packages": {
"apt": "inkscape",
"appstream": "org.inkscape.Inkscape",
"flatpak": "https://flathub.org-repo/inkscape.flatpakref"
}
},
{
"id": "discord",
"name": "Discord",
"description": "All-in-one voice and text chat for gamers and communities.",
"icon": "fa-comments",
"colors": "from-indigo-500 to-purple-600",
"packages": {
"apt": "discord",
"appstream": "com.discordapp.Discord",
"flatpak": "https://flathub.org-repo/discord.flatpakref"
}
},
{
"id": "github-desktop",
"name": "GitHub Desktop",
"description": "Simple collaboration from your desktop - git made easy.",
"icon": "fa-github",
"colors": "from-gray-600 to-gray-800",
"packages": {
"apt": "github-desktop",
"appstream": "com.githubDesktop.githubDesktop",
"flatpak": "https://flathub.org-repo/github-desktop.flatpakref"
}
}
]
}

49
frontend/index.html Normal file
View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linux App Store</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
gray: {
850: '#1f2937',
900: '#111827',
}
}
}
}
}
</script>
</head>
<body class="bg-gray-900 text-white">
<nav class="bg-gray-850 border-b border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<i class="fas fa-store text-blue-500 text-2xl mr-3"></i>
<span class="text-xl font-bold">Linux App Store</span>
</div>
<div id="os-display" class="hidden text-sm text-gray-400"></div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold mb-2">Discovery</h1>
<p class="text-gray-400">Discover and install applications directly on your Linux system</p>
</div>
<div id="apps-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
</div>
</main>
<script src="script.js"></script>
</body>
</html>

192
frontend/script.js Normal file
View File

@@ -0,0 +1,192 @@
const OS_TYPE = {
DEB: 'deb',
RPM: 'rpm',
ARCH: 'arch',
GENERIC: 'generic'
};
function detectOS() {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('ubuntu') ||
userAgent.includes('debian') ||
userAgent.includes('linuxmint') ||
userAgent.includes('pop!_os')) {
return { type: OS_TYPE.DEB, name: 'Debian/Ubuntu-based' };
}
if (userAgent.includes('fedora') ||
userAgent.includes('rhel') ||
userAgent.includes('redhat') ||
userAgent.includes('centos') ||
userAgent.includes('opensuse') ||
userAgent.includes('suse')) {
return { type: OS_TYPE.RPM, name: 'Fedora/RedHat-based' };
}
if (userAgent.includes('arch') ||
userAgent.includes('endeavouros') ||
userAgent.includes('manjaro')) {
return { type: OS_TYPE.ARCH, name: 'Arch Linux' };
}
return { type: OS_TYPE.GENERIC, name: 'Generic Linux' };
}
function renderApps(apps) {
const container = document.getElementById('apps-container');
apps.forEach(app => {
const appCard = document.createElement('div');
appCard.className = 'bg-gray-850 rounded-xl overflow-hidden shadow-lg hover:shadow-2xl transition-shadow duration-300 border border-gray-700';
appCard.innerHTML = `
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center">
<div class="w-12 h-12 rounded-lg bg-gradient-to-br ${app.colors} flex items-center justify-center mr-4">
<i class="${app.icon} fa-xl text-white"></i>
</div>
<h3 class="text-xl font-bold">${app.name}</h3>
</div>
</div>
<p class="text-gray-400 text-sm mb-6 line-clamp-2">${app.description}</p>
<div class="bg-gray-900 rounded-lg p-3 mb-4 text-xs text-gray-500 font-mono">
<div class="mb-1"><i class="fas fa-terminal mr-2"></i>Installazione:</div>
<div id="install-${app.id}">Caricamento...</div>
</div>
<button onclick="installApp('${app.id}', '${app.packages.apt}', '${app.packages.appstream}', '${app.packages.flatpak}')"
class="w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-bold py-3 px-4 rounded-lg transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
<i class="fas fa-download mr-2"></i>Installa
</button>
</div>
`;
container.appendChild(appCard);
});
}
function updateInstallLink(appId, aptPackage, appstreamId, flatpakUrl) {
const osInfo = detectOS();
const installDiv = document.getElementById(`install-${appId}`);
let installCommand = '';
let protocol = '';
switch (osInfo.type) {
case OS_TYPE.DEB:
protocol = 'apt://';
installCommand = `${protocol}${aptPackage}`;
break;
case OS_TYPE.RPM:
protocol = 'appstream://';
installCommand = `${protocol}${appstreamId}`;
break;
case OS_TYPE.ARCH:
protocol = 'appstream://';
installCommand = `${protocol}${appstreamId} (pacman -S ${aptPackage})`;
break;
case OS_TYPE.GENERIC:
protocol = 'flatpak';
installCommand = `Download: ${flatpakUrl}`;
break;
}
installDiv.innerHTML = `<i class="fas fa-terminal mr-2"></i> ${installCommand}`;
const installBtn = document.querySelector(`button[onclick="installApp('${appId}', '${aptPackage}', '${appstreamId}', '${flatpakUrl}')"]`);
installBtn.onclick = function() {
handleInstall(appId, aptPackage, appstreamId, flatpakUrl);
};
}
function handleInstall(appId, aptPackage, appstreamId, flatpakUrl) {
const osInfo = detectOS();
let url = '';
switch (osInfo.type) {
case OS_TYPE.DEB:
url = `apt://${aptPackage}`;
break;
case OS_TYPE.RPM:
url = `appstream://${appstreamId}`;
break;
case OS_TYPE.ARCH:
url = `appstream://${appstreamId}`;
break;
case OS_TYPE.GENERIC:
url = flatpakUrl;
break;
}
if (url.startsWith('apt://') || url.startsWith('appstream://')) {
const protocol = url.split(':')[0];
let command = url.replace(`${protocol}://`, '');
if (protocol === 'apt') {
command = `<span class="text-green-400">sudo apt install ${command}</span>`;
} else {
command = `<span class="text-green-400">${protocol} install ${command}</span>`;
}
const notification = createNotification(`Comando generato per ${osInfo.name}:<br>${command}`, 'success');
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 5000);
} else if (url) {
window.open(url, '_blank');
}
}
function createNotification(message, type) {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 bg-gray-800 border-l-4 border-${type === 'success' ? 'green' : 'red'}-500 text-white px-6 py-4 rounded shadow-lg z-50 transition-all duration-500 transform translate-x-full`;
notification.innerHTML = `
<div class="flex items-center">
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'} text-${type === 'success' ? 'green' : 'red'}-500 mr-3"></i>
<div>${message}</div>
</div>
`;
setTimeout(() => {
notification.classList.remove('translate-x-full');
}, 100);
return notification;
}
function highlightOS(osInfo) {
const osDisplay = document.getElementById('os-display');
if (osDisplay) {
osDisplay.innerHTML = `<i class="fab fa-${osInfo.name.includes('Ubuntu') ? 'ubuntu' : osInfo.name.includes('Fedora') ? 'fedora' : osInfo.name.includes('Arch') ? 'linux' : 'linux'} mr-2"></i>${osInfo.name}`;
osDisplay.classList.remove('hidden');
}
}
function init() {
fetch('apps.json')
.then(response => response.json())
.then(data => {
const osInfo = detectOS();
highlightOS(osInfo);
renderApps(data.apps);
data.apps.forEach(app => {
updateInstallLink(app.id, app.packages.apt, app.packages.appstream, app.packages.flatpak);
});
})
.catch(error => {
console.error('Error loading apps:', error);
const container = document.getElementById('apps-container');
container.innerHTML = `<div class="col-span-full text-center py-10 text-red-500">
<i class="fas fa-exclamation-triangle text-3xl mb-2"></i>
<p>Errore nel caricamento delle applicazioni</p>
</div>`;
});
}
document.addEventListener('DOMContentLoaded', init);

47
k8s/deployment.yaml Normal file
View File

@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: linux-app-store
namespace: linuxwebapp
labels:
app: linux-app-store
spec:
replicas: 2
selector:
matchLabels:
app: linux-app-store
template:
metadata:
labels:
app: linux-app-store
spec:
containers:
- name: linux-app-store
image: git.giaco.net/capitano/webapplinux:latest
imagePullPolicy: Always
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: NGINX_HOST
value: "apps.local"
- name: NGINX_PORT
value: "80"

22
k8s/ingress.yaml Normal file
View File

@@ -0,0 +1,22 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: linux-app-store-ingress
namespace: linuxwebapp
labels:
app: linux-app-store
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: apps.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: linux-app-store-service
port:
number: 80

6
k8s/namespace.yaml Normal file
View File

@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: linuxwebapp
labels:
name: linuxwebapp

16
k8s/service.yaml Normal file
View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: linux-app-store-service
namespace: linuxwebapp
labels:
app: linux-app-store
spec:
type: LoadBalancer
selector:
app: linux-app-store
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http

26
nginx.conf Normal file
View File

@@ -0,0 +1,26 @@
server {
listen 80;
server_name apps.local;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /apps.json {
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml application/json;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}