Welcome to LAB04A! In this lab, you'll add a lightweight developer portal to provide a user-friendly interface for your platform. By the end of this lab, you'll have:
- A simple, reliable web-based form interface for requesting resources
- Pre-built YAML templates for namespaces and Azure resources
- Command-line tools for easy resource requests
- Integration with your platform-self-service repository from LAB02
- A complete self-service workflow where ArgoCD syncs requested resources
Note: This lab provides practical, workshop-friendly alternatives to complex developer portals like Backstage, focusing on simplicity and reliability for local development environments.
Before starting, ensure you have completed:
- ✅ LAB01: Kind cluster with NGINX ingress and ArgoCD installed
- ✅ LAB02: Platform-self-service repository and ArgoCD ApplicationSets
- ✅ LAB03: Azure Service Operator installed (for Azure resource templates)
Additional Requirements:
- ✅ kubectl: Command-line tool for Kubernetes
- ✅ Git: For committing and pushing resource requests
- ✅ GitHub Account: Your platform-self-service repository from LAB02
- ✅ curl or web browser: For accessing the simple web UI
In previous labs, you created a GitOps-based self-service platform where teams request resources through Git Pull Requests. While powerful, this approach requires developers to:
- Understand Git workflows
- Write YAML manifests correctly
- Wait for PR reviews and merges
In this lab, we'll add simple, reliable self-service tools that:
- Provide easy-to-use interfaces instead of complex YAML editing
- Generate correct YAML from templates automatically
- Integrate with your existing GitOps workflow
- Work reliably in workshop and local development environments
While Backstage is a powerful platform for production environments, it has challenges in workshop settings:
- Complex installation and configuration
- Heavy resource requirements (Node.js, databases, etc.)
- Requires extensive setup time
- Can be unreliable in local Kind clusters
- Steep learning curve for workshop timeframes
This lab explores three practical, workshop-friendly alternatives that capture the essence of self-service:
- Simple Web UI: Lightweight HTML forms hosted in your cluster
- Command-Line Helper: Bash scripts that generate YAML from prompts
- YAML Templates: Pre-built templates with simple parameter substitution
All three approaches integrate seamlessly with your GitOps workflow from LAB02.
graph LR
A["Developer"] --> B["Simple Web UI / CLI</br>(Easy interface)"]
B --> C["YAML Templates</br>(Generated)"]
C --> D["Git Commit</br>(platform-self-service repo)"]
D --> E["ArgoCD</br>(Sync)"]
E --> F["Kubernetes / Azure"]
- Set up YAML templates for common resource requests
- Deploy a simple web UI for form-based requests (Option A)
- Create command-line helper scripts (Option B)
- Test the complete workflow: Request → Generate → Commit → Sync → Deployed
- Compare approaches and choose what works best for your team
Instead of complex form builders, we'll create reusable YAML templates that developers can easily customize. This is lightweight, version-controlled, and requires no special infrastructure.
First, ensure you have your platform-self-service repository from LAB02:
# Navigate to your platform-self-service repository
cd ~/platform-self-service
# If you don't have it, clone it
# git clone https://github.com/YOUR_USERNAME/platform-self-service.git
# cd platform-self-service
# Verify you're in the right place
ls -la
# Should show your namespaces/ and azure-resources/ directories from LAB02# Create a templates directory
mkdir -p templates/namespaces
mkdir -p templates/azure-storage
mkdir -p templates/helpers
# Verify the structure matches the expected layout for azure-resources
# Azure resources should be organized by team: azure-resources/{teamname}/*.yaml
mkdir -p azure-resources/example-team
Create a template that developers can easily customize:
cat << 'EOF' > templates/namespaces/namespace-template.yaml
# Kubernetes Namespace Request Template
#
# Instructions:
# 1. Copy this file to: namespaces/{environment}/{your-team}-namespace.yaml
# 2. Replace all {{PLACEHOLDERS}} with your values
# 3. Commit and push to create a PR
# 4. After PR approval and merge, ArgoCD will create your namespace
#
# Example values:
# TEAM_NAME: frontend, backend, data-team
# ENVIRONMENT: dev, staging, prod
# CONTACT_EMAIL: team@company.com
# PURPOSE: Application deployment, testing, etc.
# CPU_CORES: 1, 2, 4, 8
# MEMORY_GB: 2, 4, 8, 16
---
apiVersion: v1
kind: Namespace
metadata:
name: devops-{{TEAM_NAME}}-{{ENVIRONMENT}}
labels:
team: {{TEAM_NAME}}
environment: {{ENVIRONMENT}}
managed-by: platform-team
created-via: self-service
annotations:
team.contact: "{{CONTACT_EMAIL}}"
purpose: "{{PURPOSE}}"
requested-date: "{{DATE}}"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: {{TEAM_NAME}}-{{ENVIRONMENT}}-quota
namespace: devops-{{TEAM_NAME}}-{{ENVIRONMENT}}
spec:
hard:
requests.cpu: "{{CPU_CORES}}"
requests.memory: "{{MEMORY_GB}}Gi"
limits.cpu: "{{CPU_CORES * 2}}"
limits.memory: "{{MEMORY_GB * 2}}Gi"
persistentvolumeclaims: "5"
services: "10"
pods: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: {{TEAM_NAME}}-{{ENVIRONMENT}}-limits
namespace: devops-{{TEAM_NAME}}-{{ENVIRONMENT}}
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: Container
EOF
echo "✅ Created namespace template"cat << 'EOF' > templates/azure-storage/storage-template.yaml
# Azure Storage Account Request Template
#
# Instructions:
# 1. Copy this file to: azure-resources/{your-team-name}/{your-storage-name}.yaml
# 2. Replace all {{PLACEHOLDERS}} with your values
# 3. Commit and push to create a PR
# 4. After PR approval and merge, ArgoCD and ASO will create your storage account
#
# Example values:
# TEAM_NAME: frontend, backend, data-team
# ENVIRONMENT: dev, staging, prod
# STORAGE_NAME: myteamstorage001 (must be globally unique, 3-24 chars, lowercase alphanumeric only)
# PURPOSE: Application data, backups, etc.
# AZURE_REGION: swedencentral, westeurope, northeurope
# SKU: Standard_LRS (local), Standard_GRS (geo-redundant)
---
# Resource Group for Storage
apiVersion: resources.azure.com/v1api20200601
kind: ResourceGroup
metadata:
name: {{TEAM_NAME}}-{{ENVIRONMENT}}-storage-rg
namespace: default
spec:
location: {{AZURE_REGION}}
tags:
team: {{TEAM_NAME}}
environment: {{ENVIRONMENT}}
resource-type: storage
managed-by: platform-team
created-via: self-service
requested-date: "{{DATE}}"
---
# Storage Account
apiVersion: storage.azure.com/v1api20230101
kind: StorageAccount
metadata:
name: {{STORAGE_NAME}}
namespace: default
spec:
location: {{AZURE_REGION}}
kind: StorageV2
sku:
name: {{SKU}}
owner:
name: {{TEAM_NAME}}-{{ENVIRONMENT}}-storage-rg
accessTier: Hot
tags:
team: {{TEAM_NAME}}
environment: {{ENVIRONMENT}}
purpose: "{{PURPOSE}}"
managed-by: platform-team
created-via: self-service
EOF
echo "✅ Created Azure storage template"cat << 'EOF' > templates/README.md
# Self-Service Platform Templates
This directory contains templates for requesting platform resources. Developers use these templates to quickly generate properly formatted resource requests.
## Available Templates
### Namespace Request (`namespaces/namespace-template.yaml`)
Request a Kubernetes namespace with resource quotas and limits.
**Required values:**
- `TEAM_NAME`: Your team name (lowercase, no spaces)
- `ENVIRONMENT`: dev, staging, or prod
- `CONTACT_EMAIL`: Team contact email
- `PURPOSE`: What the namespace will be used for
- `CPU_CORES`: Number of CPU cores (1, 2, 4, 8)
- `MEMORY_GB`: Memory in GB (2, 4, 8, 16)
- `DATE`: Request date (YYYY-MM-DD)
### Azure Storage Request (`azure-storage/storage-template.yaml`)
Request an Azure Storage Account for your team.
**Required values:**
- `TEAM_NAME`: Your team name
- `ENVIRONMENT`: dev, staging, or prod
- `STORAGE_NAME`: Globally unique storage name (3-24 chars, lowercase alphanumeric)
- `PURPOSE`: What the storage will be used for
- `AZURE_REGION`: swedencentral, westeurope, or northeurope
- `SKU`: Standard_LRS or Standard_GRS
- `DATE`: Request date (YYYY-MM-DD)
**File Location**: `azure-resources/{TEAM_NAME}/{STORAGE_NAME}.yaml`
## How to Use Templates
### Manual Method
1. Copy the appropriate template file
2. Replace all `{{PLACEHOLDERS}}` with your actual values
3. Save to the correct directory
4. Commit and push to create a PR
5. Wait for PR approval and merge
6. ArgoCD will automatically sync the resources
### Using Helper Scripts (see Part 2)
Use the provided scripts to interactively generate YAML from templates.
## Directory Structure
```mermaid
graph TD
A[platform-self-service] --> B[namespaces]
B --> B1[dev]
B --> B2[staging]
B --> B3[prod]
A --> C[azure-resources]
C --> C1[team-alpha]
C --> C2[team-beta]
C --> C3[team-gamma]
A --> D[templates]
D --> D1[namespaces]
D --> D2[azure-storage]
D --> D3[helpers]Note: Azure resources are organized by team name under azure-resources/, where each team has their own directory containing their YAML resource definitions.
For questions or issues, contact the platform team or consult LAB04A documentation. EOF
### ✅ Verification Steps - Part 1
```bash
# Verify template structure
ls -la templates/
ls -la templates/namespaces/
ls -la templates/azure-storage/
# View the templates
cat templates/namespaces/namespace-template.yaml
cat templates/azure-storage/storage-template.yaml
# Commit templates to repository
git add templates/
git commit -m "Add self-service YAML templates for namespaces and Azure storage"
git push origin main
Expected Output:
- Templates directory created with proper structure
- Namespace and storage templates contain placeholder syntax
- Templates committed to Git repository
- README explains how to use templates
The simplest self-service approach is an interactive command-line script that generates YAML from templates. This requires no infrastructure and works everywhere.
cat << 'EOF' > templates/helpers/request-namespace.sh
#!/bin/bash
# Interactive script to request a Kubernetes namespace
# This script generates YAML from templates and can optionally commit it
set -e
echo "==================================="
echo " Namespace Request Tool"
echo "==================================="
echo
# Prompt for values
read -p "Team Name (lowercase, no spaces): " TEAM_NAME
read -p "Environment (dev/staging/prod): " ENVIRONMENT
read -p "Contact Email: " CONTACT_EMAIL
read -p "Purpose: " PURPOSE
read -p "CPU Cores (1/2/4/8): " CPU_CORES
read -p "Memory GB (2/4/8/16): " MEMORY_GB
# Calculate limits (double the requests)
CPU_LIMIT=$((CPU_CORES * 2))
MEMORY_LIMIT=$((MEMORY_GB * 2))
# Get current date
DATE=$(date +%Y-%m-%d)
# Generate YAML
OUTPUT_DIR="../namespaces/${ENVIRONMENT}"
OUTPUT_FILE="${OUTPUT_DIR}/${TEAM_NAME}-namespace.yaml"
# Create directory if it doesn't exist
mkdir -p "$OUTPUT_DIR"
echo
echo "Generating YAML file: $OUTPUT_FILE"
echo
# Generate the YAML file
cat > "$OUTPUT_FILE" << YAML
# Kubernetes Namespace Request
# Team: ${TEAM_NAME}
# Environment: ${ENVIRONMENT}
# Requested: ${DATE}
---
apiVersion: v1
kind: Namespace
metadata:
name: devops-${TEAM_NAME}-${ENVIRONMENT}
labels:
team: ${TEAM_NAME}
environment: ${ENVIRONMENT}
managed-by: platform-team
created-via: self-service
annotations:
team.contact: "${CONTACT_EMAIL}"
purpose: "${PURPOSE}"
requested-date: "${DATE}"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: ${TEAM_NAME}-${ENVIRONMENT}-quota
namespace: devops-${TEAM_NAME}-${ENVIRONMENT}
spec:
hard:
requests.cpu: "${CPU_CORES}"
requests.memory: "${MEMORY_GB}Gi"
limits.cpu: "${CPU_LIMIT}"
limits.memory: "${MEMORY_LIMIT}Gi"
persistentvolumeclaims: "5"
services: "10"
pods: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: ${TEAM_NAME}-${ENVIRONMENT}-limits
namespace: devops-${TEAM_NAME}-${ENVIRONMENT}
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: Container
YAML
echo "✅ YAML file generated successfully!"
echo
echo "File location: $OUTPUT_FILE"
echo
echo "Namespace that will be created: devops-${TEAM_NAME}-${ENVIRONMENT}"
echo
# Show the file contents
cat "$OUTPUT_FILE"
echo
echo "=================================="
echo "Next Steps:"
echo "=================================="
echo "1. Review the generated YAML above"
echo "2. Commit the file to git:"
echo " cd $(dirname $OUTPUT_DIR)"
echo " git add $OUTPUT_FILE"
echo " git commit -m 'Request namespace for ${TEAM_NAME} ${ENVIRONMENT}'"
echo " git push origin main"
echo "3. Create a Pull Request on GitHub"
echo "4. After PR approval, ArgoCD will sync the namespace"
echo
read -p "Would you like to commit this file now? (y/n): " COMMIT_NOW
if [[ "$COMMIT_NOW" == "y" || "$COMMIT_NOW" == "Y" ]]; then
cd "$(dirname $OUTPUT_DIR)"
git add "$OUTPUT_FILE"
git commit -m "Request namespace for ${TEAM_NAME} in ${ENVIRONMENT} environment
Requested by: ${CONTACT_EMAIL}
Purpose: ${PURPOSE}
Resources: ${CPU_CORES} CPU cores, ${MEMORY_GB}GB memory"
echo
echo "✅ Changes committed locally!"
echo "Run 'git push origin main' to push to GitHub and create a PR"
fi
echo
echo "Done!"
EOF
# Make script executable
chmod +x templates/helpers/request-namespace.sh
echo "✅ Created namespace request script"cat << 'EOF' > templates/helpers/request-storage.sh
#!/bin/bash
# Interactive script to request Azure Storage Account
# This script generates YAML from templates and can optionally commit it
set -e
echo "==================================="
echo " Azure Storage Request Tool"
echo "==================================="
echo
# Prompt for values
read -p "Team Name: " TEAM_NAME
read -p "Environment (dev/staging/prod): " ENVIRONMENT
read -p "Storage Account Name (3-24 chars, lowercase alphanumeric only): " STORAGE_NAME
read -p "Purpose: " PURPOSE
read -p "Azure Region (swedencentral/westeurope/northeurope): " AZURE_REGION
read -p "SKU (Standard_LRS/Standard_GRS): " SKU
# Validate storage name
if [[ ! "$STORAGE_NAME" =~ ^[a-z0-9]{3,24}$ ]]; then
echo "❌ Error: Storage name must be 3-24 characters, lowercase alphanumeric only"
exit 1
fi
# Get current date
DATE=$(date +%Y-%m-%d)
# Generate YAML
OUTPUT_DIR="../azure-resources/${TEAM_NAME}"
OUTPUT_FILE="${OUTPUT_DIR}/${STORAGE_NAME}.yaml"
# Create directory if it doesn't exist
mkdir -p "$OUTPUT_DIR"
echo
echo "Generating YAML file: $OUTPUT_FILE"
echo
# Generate the YAML file
cat > "$OUTPUT_FILE" << YAML
# Azure Storage Account Request
# Team: ${TEAM_NAME}
# Environment: ${ENVIRONMENT}
# Requested: ${DATE}
---
# Resource Group for Storage
apiVersion: resources.azure.com/v1api20200601
kind: ResourceGroup
metadata:
name: ${TEAM_NAME}-${ENVIRONMENT}-storage-rg
namespace: default
spec:
location: ${AZURE_REGION}
tags:
team: ${TEAM_NAME}
environment: ${ENVIRONMENT}
resource-type: storage
managed-by: platform-team
created-via: self-service
requested-date: "${DATE}"
---
# Storage Account
apiVersion: storage.azure.com/v1api20230101
kind: StorageAccount
metadata:
name: ${STORAGE_NAME}
namespace: default
spec:
location: ${AZURE_REGION}
kind: StorageV2
sku:
name: ${SKU}
owner:
name: ${TEAM_NAME}-${ENVIRONMENT}-storage-rg
accessTier: Hot
tags:
team: ${TEAM_NAME}
environment: ${ENVIRONMENT}
purpose: "${PURPOSE}"
managed-by: platform-team
created-via: self-service
YAML
echo "✅ YAML file generated successfully!"
echo
echo "File location: $OUTPUT_FILE"
echo
echo "Storage account name: ${STORAGE_NAME}"
echo "Resource group: ${TEAM_NAME}-${ENVIRONMENT}-storage-rg"
echo
# Show the file contents
cat "$OUTPUT_FILE"
echo
echo "=================================="
echo "Next Steps:"
echo "=================================="
echo "1. Review the generated YAML above"
echo "2. Commit the file to git:"
echo " cd $(dirname $OUTPUT_DIR)"
echo " git add $OUTPUT_FILE"
echo " git commit -m 'Request storage account ${STORAGE_NAME}'"
echo " git push origin main"
echo "3. Create a Pull Request on GitHub"
echo "4. After PR approval, ArgoCD and ASO will create the storage account"
echo
read -p "Would you like to commit this file now? (y/n): " COMMIT_NOW
if [[ "$COMMIT_NOW" == "y" || "$COMMIT_NOW" == "Y" ]]; then
cd "$(dirname $OUTPUT_DIR)"
git add "$OUTPUT_FILE"
git commit -m "Request Azure storage account ${STORAGE_NAME}
Team: ${TEAM_NAME}
Environment: ${ENVIRONMENT}
Purpose: ${PURPOSE}
Region: ${AZURE_REGION}
SKU: ${SKU}"
echo
echo "✅ Changes committed locally!"
echo "Run 'git push origin main' to push to GitHub and create a PR"
fi
echo
echo "Done!"
EOF
# Make script executable
chmod +x templates/helpers/request-storage.sh
echo "✅ Created storage request script"Let's test the namespace request script:
# Navigate to the helpers directory
cd templates/helpers
# Run the script interactively
./request-namespace.sh
# Example input:
# Team Name: testteam
# Environment: dev
# Contact Email: testteam@company.com
# Purpose: Testing self-service workflow
# CPU Cores: 2
# Memory GB: 4
# Commit now: n (for now, we'll review first)# Verify scripts were created
ls -la templates/helpers/
file templates/helpers/request-namespace.sh
file templates/helpers/request-storage.sh
# Check scripts are executable
[[ -x templates/helpers/request-namespace.sh ]] && echo "✅ Namespace script is executable"
[[ -x templates/helpers/request-storage.sh ]] && echo "✅ Storage script is executable"
# Commit the helper scripts
cd ~/platform-self-service
git add templates/helpers/
git commit -m "Add interactive CLI helper scripts for self-service"
git push origin mainExpected Output:
- Two executable shell scripts created
- Scripts have proper permissions
- Scripts committed to repository
For teams that prefer a web interface, we can deploy a lightweight HTML form that generates YAML. For the purposes of this lab, we'll create a static HTML file that can be served via a simple web server or ingress.
cat << 'EOF' > templates/helpers/self-service-ui.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Platform Self-Service Portal</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
header {
background: #2d3748;
color: white;
padding: 30px;
text-align: center;
}
h1 {
font-size: 28px;
margin-bottom: 10px;
}
.subtitle {
color: #a0aec0;
font-size: 14px;
}
.tabs {
display: flex;
background: #edf2f7;
border-bottom: 2px solid #cbd5e0;
}
.tab {
flex: 1;
padding: 15px;
text-align: center;
cursor: pointer;
background: #edf2f7;
border: none;
font-size: 16px;
font-weight: 600;
color: #4a5568;
transition: all 0.3s;
}
.tab:hover {
background: #e2e8f0;
}
.tab.active {
background: white;
color: #667eea;
border-bottom: 3px solid #667eea;
}
.tab-content {
display: none;
padding: 30px;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #2d3748;
}
input, select, textarea {
width: 100%;
padding: 10px;
border: 2px solid #e2e8f0;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
textarea {
resize: vertical;
min-height: 80px;
}
.help-text {
font-size: 12px;
color: #718096;
margin-top: 5px;
}
button {
background: #667eea;
color: white;
padding: 12px 30px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #5a67d8;
}
.output {
margin-top: 30px;
padding: 20px;
background: #f7fafc;
border-radius: 5px;
border: 2px solid #e2e8f0;
}
.output h3 {
margin-bottom: 10px;
color: #2d3748;
}
pre {
background: #2d3748;
color: #68d391;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
font-size: 13px;
line-height: 1.5;
}
.copy-btn {
background: #48bb78;
margin-top: 10px;
}
.copy-btn:hover {
background: #38a169;
}
.instructions {
background: #ebf8ff;
border-left: 4px solid #4299e1;
padding: 15px;
margin-top: 15px;
border-radius: 0 5px 5px 0;
}
.instructions h4 {
color: #2c5282;
margin-bottom: 10px;
}
.instructions ol {
margin-left: 20px;
color: #2d3748;
}
.instructions li {
margin-bottom: 5px;
}
.github-config {
background: #f0fff4;
border: 2px solid #48bb78;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.config-row {
display: flex;
gap: 15px;
margin-bottom: 15px;
align-items: end;
}
.config-row .form-group {
flex: 1;
margin-bottom: 0;
}
.config-row button {
padding: 8px 15px;
font-size: 14px;
}
.status-message {
padding: 10px;
border-radius: 5px;
margin: 10px 0;
font-weight: 600;
}
.status-success {
background: #c6f6d5;
border: 1px solid #48bb78;
color: #22543d;
}
.status-error {
background: #fed7d7;
border: 1px solid #e53e3e;
color: #742a2a;
}
.status-info {
background: #bee3f8;
border: 1px solid #3182ce;
color: #2a4365;
}
.auto-commit-section {
background: #fefcbf;
border: 2px solid #d69e2e;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.auto-commit-section h4 {
color: #744210;
margin-bottom: 15px;
}
.commit-controls {
display: flex;
gap: 10px;
margin-top: 15px;
flex-wrap: wrap;
}
.commit-btn {
background: #d69e2e;
color: white;
}
.commit-btn:hover {
background: #b7791f;
}
.commit-btn:disabled {
background: #a0aec0;
cursor: not-allowed;
}
.pr-btn {
background: #805ad5;
color: white;
}
.pr-btn:hover {
background: #6b46c1;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🚀 Platform Self-Service Portal</h1>
<p class="subtitle">Request namespaces and Azure resources with ease</p>
</header>
<!-- GitHub Configuration Section -->
<div class="github-config">
<h3>🔧 GitHub Integration Setup</h3>
<p>Configure your GitHub settings to enable automatic commits and PR creation.</p>
<div class="config-row">
<div class="form-group">
<label>GitHub Username</label>
<input type="text" id="gh-username" placeholder="your-username">
</div>
<div class="form-group">
<label>Repository Name</label>
<input type="text" id="gh-repo" placeholder="platform-self-service">
</div>
<div class="form-group">
<label>Personal Access Token</label>
<input type="password" id="gh-token" placeholder="ghp_xxxxxxxxxxxx">
<div class="help-text">Token needs 'repo' scope. <a href="https://github.com/settings/tokens" target="_blank">Create one here</a></div>
</div>
<button onclick="testGitHubConnection()">Test Connection</button>
</div>
<div id="gh-status"></div>
</div>
<div class="tabs">
<button class="tab active" onclick="showTab('namespace')">Namespace Request</button>
<button class="tab" onclick="showTab('storage')">Azure Storage</button>
</div>
<!-- Namespace Tab -->
<div id="namespace" class="tab-content active">
<h2>Request Kubernetes Namespace</h2>
<form onsubmit="generateNamespaceYAML(event)">
<div class="form-group">
<label>Team Name *</label>
<input type="text" id="ns-team" required pattern="[a-z0-9-]+" placeholder="frontend">
<div class="help-text">Lowercase letters, numbers, and hyphens only</div>
</div>
<div class="form-group">
<label>Environment *</label>
<select id="ns-env" required>
<option value="dev">Development</option>
<option value="staging">Staging</option>
<option value="prod">Production</option>
</select>
</div>
<div class="form-group">
<label>Contact Email *</label>
<input type="email" id="ns-email" required placeholder="team@company.com">
</div>
<div class="form-group">
<label>Purpose *</label>
<textarea id="ns-purpose" required placeholder="What will this namespace be used for?"></textarea>
</div>
<div class="form-group">
<label>CPU Cores *</label>
<select id="ns-cpu" required>
<option value="1">1 core</option>
<option value="2" selected>2 cores</option>
<option value="4">4 cores</option>
<option value="8">8 cores</option>
</select>
</div>
<div class="form-group">
<label>Memory (GB) *</label>
<select id="ns-memory" required>
<option value="2">2 GB</option>
<option value="4" selected>4 GB</option>
<option value="8">8 GB</option>
<option value="16">16 GB</option>
</select>
</div>
<button type="submit">Generate YAML</button>
</form>
<div id="ns-output" style="display:none;" class="output">
<h3>Generated YAML</h3>
<pre id="ns-yaml"></pre>
<button class="copy-btn" onclick="copyToClipboard('ns-yaml')">📋 Copy to Clipboard</button>
<div class="auto-commit-section">
<h4>🚀 Automatic GitHub Integration</h4>
<p>Skip the manual copy-paste! Let the portal commit directly to your repository.</p>
<div class="commit-controls">
<button class="commit-btn" onclick="commitNamespaceToGithub()" id="ns-commit-btn" disabled>
📝 Commit to GitHub
</button>
<button class="pr-btn" onclick="createNamespacePR()" id="ns-pr-btn" disabled>
🔀 Create Pull Request
</button>
</div>
<div id="ns-commit-status"></div>
</div>
<div class="instructions">
<h4>Manual Steps (if not using auto-commit):</h4>
<ol>
<li>Copy the YAML above</li>
<li>Save it to: <code>namespaces/<span id="ns-env-path"></span>/<span id="ns-team-path"></span>-namespace.yaml</code></li>
<li>Commit and push to your platform-self-service repository</li>
<li>Create a Pull Request on GitHub</li>
<li>After approval and merge, ArgoCD will create your namespace</li>
</ol>
</div>
</div>
</div>
<!-- Storage Tab -->
<div id="storage" class="tab-content">
<h2>Request Azure Storage Account</h2>
<form onsubmit="generateStorageYAML(event)">
<div class="form-group">
<label>Team Name *</label>
<input type="text" id="st-team" required pattern="[a-z0-9-]+" placeholder="frontend">
<div class="help-text">Lowercase letters, numbers, and hyphens only</div>
</div>
<div class="form-group">
<label>Environment *</label>
<select id="st-env" required>
<option value="dev">Development</option>
<option value="staging">Staging</option>
<option value="prod">Production</option>
</select>
</div>
<div class="form-group">
<label>Storage Account Name *</label>
<input type="text" id="st-name" required pattern="[a-z0-9]{3,24}" placeholder="myteamstorage001">
<div class="help-text">3-24 characters, lowercase letters and numbers only. Must be globally unique!</div>
</div>
<div class="form-group">
<label>Purpose *</label>
<textarea id="st-purpose" required placeholder="What will this storage be used for?"></textarea>
</div>
<div class="form-group">
<label>Azure Region *</label>
<select id="st-region" required>
<option value="swedencentral" selected>Sweden Central</option>
<option value="westeurope">West Europe</option>
<option value="northeurope">North Europe</option>
</select>
</div>
<div class="form-group">
<label>Redundancy Level *</label>
<select id="st-sku" required>
<option value="Standard_LRS" selected>Standard Locally Redundant (LRS)</option>
<option value="Standard_GRS">Standard Geo-Redundant (GRS)</option>
</select>
</div>
<button type="submit">Generate YAML</button>
</form>
<div id="st-output" style="display:none;" class="output">
<h3>Generated YAML</h3>
<pre id="st-yaml"></pre>
<button class="copy-btn" onclick="copyToClipboard('st-yaml')">📋 Copy to Clipboard</button>
<div class="auto-commit-section">
<h4>🚀 Automatic GitHub Integration</h4>
<p>Skip the manual copy-paste! Let the portal commit directly to your repository.</p>
<div class="commit-controls">
<button class="commit-btn" onclick="commitStorageToGithub()" id="st-commit-btn" disabled>
📝 Commit to GitHub
</button>
<button class="pr-btn" onclick="createStoragePR()" id="st-pr-btn" disabled>
🔀 Create Pull Request
</button>
</div>
<div id="st-commit-status"></div>
</div>
<div class="instructions">
<h4>Manual Steps (if not using auto-commit):</h4>
<ol>
<li>Copy the YAML above</li>
<li>Save it to: <code>azure-resources/<span id="st-team-path"></span>/<span id="st-name-path"></span>.yaml</code></li>
<li>Commit and push to your platform-self-service repository</li>
<li>Create a Pull Request on GitHub</li>
<li>After approval and merge, ArgoCD and ASO will create your storage account</li>
</ol>
</div>
</div>
</div>
</div>
<script>
// Global variables for generated YAML content
let currentNamespaceYAML = '';
let currentNamespaceFilePath = '';
let currentStorageYAML = '';
let currentStorageFilePath = '';
function showTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
// Show selected tab
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
}
// GitHub API Helper Functions
function getGitHubConfig() {
return {
username: document.getElementById('gh-username').value,
repo: document.getElementById('gh-repo').value,
token: document.getElementById('gh-token').value
};
}
function updateStatus(elementId, message, type = 'info') {
const statusDiv = document.getElementById(elementId);
statusDiv.innerHTML = `<div class="status-message status-${type}">${message}</div>`;
}
async function testGitHubConnection() {
const config = getGitHubConfig();
if (!config.username || !config.repo || !config.token) {
updateStatus('gh-status', '❌ Please fill in all GitHub configuration fields', 'error');
return;
}
updateStatus('gh-status', '🔄 Testing GitHub connection...', 'info');
try {
const response = await fetch(`https://api.github.com/repos/${config.username}/${config.repo}`, {
headers: {
'Authorization': `token ${config.token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
if (response.ok) {
const repo = await response.json();
updateStatus('gh-status', `✅ Connected to ${repo.full_name} (${repo.default_branch} branch)`, 'success');
// Enable commit buttons
updateCommitButtonStates();
} else if (response.status === 404) {
updateStatus('gh-status', '❌ Repository not found. Check username and repository name.', 'error');
} else if (response.status === 401) {
updateStatus('gh-status', '❌ Authentication failed. Check your personal access token.', 'error');
} else {
updateStatus('gh-status', `❌ Error: ${response.status} ${response.statusText}`, 'error');
}
} catch (error) {
updateStatus('gh-status', `❌ Network error: ${error.message}`, 'error');
}
}
function updateCommitButtonStates() {
const config = getGitHubConfig();
const isConfigured = config.username && config.repo && config.token;
// Update namespace buttons
const nsCommitBtn = document.getElementById('ns-commit-btn');
const nsPrBtn = document.getElementById('ns-pr-btn');
if (nsCommitBtn) {
nsCommitBtn.disabled = !isConfigured || !currentNamespaceYAML;
}
if (nsPrBtn) {
nsPrBtn.disabled = !isConfigured || !currentNamespaceYAML;
}
// Update storage buttons
const stCommitBtn = document.getElementById('st-commit-btn');
const stPrBtn = document.getElementById('st-pr-btn');
if (stCommitBtn) {
stCommitBtn.disabled = !isConfigured || !currentStorageYAML;
}
if (stPrBtn) {
stPrBtn.disabled = !isConfigured || !currentStorageYAML;
}
}
async function commitFileToGitHub(filePath, content, commitMessage, statusElementId) {
const config = getGitHubConfig();
updateStatus(statusElementId, '🔄 Committing to GitHub...', 'info');
try {
// First, get the current file (if it exists) to get its SHA
let sha = null;
try {
const existingResponse = await fetch(`https://api.github.com/repos/${config.username}/${config.repo}/contents/${filePath}`, {
headers: {
'Authorization': `token ${config.token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
if (existingResponse.ok) {
const existingFile = await existingResponse.json();
sha = existingFile.sha;
}
} catch (e) {
// File doesn't exist, which is fine for new files
}
// Commit the file
const commitData = {
message: commitMessage,
content: btoa(unescape(encodeURIComponent(content))), // Base64 encode with UTF-8 support
branch: 'main'
};
if (sha) {
commitData.sha = sha; // Include SHA if updating existing file
}
const response = await fetch(`https://api.github.com/repos/${config.username}/${config.repo}/contents/${filePath}`, {
method: 'PUT',
headers: {
'Authorization': `token ${config.token}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
},
body: JSON.stringify(commitData)
});
if (response.ok) {
const result = await response.json();
updateStatus(statusElementId,
`✅ File committed successfully! <a href="${result.content.html_url}" target="_blank">View on GitHub</a>`,
'success'
);
return result;
} else {
const error = await response.json();
updateStatus(statusElementId,
`❌ Commit failed: ${error.message || response.statusText}`,
'error'
);
return null;
}
} catch (error) {
updateStatus(statusElementId, `❌ Error: ${error.message}`, 'error');
return null;
}
}
async function createPullRequest(title, body, statusElementId) {
const config = getGitHubConfig();
updateStatus(statusElementId, '🔄 Creating pull request...', 'info');
try {
const prData = {
title: title,
body: body,
head: 'main',
base: 'main'
};
const response = await fetch(`https://api.github.com/repos/${config.username}/${config.repo}/pulls`, {
method: 'POST',
headers: {
'Authorization': `token ${config.token}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
},
body: JSON.stringify(prData)
});
if (response.ok) {
const pr = await response.json();
updateStatus(statusElementId,
`✅ Pull request created! <a href="${pr.html_url}" target="_blank">View PR #${pr.number}</a>`,
'success'
);
} else if (response.status === 422) {
// PR might already exist or no changes to create PR
updateStatus(statusElementId,
'⚠️ Pull request could not be created. Changes may already be on main branch or PR already exists.',
'info'
);
} else {
const error = await response.json();
updateStatus(statusElementId,
`❌ PR creation failed: ${error.message || response.statusText}`,
'error'
);
}
} catch (error) {
updateStatus(statusElementId, `❌ Error: ${error.message}`, 'error');
}
} function generateNamespaceYAML(event) {
event.preventDefault();
const team = document.getElementById('ns-team').value;
const env = document.getElementById('ns-env').value;
const email = document.getElementById('ns-email').value;
const purpose = document.getElementById('ns-purpose').value;
const cpu = document.getElementById('ns-cpu').value;
const memory = document.getElementById('ns-memory').value;
const cpuLimit = cpu * 2;
const memoryLimit = memory * 2;
const date = new Date().toISOString().split('T')[0];
const yaml = `# Kubernetes Namespace Request
# Team: ${team}
# Environment: ${env}
# Requested: ${date}
---
apiVersion: v1
kind: Namespace
metadata:
name: devops-${team}-${env}
labels:
team: ${team}
environment: ${env}
managed-by: platform-team
created-via: self-service
annotations:
team.contact: "${email}"
purpose: "${purpose}"
requested-date: "${date}"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: ${team}-${env}-quota
namespace: devops-${team}-${env}
spec:
hard:
requests.cpu: "${cpu}"
requests.memory: "${memory}Gi"
limits.cpu: "${cpuLimit}"
limits.memory: "${memoryLimit}Gi"
persistentvolumeclaims: "5"
services: "10"
pods: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: ${team}-${env}-limits
namespace: devops-${team}-${env}
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: Container`;
document.getElementById('ns-yaml').textContent = yaml;
document.getElementById('ns-output').style.display = 'block';
document.getElementById('ns-env-path').textContent = env;
document.getElementById('ns-team-path').textContent = team;
// Store YAML for GitHub integration
currentNamespaceYAML = yaml;
currentNamespaceFilePath = `namespaces/${env}/${team}-namespace.yaml`;
// Update button states
updateCommitButtonStates();
// Scroll to output
document.getElementById('ns-output').scrollIntoView({ behavior: 'smooth' });
}
function generateStorageYAML(event) {
event.preventDefault();
const team = document.getElementById('st-team').value;
const env = document.getElementById('st-env').value;
const name = document.getElementById('st-name').value;
const purpose = document.getElementById('st-purpose').value;
const region = document.getElementById('st-region').value;
const sku = document.getElementById('st-sku').value;
const date = new Date().toISOString().split('T')[0];
const yaml = `# Azure Storage Account Request
# Team: ${team}
# Environment: ${env}
# Requested: ${date}
---
# Resource Group for Storage
apiVersion: resources.azure.com/v1api20200601
kind: ResourceGroup
metadata:
name: ${team}-${env}-storage-rg
namespace: default
spec:
location: ${region}
tags:
team: ${team}
environment: ${env}
resource-type: storage
managed-by: platform-team
created-via: self-service
requested-date: "${date}"
---
# Storage Account
apiVersion: storage.azure.com/v1api20230101
kind: StorageAccount
metadata:
name: ${name}
namespace: default
spec:
location: ${region}
kind: StorageV2
sku:
name: ${sku}
owner:
name: ${team}-${env}-storage-rg
accessTier: Hot
minimumTlsVersion: TLS1_2
supportsHttpsTrafficOnly: true
allowBlobPublicAccess: false
tags:
team: ${team}
environment: ${env}
purpose: "${purpose}"
managed-by: platform-team
created-via: self-service`;
document.getElementById('st-yaml').textContent = yaml;
document.getElementById('st-output').style.display = 'block';
document.getElementById('st-team-path').textContent = team;
document.getElementById('st-name-path').textContent = name;
// Store YAML for GitHub integration
currentStorageYAML = yaml;
currentStorageFilePath = `azure-resources/${team}/${name}.yaml`;
// Update button states
updateCommitButtonStates();
// Scroll to output
document.getElementById('st-output').scrollIntoView({ behavior: 'smooth' });
}
function copyToClipboard(elementId) {
const text = document.getElementById(elementId).textContent;
navigator.clipboard.writeText(text).then(() => {
// Show feedback
event.target.textContent = '✅ Copied!';
setTimeout(() => {
event.target.textContent = '📋 Copy to Clipboard';
}, 2000);
});
}
// GitHub Integration Functions
async function commitNamespaceToGithub() {
if (!currentNamespaceYAML) {
updateStatus('ns-commit-status', '❌ No YAML to commit. Generate YAML first.', 'error');
return;
}
const team = document.getElementById('ns-team').value;
const env = document.getElementById('ns-env').value;
const email = document.getElementById('ns-email').value;
const purpose = document.getElementById('ns-purpose').value;
const commitMessage = `Request namespace for ${team} in ${env} environment
Requested by: ${email}
Purpose: ${purpose}
Generated via: Self-Service Portal`;
await commitFileToGitHub(currentNamespaceFilePath, currentNamespaceYAML, commitMessage, 'ns-commit-status');
}
async function createNamespacePR() {
if (!currentNamespaceYAML) {
updateStatus('ns-commit-status', '❌ No YAML to create PR with. Generate YAML first.', 'error');
return;
}
const team = document.getElementById('ns-team').value;
const env = document.getElementById('ns-env').value;
const email = document.getElementById('ns-email').value;
const purpose = document.getElementById('ns-purpose').value;
const title = `Request namespace: devops-${team}-${env}`;
const body = `## Namespace Request
**Team:** ${team}
**Environment:** ${env}
**Contact:** ${email}
**Purpose:** ${purpose}
### Resources Requested
- CPU: ${document.getElementById('ns-cpu').value} cores
- Memory: ${document.getElementById('ns-memory').value} GB
### Generated Resources
- Namespace: \`devops-${team}-${env}\`
- ResourceQuota: CPU and memory limits
- LimitRange: Default container limits
This request was generated using the Self-Service Portal.
/cc @platform-team`;
await createPullRequest(title, body, 'ns-commit-status');
}
async function commitStorageToGithub() {
if (!currentStorageYAML) {
updateStatus('st-commit-status', '❌ No YAML to commit. Generate YAML first.', 'error');
return;
}
const team = document.getElementById('st-team').value;
const env = document.getElementById('st-env').value;
const name = document.getElementById('st-name').value;
const purpose = document.getElementById('st-purpose').value;
const commitMessage = `Request Azure storage account ${name}
Team: ${team}
Environment: ${env}
Purpose: ${purpose}
Generated via: Self-Service Portal`;
await commitFileToGitHub(currentStorageFilePath, currentStorageYAML, commitMessage, 'st-commit-status');
}
async function createStoragePR() {
if (!currentStorageYAML) {
updateStatus('st-commit-status', '❌ No YAML to create PR with. Generate YAML first.', 'error');
return;
}
const team = document.getElementById('st-team').value;
const env = document.getElementById('st-env').value;
const name = document.getElementById('st-name').value;
const purpose = document.getElementById('st-purpose').value;
const region = document.getElementById('st-region').value;
const sku = document.getElementById('st-sku').value;
const title = `Request Azure storage account: ${name}`;
const body = `## Azure Storage Request
**Team:** ${team}
**Environment:** ${env}
**Storage Account:** ${name}
**Purpose:** ${purpose}
### Configuration
- Region: ${region}
- SKU: ${sku}
- TLS Version: 1.2 minimum
- HTTPS Only: Yes
- Public Blob Access: Disabled
### Generated Resources
- Resource Group: \`${team}-${env}-storage-rg\`
- Storage Account: \`${name}\`
This request was generated using the Self-Service Portal.
/cc @platform-team`;
await createPullRequest(title, body, 'st-commit-status');
}
// Initialize button states on page load
document.addEventListener('DOMContentLoaded', function() {
updateCommitButtonStates();
});
</script>
</body>
</html>
EOF
echo "✅ Created web UI HTML file"# Create a ConfigMap with the HTML file
kubectl create configmap self-service-ui \
--from-file=index.html=templates/helpers/self-service-ui.html \
-n default
# Create a simple nginx deployment to serve the UI
cat << 'EOF' > /tmp/self-service-ui-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: self-service-ui
namespace: default
labels:
app: self-service-ui
spec:
replicas: 1
selector:
matchLabels:
app: self-service-ui
template:
metadata:
labels:
app: self-service-ui
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
configMap:
name: self-service-ui
---
apiVersion: v1
kind: Service
metadata:
name: self-service-ui
namespace: default
spec:
selector:
app: self-service-ui
ports:
- port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: self-service-ui
namespace: default
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
ingressClassName: nginx
rules:
- host: selfservice.127.0.0.1.nip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: self-service-ui
port:
number: 80
EOF
# Apply the deployment
kubectl apply -f /tmp/self-service-ui-deployment.yaml
# Wait for pod to be ready
kubectl wait --for=condition=ready pod -l app=self-service-ui -n default --timeout=60s
echo "✅ Web UI deployed successfully!"
echo
echo "Access the UI at: http://selfservice.127.0.0.1.nip.io"# Check deployment status
kubectl get pods -l app=self-service-ui -n default
kubectl get svc self-service-ui -n default
kubectl get ingress self-service-ui -n default
# Open in browser
echo "Open your browser to: http://selfservice.127.0.0.1.nip.io"The web UI now includes automatic GitHub integration that can commit files and create PRs directly from the browser. This eliminates the manual copy-paste workflow!
- Go to GitHub Settings: https://github.com/settings/tokens
- Click "Generate new token (classic)"
- Configure the token:
- Name:
Platform Self-Service Portal - Expiration:
30 days(or as per your security policy) - Scopes: Select
repo(Full control of private repositories)
- Name:
- Generate and copy the token (starts with
ghp_)
- Open the web UI: http://selfservice.127.0.0.1.nip.io
- Fill in the GitHub Integration Setup section:
- GitHub Username: Your GitHub username (e.g.,
your-username) - Repository Name: Your platform-self-service repository name (e.g.,
platform-self-service) - Personal Access Token: The token you created (paste the full
ghp_...token)
- GitHub Username: Your GitHub username (e.g.,
- Click "Test Connection" to verify the setup
✅ Expected Result: "Connected to your-username/platform-self-service (main branch)"
-
Fill out a namespace request form:
- Team Name:
autotest - Environment:
dev - Contact Email:
autotest@workshop.local - Purpose:
Testing automatic GitHub integration - CPU Cores:
2 - Memory:
4 GB
- Team Name:
-
Click "Generate YAML" - The YAML will be displayed
-
Click "📝 Commit to GitHub" - This will:
- Create/update the file
namespaces/dev/autotest-namespace.yaml - Commit directly to the main branch
- Show a success message with link to view on GitHub
- Create/update the file
-
Verify the commit:
cd ~/platform-self-service git pull origin main cat namespaces/dev/autotest-namespace.yaml
For a more controlled workflow, you can create PRs instead of direct commits:
- Generate YAML as before
- Click "🔀 Create Pull Request" instead of commit
- Review the PR on GitHub with detailed description and context
- Merge the PR after review to deploy the resources
✅ No manual copy-paste: Eliminates human errors in YAML handling ✅ Immediate feedback: See results instantly with links to GitHub ✅ Proper commit messages: Structured, informative commit messages ✅ PR automation: Rich PR descriptions with all context ✅ Audit trail: Every request tracked in Git history ✅ Workshop-friendly: Works entirely in the browser ✅ No infrastructure: Client-side integration with GitHub API
# Verify web UI deployment
kubectl get deployment self-service-ui -n default
kubectl get pods -l app=self-service-ui -n default
# Check service and ingress
kubectl get svc self-service-ui -n default
kubectl get ingress self-service-ui -n default
# Test the UI is accessible
curl -I http://selfservice.127.0.0.1.nip.io
# Commit the web UI file
cd ~/platform-self-service
git add templates/helpers/self-service-ui.html
git commit -m "Add simple web UI with GitHub integration for self-service portal"
git push origin mainExpected Output:
- Web UI HTML file created with GitHub integration
- nginx pod running in default namespace
- Service and Ingress configured
- UI accessible at http://selfservice.127.0.0.1.nip.io
- Beautiful form interface with auto-commit capabilities
- GitHub configuration section at the top of the UI
GitHub Integration Test:
# After configuring GitHub integration in the UI and testing auto-commit:
cd ~/platform-self-service
git pull origin main
ls -la namespaces/dev/ # Should show autotest-namespace.yaml if you tested
git log --oneline -5 # Should show commits from the web UINow let's test the complete workflow using either the command-line helper or the web UI.
# Navigate to helpers directory
cd ~/platform-self-service/templates/helpers
# Request a namespace for testteam
./request-namespace.sh
# When prompted, enter:
# Team Name: testteam
# Environment: dev
# Contact Email: testteam@workshop.local
# Purpose: Testing self-service workflow with CLI helper
# CPU Cores: 2
# Memory GB: 4
# Commit now: y
# Push to GitHub
git push origin main
# Watch ArgoCD sync the namespace
kubectl get namespace --watch
# (Press Ctrl+C after you see devops-testteam-dev)
# Verify the namespace was created
kubectl get namespace devops-testteam-dev
kubectl describe namespace devops-testteam-dev
kubectl get resourcequota -n devops-testteam-dev
kubectl get limitrange -n devops-testteam-dev-
Open the Web UI:
echo "Open: http://selfservice.127.0.0.1.nip.io"
-
Fill in the Namespace Request Form:
- Team Name:
webteam - Environment:
dev - Contact Email:
webteam@workshop.local - Purpose:
Testing self-service workflow with web UI - CPU Cores:
2 - Memory:
4 GB
- Team Name:
-
Click "Generate YAML" and review the output
-
Copy the YAML and save it:
cd ~/platform-self-service mkdir -p namespaces/dev # Paste the copied YAML into a new file nano namespaces/dev/webteam-namespace.yaml # (or use your preferred editor) # Commit and push git add namespaces/dev/webteam-namespace.yaml git commit -m "Request namespace for webteam dev environment" git push origin main
-
Watch ArgoCD sync:
# If you have ArgoCD ApplicationSet from LAB02, it will auto-sync # Otherwise, manually sync: argocd app sync dev-namespaces # Verify namespace creation kubectl get namespace devops-webteam-dev kubectl get resourcequota -n devops-webteam-dev
Let's test requesting Azure storage using the web UI:
-
Open the Web UI and switch to "Azure Storage" tab
-
Fill in the Storage Request Form:
- Team Name:
testteam - Environment:
dev - Storage Account Name:
testteamstorage001(must be globally unique!) - Purpose:
Testing Azure resource provisioning - Azure Region:
swedencentral - Redundancy Level:
Standard_LRS
- Team Name:
-
Generate and save the YAML:
cd ~/platform-self-service mkdir -p azure-resources/testteam # Save the generated YAML to the file nano azure-resources/testteam/testteamstorage001.yaml # Commit and push git add azure-resources/testteam/testteamstorage001.yaml git commit -m "Request Azure storage account for testteam" git push origin main
-
Watch ArgoCD sync and ASO create resources:
# Sync ArgoCD application (assuming it's configured for azure-resources) argocd app sync azure-resources # Watch resources being created kubectl get resourcegroup,storageaccount --watch # Verify storage account (this may take a few minutes) kubectl get storageaccount -n default kubectl describe storageaccount testteamstorage001
Complete workflow verification:
# Check namespaces created
kubectl get namespace | grep devops
kubectl get namespace devops-testteam-dev -o yaml | grep -A 5 "labels:"
kubectl get namespace devops-webteam-dev -o yaml | grep -A 5 "labels:"
# Verify resource quotas
kubectl get resourcequota --all-namespaces | grep testteam
kubectl describe resourcequota -n devops-testteam-dev
# Check Azure resources (if LAB03 completed)
kubectl get resourcegroup,storageaccount --all-namespaces | grep testteam
# View Git history
cd ~/platform-self-service
git log --oneline --graph -10
# Check ArgoCD application status
argocd app list
argocd app get dev-namespaces | grep -E "(Health|Sync)"Expected Results:
- ✅ Namespaces created with correct labels and quotas
- ✅ Resource limits applied
- ✅ Azure storage account request in queue or created
- ✅ Git history shows commits from self-service requests
- ✅ ArgoCD applications healthy and synced
# Check pod status
kubectl get pods -l app=self-service-ui -n default
kubectl describe pod -l app=self-service-ui -n default
# Check logs
kubectl logs -l app=self-service-ui -n default
# Verify ingress
kubectl get ingress self-service-ui -n default
kubectl describe ingress self-service-ui -n default
# Test locally
kubectl port-forward -n default svc/self-service-ui 8080:80
# Then open http://localhost:8080# Make sure scripts are executable
chmod +x ~/platform-self-service/templates/helpers/*.sh
# Check script permissions
ls -la ~/platform-self-service/templates/helpers/
# If permission denied, check file ownership
ls -l ~/platform-self-service/templates/helpers/request-namespace.sh# Validate generated YAML
cd ~/platform-self-service
kubectl apply --dry-run=client -f namespaces/dev/testteam-namespace.yaml
# Check for syntax errors
kubectl apply --dry-run=server -f namespaces/dev/testteam-namespace.yaml
# Verify placeholders were replaced
cat namespaces/dev/testteam-namespace.yaml | grep -E "{{|}}"
# Should return no results if all placeholders were replaced# Check ArgoCD is polling correctly
argocd app get dev-namespaces
# Manually trigger sync
argocd app sync dev-namespaces
# Force refresh from Git
argocd app get dev-namespaces --refresh
# Check repository connection
argocd repo list# Verify Git configuration
cd ~/platform-self-service
git remote -v
git status
# Check for uncommitted changes
git diff
# If push fails, pull first
git pull origin main
git push origin mainYou now have a lightweight, reliable self-service platform where:
- Developers use simple tools instead of complex infrastructure
- YAML is generated automatically from templates
- No heavy dependencies - works reliably in local environments
- Multiple interfaces - CLI for automation, Web UI for convenience
- Full GitOps workflow maintained from LAB02
- Platform team reviews PRs before resources are created
- Complete audit trail exists in Git history
Simple Approach (LAB04A with GitHub Integration):
- ✅ Quick to set up (minutes, not hours)
- ✅ Minimal dependencies (nginx, bash, git)
- ✅ Easy to customize and extend
- ✅ Reliable in workshop/local environments
- ✅ Low resource requirements
- ✅ GitOps workflow maintained
- ✅ Auto-commit and PR creation (NEW!)
- ✅ No manual copy-paste (NEW!)
- ✅ Rich commit messages and PR descriptions (NEW!)
⚠️ Simple resource catalog (not comprehensive like Backstage)
Complex Portal (e.g., Backstage):
- ✅ Rich UI with many features
- ✅ Automatic PR creation
- ✅ Service catalog and discovery
- ✅ Plugins for extensibility
⚠️ Complex setup and configuration⚠️ Heavy resource requirements⚠️ Can be unreliable in local/Kind environments⚠️ Steeper learning curve
Use Simple Approach When:
- Workshop or training environments
- Local development on Kind/k3s
- Small teams (5-20 developers)
- Getting started with platform engineering
- Limited infrastructure resources
- Need quick wins and fast iteration
Consider Complex Portal When:
- Large organizations (100+ developers)
- Production environments with dedicated infrastructure
- Need advanced features (service catalog, tech radar, etc.)
- Have platform team to maintain the portal
- Integration with many external systems
- Mature platform engineering practice
To make this approach production-ready, you would:
- Authentication: Add authentication to the web UI (OAuth, OIDC)
- Token Security: Replace personal tokens with GitHub Apps or organization tokens
- RBAC: Implement role-based access control for Git repository
- Validation: Add admission controllers to validate generated YAML
- Branch Protection: Use feature branches instead of direct commits to main
- Approvals: Configure branch protection and CODEOWNERS for reviews
- Monitoring: Add metrics for resource requests and usage
- Documentation: Create comprehensive guides for developers
- Templates: Expand template library for all common resources
- Rate Limiting: Implement API rate limiting and error handling
To remove the self-service tools:
# Delete web UI deployment
kubectl delete deployment self-service-ui -n default
kubectl delete svc self-service-ui -n default
kubectl delete ingress self-service-ui -n default
kubectl delete configmap self-service-ui -n default
# Remove test resources
kubectl delete namespace devops-testteam-dev
kubectl delete namespace devops-webteam-dev
# Remove Azure test resources (if created)
# az storage account delete --name testteamstorage001 --resource-group testteam-dev-storage-rg --yes
# az group delete --name testteam-dev-storage-rg --yes
# Keep templates in repository for future use
# They're lightweight and valuable for the platformCongratulations! You've completed the Platform Engineering Workshop!
Across all labs, you've built a complete Internal Developer Platform with:
LAB01:
- ✅ Local Kubernetes cluster (Kind)
- ✅ GitOps with ArgoCD
LAB02:
- ✅ Self-service via Git
- ✅ Multi-tenant namespaces with quotas
- ✅ ArgoCD ApplicationSets for automation
LAB03:
- ✅ Azure Service Operator for cloud resources
- ✅ Kubernetes as control plane for Azure
- ✅ GitOps for infrastructure
LAB04A:
- ✅ Simple, reliable self-service tools
- ✅ YAML templates for common resources
- ✅ Command-line and web interfaces
- ✅ Practical approach for workshop environments
- ✅ Complete developer experience with GitOps
If you have time, continue to LAB04B: Advanced Platform Concepts - Abstractions to learn about:
- Kubernetes Resource Model (KRO)
- Creating higher-level abstractions
- Hiding infrastructure complexity
- Building "App Concepts" that compose multiple resources
As your platform matures, you can evolve this approach:
-
Phase 1 (Where you are now):
- Simple templates and scripts
- Manual PR creation
- Basic GitOps workflow
-
Phase 2 (Near future):
- GitHub Actions to automate PR creation from web UI
- More sophisticated templates
- Validation webhooks
-
Phase 3 (Mature platform):
- Consider adopting Backstage or similar portal
- Advanced service catalog
- Self-service for all platform capabilities
- Integration with monitoring, logging, etc.
Platform Engineering:
Tools Deep Dive:
- ArgoCD Documentation
- Backstage Documentation - When you're ready for it
- Azure Service Operator
- Crossplane - Alternative to ASO
Self-Service Patterns:
CNCF Landscape:
- CNCF Cloud Native Interactive Landscape
- Explore tools for observability, security, networking, storage
In this lab, you:
- ✅ Created reusable YAML templates for common resources
- ✅ Built interactive command-line helper scripts
- ✅ Deployed a simple, beautiful web UI for self-service
- ✅ Added automatic GitHub integration with one-click commits and PR creation
- ✅ Tested complete self-service workflow with zero manual copy-paste
- ✅ Maintained GitOps principles with simple tools
- ✅ Learned practical alternatives to complex developer portals
- Simplicity is Powerful: Simple tools can provide great developer experience
- Templates are Golden Paths: Standardized templates reduce errors and ensure best practices
- GitOps Provides Safety: All changes go through Git and PR reviews
- Right Tool for Context: Choose tools appropriate for your environment and maturity
- Developer Experience Matters: Even simple interfaces dramatically improve adoption
- Start Simple, Evolve: Begin with simple approaches and add complexity as needed
- Reliability Wins: Tools that work reliably are better than feature-rich but problematic ones
- API Integration is Accessible: Modern web APIs make powerful automation achievable with simple tools
- Progressive Enhancement: You can start manual and add automation incrementally
- Workshop-Friendly: Reliable in time-constrained workshop settings
- Low Resource: Runs smoothly on local Kind clusters
- Fast Setup: Minutes instead of hours
- Easy to Understand: Participants can see how everything works
- Maintainable: No complex dependencies to manage
- Extensible: Easy to add new templates and features
- Production-Ready Path: Clear evolution to more sophisticated tools
Template Systems:
- Kustomize - For more advanced YAML templating
- Helm - Package manager that can work with templates
- yq - YAML processor for scripts
GitOps Best Practices:
Developer Portals (when you're ready):
Platform Engineering Community:
Congratulations on completing LAB04A!
You've built a practical, workshop-friendly self-service platform with:
- ✅ Simple, reliable tools that work everywhere
- ✅ Beautiful web interface and convenient CLI tools
- ✅ YAML templates for standardized resources
- ✅ Complete GitOps workflow maintained
- ✅ Low resource requirements for local development
- ✅ Clear path to more sophisticated solutions
This approach demonstrates that you don't always need complex tools to deliver great developer experience. Sometimes, the simplest solution is the best solution! 🚀