Initial Commit

This commit is contained in:
Nishan
2025-04-02 10:39:59 +05:30
commit d8b69865f6
387 changed files with 28558 additions and 0 deletions

63
.gitignore vendored Normal file
View File

@@ -0,0 +1,63 @@
# Node.js dependencies and logs
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
yarn.lock
.npm/
# Environment variables
.env
.env.*
!.env.example
# Docker related
.docker/
docker-compose.override.yml
# Kubernetes secrets (in production, consider using sealed secrets or other secure methods)
# k8s/secrets/
# Build outputs
dist/
build/
tmp/
out/
logs/
*.log
# IDE and editors
.idea/
.vscode/
*.swp
*.swo
.DS_Store
.history/
*.sublime*
.project
.classpath
.settings/
*.code-workspace
# OS generated files
Thumbs.db
ehthumbs.db
Desktop.ini
# Temporary files
*.tmp
*.bak
.temp/
.cache/
# Testing
coverage/
.nyc_output/
# Miscellaneous
.vagrant/
.terraform/
terraform.tfstate
terraform.tfstate.backup
.terragrunt-cache/

63
README.md Normal file
View File

@@ -0,0 +1,63 @@
# CK-X Simulator 🚀
A powerful Kubernetes certification practice environment that provides a realistic exam-like experience for kubernetess exam preparation.
## Major Features
- **Realistic exam environment** with web-based interface and remote desktop support
- Comprehensive practice labs for **CKAD, CKA, CKS**, and other Kubernetes certifications
- **Smart evaluation system** with real-time solution verification
- **Docker-based deployment** for easy setup and consistent environment
- **Timed exam mode** with real exam-like conditions and countdown timer
## Installation
#### Linux & macOS
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/nishanb/ck-x/main/scripts/install.sh)
```
#### Windows
```powershell
irm https://raw.githubusercontent.com/nishanb/ck-x/main/scripts/install.ps1 | iex
```
### Manual Installation
For detailed installation instructions, please refer to our [Deployment Guide](scripts/COMPOSE-DEPLOY.md).
## Community & Support
- Join our [Telegram Community](https://t.me/ckxdev) for discussions and support
- Feature requests and pull requests are welcome
## Adding New Labs
Check our [Lab Creation Guide](docs/how-to-add-new-labs.md) for instructions on adding new labs.
## Contributing
We welcome contributions! Whether you want to:
- Add new practice labs
- Improve existing features
- Fix bugs
- Enhance documentation
## Buy Me a Coffee ☕
If you find CK-X Simulator helpful, consider [buying me a coffee](https://buymeacoffee.com/nishan.b) to support the project.
## Disclaimer
CK-X is an independent tool, not affiliated with CNCF, Linux Foundation, or PSI. We do not guarantee exam success. Please read our [Privacy Policy](PRIVACY_POLICY.md) and [Terms of Service](TERMS_OF_SERVICE.md) for more details about data collection, usage, and limitations.
## Acknowledgments
- [DIND](https://github.com/earthly/dind)
- [KIND](https://github.com/kubernetes-sigs/kind)
- [Node](https://nodejs.org/en)
- [Nginx](https://nginx.org/)
- [ConSol-Vnc](https://github.com/ConSol/docker-headless-vnc-container/)
## License
This project is licensed under the MIT License - see the LICENSE file for details.

39
TERMS_OF_SERVICE.md Normal file
View File

@@ -0,0 +1,39 @@
# Terms of Service
**Effective Date:** [Insert Date]
## 1. Acceptance of Terms
By accessing or using CK-X, you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use CK-X.
## 2. No Affiliation
CK-X is **not affiliated with, endorsed by, or associated with the Cloud Native Computing Foundation (CNCF), the Linux Foundation, or PSI.** Any references to Kubernetes, CKAD, CKA, or CKS are for descriptive purposes only, and all trademarks belong to their respective owners.
## 3. Use of CK-X
- CK-X is provided for educational and training purposes only.
- You may use CK-X to practice Kubernetes concepts and improve your skills.
- You may not use CK-X for any illegal, malicious, or unauthorized purposes.
## 4. No Guarantees
- CK-X does **not** guarantee success in CKAD, CKA, or CKS certification exams.
- The content provided is for **educational purposes only** and does not replicate actual exam questions.
## 5. Data Collection & Privacy
- CK-X collects minimal data necessary for improving the simulator.
- We **do not** sell, share, or misuse user data.
- See our [Privacy Policy](PRIVACY_POLICY.md) for more details.
## 6. Contributions
- Contributions to CK-X are welcome.
- By submitting code or content, you agree that it can be used and distributed under the CK-X projects open-source license.
## 7. Security Disclaimer
- CK-X is designed for educational use only and should **not** be deployed in production environments.
- Running CK-X on personal or enterprise systems is **at your own risk**.
- We do **not** take responsibility for security vulnerabilities, misconfigurations, or unintended consequences of using CK-X.
## 8. Limitation of Liability
- CK-X is provided "as is" without any warranties.
- The maintainers and contributors are not liable for any losses, damages, or exam failures resulting from the use of CK-X.
## 9. Changes to Terms
We reserve the right to update these Terms of Service at any time. Continued use of CK-X after changes implies acceptance of the revised terms.

57
app/.dockerignore Normal file
View File

@@ -0,0 +1,57 @@
# Dependency directories
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
# Build artifacts
dist/
build/
*.tar.gz
*.tgz
# Environment files
.env
.env.local
.env.development
.env.test
.env.production
# Docker files
Dockerfile
docker-compose*.yml
compose*.yaml
.dockerignore
# Logs
logs/
*.log
# Testing
coverage/
.nyc_output/
.coverage
.pytest_cache/
__pycache__/
*.pyc
*.pyo
*.pyd
# OS specific
.DS_Store
Thumbs.db
# Editor directories and files
.idea/
.vscode/
*.swp
*.swo
*~
# Other unnecessary files
*.gz
*.zip
*.tar
*.rar
tmp/
temp/

22
app/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM node:16-alpine
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy the application files
COPY server.js ./
COPY public/ ./public/
# Ensure the public directory exists
RUN mkdir -p /app/public
# Expose the port
EXPOSE 3000
# Start the application
CMD ["node", "server.js"]

19
app/config/config.js Normal file
View File

@@ -0,0 +1,19 @@
/**
* Configuration module for the application
* Centralizes all environment variables and configuration settings
*/
// Server configuration
const PORT = process.env.PORT || 3000;
// VNC service configuration
const VNC_SERVICE_HOST = process.env.VNC_SERVICE_HOST || 'remote-desktop-service';
const VNC_SERVICE_PORT = process.env.VNC_SERVICE_PORT || 6901;
const VNC_PASSWORD = process.env.VNC_PASSWORD || 'bakku-the-wizard'; // Default password
module.exports = {
PORT,
VNC_SERVICE_HOST,
VNC_SERVICE_PORT,
VNC_PASSWORD
};

7
app/init.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Copy the HTML file to Nginx's directory
cp /app/public/* /usr/share/nginx/html/
# Keep the container running
tail -f /dev/null

View File

@@ -0,0 +1,22 @@
/**
* Error Handler Middleware
* Centralized error handling for the application
*/
const path = require('path');
/**
* Global error handler middleware
* @param {Error} err - The error object
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function errorHandler(err, req, res, next) {
console.error('Server error:', err);
// Send a user-friendly error page
res.status(500).sendFile(path.join(__dirname, '..', 'public', '50x.html'));
}
module.exports = errorHandler;

91
app/middleware/proxy.js Normal file
View File

@@ -0,0 +1,91 @@
/**
* Proxy middleware module
* Sets up the proxies for VNC connections
*/
const { createProxyMiddleware } = require('http-proxy-middleware');
const config = require('../config/config');
/**
* Creates the VNC proxy configuration object
* @returns {Object} Proxy configuration
*/
function createVncProxyConfig() {
return {
target: `http://${config.VNC_SERVICE_HOST}:${config.VNC_SERVICE_PORT}`,
changeOrigin: true,
ws: true,
secure: false,
pathRewrite: {
'^/vnc-proxy': ''
},
onProxyReq: (proxyReq, req, res) => {
// Log HTTP requests being proxied
console.log(`Proxying HTTP request to VNC server: ${req.url}`);
},
onProxyReqWs: (proxyReq, req, socket, options, head) => {
// Log WebSocket connections
console.log(`WebSocket connection established to VNC server: ${req.url}`);
},
onProxyRes: (proxyRes, req, res) => {
// Log the responses from VNC server
console.log(`Received response from VNC server for: ${req.url}`);
},
onError: (err, req, res) => {
console.error(`Proxy error: ${err.message}`);
if (res && res.writeHead) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end(`Proxy error: ${err.message}`);
}
}
};
}
/**
* Sets up VNC proxy middleware on the Express app
* @param {Object} app - Express application
*/
function setupProxies(app) {
const vncProxyConfig = createVncProxyConfig();
// Middleware to enhance VNC URLs with authentication if needed
app.use('/vnc-proxy', (req, res, next) => {
// Check if the URL already has a password parameter
if (!req.query.password) {
// If no password provided, add default password
console.log('Adding default VNC password to request');
const separator = req.url.includes('?') ? '&' : '?';
req.url = `${req.url}${separator}password=${config.VNC_PASSWORD}`;
}
next();
}, createProxyMiddleware(vncProxyConfig));
// Direct WebSocket proxy to handle the websockify endpoint
app.use('/websockify', createProxyMiddleware({
...vncProxyConfig,
pathRewrite: {
'^/websockify': '/websockify'
},
ws: true,
onProxyReqWs: (proxyReq, req, socket, options, head) => {
// Log WebSocket connections to websockify
console.log(`WebSocket connection to websockify established: ${req.url}`);
// Add additional headers if needed
proxyReq.setHeader('Origin', `http://${config.VNC_SERVICE_HOST}:${config.VNC_SERVICE_PORT}`);
},
onError: (err, req, res) => {
console.error(`Websockify proxy error: ${err.message}`);
if (res && res.writeHead) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end(`Websockify proxy error: ${err.message}`);
}
}
}));
}
module.exports = setupProxies;

21
app/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "killer-sh-clone-webapp",
"version": "1.0.0",
"description": "Web application for killer.sh clone with VNC access",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.2",
"http-proxy-middleware": "^2.0.7",
"socket.io": "^4.7.2",
"ssh2": "^1.14.0",
"xterm": "^5.3.0"
},
"devDependencies": {
"nodemon": "^2.0.15"
}
}

44
app/public/50x.html Normal file
View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Error - CKAD Simulator</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-container {
max-width: 600px;
padding: 2rem;
background-color: white;
border-radius: 8px;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
text-align: center;
}
.error-icon {
font-size: 5rem;
color: #dc3545;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">⚠️</div>
<h1 class="mb-4">Server Error</h1>
<p class="lead mb-4">Sorry, something went wrong on our server. Please try again later.</p>
<a href="/" class="btn btn-primary">Go back to home page</a>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

65
app/public/answers.html Normal file
View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exam Answers - CK-X Simulator</title>
<link rel="stylesheet" href="/css/styles.css">
<link rel="stylesheet" href="/css/results.css">
<link rel="stylesheet" href="/css/answers.css">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Marked.js for Markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Highlight.js for code syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
</head>
<body>
<div class="container">
<main class="answers-container">
<!-- Loading state -->
<div id="pageLoader" class="page-loader">
<div class="spinner"></div>
<p>Loading exam answers...</p>
</div>
<!-- Error message -->
<div id="errorMessage" class="error-message" style="display: none;">
<i class="fas fa-exclamation-circle"></i>
<p id="errorText">An error occurred while loading the answers.</p>
<button id="retryButton" class="button">Retry</button>
</div>
<!-- Answers content -->
<div id="answersContent" class="answers-content" style="display: none;">
<div class="answers-header">
<h2>Exam Answers</h2>
<div class="answers-actions">
<button id="backToResultsBtn" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i> Back to Results
</button>
</div>
</div>
<div id="examInfoBox" class="exam-info-box">
<p id="examId">Exam ID: Loading...</p>
</div>
<div id="markdownContent" class="markdown-content">
<!-- Markdown content will be rendered here -->
</div>
</div>
</main>
<footer class="app-footer">
<p style="text-align: center;">&copy; <span id="currentYear"></span> CK-X Simulator.</p>
</footer>
</div>
<script src="/js/answers.js"></script>
<script>
// Set the current year for copyright
document.getElementById('currentYear').textContent = new Date().getFullYear();
</script>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="nishan.b" data-description="Support me on Buy me a coffee!" data-message="CK-X helped you prep? A coffee helps it grow !!" data-color="#5F7FFF" data-position="Right" data-x_margin="18" data-y_margin="18"></script>
</body>
</html>

52
app/public/assets/bmc.svg Normal file
View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 600 450" style="enable-background:new 0 0 600 450;" xml:space="preserve">
<style type="text/css">
.st0{fill:#010202;}
.st1{fill:#FFDD06;}
</style>
<path class="st0" d="M390.1,137.3l-0.2-0.1l-0.5-0.2C389.5,137.2,389.8,137.3,390.1,137.3z"/>
<path class="st0" d="M393.4,160.9l-0.3,0.1L393.4,160.9z"/>
<path class="st0" d="M390.1,137.2C390.1,137.2,390.1,137.2,390.1,137.2C390,137.2,390,137.2,390.1,137.2
C390.1,137.2,390.1,137.2,390.1,137.2z"/>
<path class="st0" d="M393.1,160.7l0.4-0.2l0.1-0.1l0.1-0.1C393.5,160.4,393.3,160.6,393.1,160.7z"/>
<path class="st0" d="M390.7,137.8l-0.4-0.4l-0.3-0.1C390.2,137.5,390.4,137.7,390.7,137.8z"/>
<path class="st0" d="M297,366.2c-0.3,0.1-0.6,0.3-0.8,0.6l0.2-0.2C296.7,366.4,296.9,366.3,297,366.2z"/>
<path class="st0" d="M351.5,355.4c0-0.3-0.2-0.3-0.1,0.9c0-0.1,0-0.2,0.1-0.3C351.4,355.9,351.4,355.7,351.5,355.4z"/>
<path class="st0" d="M345.8,366.2c-0.3,0.1-0.6,0.3-0.8,0.6l0.2-0.2C345.5,366.4,345.7,366.3,345.8,366.2z"/>
<path class="st0" d="M258.7,368.7c-0.2-0.2-0.5-0.3-0.8-0.4c0.2,0.1,0.5,0.2,0.6,0.3L258.7,368.7z"/>
<path class="st0" d="M250.2,360.5c0-0.3-0.1-0.7-0.3-1C250,359.8,250.1,360.1,250.2,360.5L250.2,360.5z"/>
<path class="st1" d="M308,212.8c-11.8,5.1-25.3,10.8-42.7,10.8c-7.3,0-14.5-1-21.5-3l12,123.6c0.4,5.2,2.8,10,6.6,13.5
c3.8,3.5,8.8,5.5,14,5.5c0,0,17.1,0.9,22.8,0.9c6.1,0,24.5-0.9,24.5-0.9c5.2,0,10.2-1.9,14-5.5c3.8-3.5,6.2-8.3,6.6-13.5l12.9-136.5
c-5.8-2-11.6-3.3-18.1-3.3C327.7,204.4,318.6,208.3,308,212.8z"/>
<path class="st0" d="M206.5,160.2l0.2,0.2l0.1,0.1C206.8,160.3,206.7,160.2,206.5,160.2z"/>
<path class="st0" d="M412.8,148.7l-1.8-9.1c-1.6-8.2-5.3-16-13.7-18.9c-2.7-0.9-5.8-1.4-7.8-3.3c-2.1-2-2.7-5-3.2-7.8
c-0.9-5.2-1.7-10.4-2.6-15.6c-0.8-4.5-1.4-9.5-3.4-13.5c-2.7-5.5-8.2-8.7-13.7-10.8c-2.8-1-5.7-1.9-8.6-2.7
c-13.7-3.6-28.1-4.9-42.2-5.7c-16.9-0.9-33.9-0.7-50.8,0.8c-12.6,1.1-25.8,2.5-37.7,6.9c-4.4,1.6-8.9,3.5-12.2,6.9
c-4.1,4.1-5.4,10.6-2.4,15.7c2.1,3.7,5.7,6.3,9.5,8c4.9,2.2,10.1,3.9,15.4,5c14.8,3.3,30,4.5,45.1,5.1c16.7,0.7,33.4,0.1,50.1-1.6
c4.1-0.5,8.2-1,12.3-1.6c4.8-0.7,7.9-7.1,6.5-11.4c-1.7-5.3-6.3-7.3-11.4-6.5c-0.8,0.1-1.5,0.2-2.3,0.3l-0.5,0.1
c-1.8,0.2-3.5,0.4-5.3,0.6c-3.6,0.4-7.2,0.7-10.9,1c-8.1,0.6-16.3,0.8-24.5,0.8c-8,0-16-0.2-24-0.8c-3.7-0.2-7.3-0.5-10.9-0.9
c-1.7-0.2-3.3-0.4-4.9-0.6l-1.6-0.2l-0.3,0l-1.6-0.2c-3.3-0.5-6.6-1.1-9.9-1.8c-0.3-0.1-0.6-0.3-0.8-0.5s-0.3-0.6-0.3-0.9
c0-0.3,0.1-0.7,0.3-0.9s0.5-0.4,0.8-0.5h0.1c2.8-0.6,5.7-1.1,8.6-1.6c1-0.2,1.9-0.3,2.9-0.4h0c1.8-0.1,3.6-0.4,5.4-0.7
c15.6-1.6,31.3-2.2,47-1.7c7.6,0.2,15.2,0.7,22.8,1.4c1.6,0.2,3.3,0.3,4.9,0.5c0.6,0.1,1.2,0.2,1.9,0.2l1.3,0.2
c3.7,0.5,7.3,1.2,11,2c5.4,1.2,12.3,1.6,14.7,7.4c0.8,1.9,1.1,3.9,1.5,5.9l0.5,2.5c0,0,0,0.1,0,0.1c1.3,5.9,2.5,11.8,3.8,17.7
c0.1,0.4,0.1,0.9,0,1.3c-0.1,0.4-0.3,0.9-0.5,1.2c-0.3,0.4-0.6,0.7-1,0.9c-0.4,0.2-0.8,0.4-1.2,0.4h0l-0.8,0.1l-0.8,0.1
c-2.4,0.3-4.9,0.6-7.3,0.9c-4.8,0.5-9.6,1-14.4,1.4c-9.6,0.8-19.1,1.3-28.7,1.6c-4.9,0.1-9.8,0.2-14.7,0.2
c-19.5,0-38.9-1.1-58.2-3.4c-2.1-0.2-4.2-0.5-6.3-0.8c1.6,0.2-1.2-0.2-1.7-0.2c-1.3-0.2-2.7-0.4-4-0.6c-4.5-0.7-8.9-1.5-13.4-2.2
c-5.4-0.9-10.5-0.4-15.4,2.2c-4,2.2-7.2,5.5-9.3,9.6c-2.1,4.3-2.7,9.1-3.7,13.7c-0.9,4.7-2.4,9.7-1.8,14.5
c1.2,10.3,8.4,18.7,18.8,20.6c9.8,1.8,19.6,3.2,29.5,4.4c38.7,4.7,77.9,5.3,116.7,1.7c3.2-0.3,6.3-0.6,9.5-1c1-0.1,2,0,2.9,0.3
c0.9,0.3,1.8,0.9,2.5,1.6c0.7,0.7,1.2,1.5,1.6,2.5c0.3,0.9,0.5,1.9,0.4,2.9l-1,9.6c-2,19.3-4,38.6-5.9,58
c-2.1,20.3-4.1,40.6-6.2,60.9c-0.6,5.7-1.2,11.4-1.8,17.1c-0.6,5.6-0.6,11.4-1.7,17c-1.7,8.7-7.6,14.1-16.2,16.1
c-7.9,1.8-16,2.7-24.1,2.8c-9,0-18-0.3-27-0.3c-9.6,0.1-21.4-0.8-28.8-8c-6.5-6.3-7.4-16.1-8.3-24.6c-1.2-11.2-2.4-22.5-3.5-33.7
l-6.5-62.5l-4.2-40.5c-0.1-0.7-0.1-1.3-0.2-2c-0.5-4.8-3.9-9.5-9.3-9.3c-4.6,0.2-9.8,4.1-9.3,9.3l3.1,30l6.5,62
c1.8,17.6,3.7,35.2,5.5,52.9c0.4,3.4,0.7,6.8,1.1,10.1c2,18.5,16.1,28.4,33.6,31.2c10.2,1.6,20.6,2,31,2.1
c13.3,0.2,26.7,0.7,39.7-1.7c19.3-3.5,33.8-16.4,35.9-36.5c0.6-5.8,1.2-11.6,1.8-17.3c2-19.1,3.9-38.2,5.9-57.4l6.4-62.5l2.9-28.6
c0.1-1.4,0.7-2.8,1.7-3.8s2.2-1.8,3.6-2c5.5-1.1,10.8-2.9,14.7-7.1C413.8,166.2,415,157.5,412.8,148.7z M205,154.9
c0.1,0-0.1,0.7-0.1,1C204.8,155.4,204.8,154.9,205,154.9z M205.5,159c0,0,0.2,0.1,0.3,0.4C205.6,159.2,205.5,159,205.5,159
L205.5,159z M206,159.7C206.2,160.1,206.3,160.3,206,159.7L206,159.7z M207.1,160.6L207.1,160.6
C207.1,160.6,207.2,160.6,207.1,160.6C207.1,160.6,207.1,160.6,207.1,160.6L207.1,160.6z M392.5,159.3c-2,1.9-5,2.8-7.9,3.2
c-33.1,4.9-66.8,7.4-100.3,6.3c-24-0.8-47.7-3.5-71.5-6.8c-2.3-0.3-4.8-0.8-6.4-2.5c-3-3.2-1.5-9.7-0.7-13.7
c0.7-3.6,2.1-8.4,6.4-8.9c6.6-0.8,14.4,2,20.9,3c7.9,1.2,15.9,2.2,23.8,2.9c34,3.1,68.7,2.6,102.5-1.9c6.2-0.8,12.3-1.8,18.5-2.9
c5.5-1,11.5-2.8,14.8,2.8c2.3,3.9,2.6,9,2.2,13.4C394.8,156.2,393.9,158,392.5,159.3L392.5,159.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

107
app/public/css/answers.css Normal file
View File

@@ -0,0 +1,107 @@
/* Answers Page Styles */
.answers-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
min-height: calc(100vh - 160px);
}
.answers-content {
width: 100%;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.answers-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.answers-header h2 {
margin: 0;
color: #333;
font-size: 1.8rem;
}
.exam-info-box {
background-color: #f0f8ff;
border-left: 4px solid #3498db;
padding: 12px 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.exam-info-box p {
margin: 0;
color: #333;
font-size: 14px;
}
.markdown-content {
background-color: #f9f9f9;
border-radius: 6px;
padding: 20px;
line-height: 1.6;
margin-bottom: 30px;
}
/* Enhanced code block styling for better readability */
.markdown-content pre {
padding: 16px;
background-color: #f0f0f0;
border-radius: 6px;
overflow-x: auto;
margin: 20px 0;
}
.markdown-content pre code {
font-family: 'Courier New', monospace;
font-size: 14px;
display: block;
line-height: 1.5;
}
/* Make tables more readable */
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
overflow-x: auto;
display: block;
}
.markdown-content thead {
background-color: #e9ecef;
}
.markdown-content th,
.markdown-content td {
padding: 10px 12px;
border: 1px solid #ddd;
text-align: left;
}
.markdown-content tr:nth-child(even) {
background-color: #f7f7f7;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.answers-header {
flex-direction: column;
align-items: flex-start;
}
.answers-actions {
margin-top: 15px;
}
}

484
app/public/css/exam.css Normal file
View File

@@ -0,0 +1,484 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #333;
height: 100vh;
max-height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
color: #333;
overflow: hidden; /* Prevent body scrolling */
}
.header {
background-color: #333;
color: white;
padding: 0.5rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #444;
flex-shrink: 0; /* Prevent header from shrinking */
}
.header-title-section {
display: flex;
align-items: center;
gap: 15px; /* Space between title and timer */
}
.header-title {
font-size: 1.25rem;
font-weight: bold;
margin: 0;
}
.header-controls {
display: flex;
align-items: center;
}
.timer {
padding: 0.25rem 0.5rem;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 0.25rem;
color: #fff;
font-weight: 500;
transition: background-color 0.3s, color 0.3s;
}
.btn-custom {
background-color: #444;
color: white;
border: none;
padding: 0.35rem 0.75rem;
margin-left: 0.5rem;
border-radius: 0.25rem;
}
.main-container {
display: flex;
flex: 1;
height: calc(100vh - 49px); /* Subtract header height */
max-height: calc(100vh - 49px);
position: relative;
overflow: hidden; /* Prevent scrollbars during resize */
}
.question-panel {
width: 30%;
min-width: 200px;
max-width: 70%; /* Prevent panel from taking too much space */
background-color: white;
overflow-y: auto;
padding: 0;
display: flex;
flex-direction: column;
transition: width 0.05s ease; /* Smoother transitions when not dragging */
will-change: width; /* Optimize for animations */
}
.vnc-panel {
flex: 1;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
min-width: 300px; /* Ensure VNC panel has enough space */
overflow: hidden; /* Prevent overflow issues */
max-height: 100%;
}
.question-nav {
display: flex;
padding: 0.5rem;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.nav-arrow {
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
}
.nav-arrow-disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: #e9ecef;
}
.question-select {
flex: 1;
margin: 0 0.5rem;
}
.question-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
transition: opacity 0.2s ease; /* Add smooth transition */
max-height: calc(100vh - 95px); /* Header + Question Nav */
}
.info-box {
background-color: #e3f2fd;
border-radius: 0.25rem;
padding: 1rem;
margin: 1rem 0;
display: flex;
align-items: flex-start;
}
.info-icon {
color: #0d6efd;
font-size: 1.25rem;
margin-right: 0.75rem;
}
.highlight {
color: #d63384;
font-weight: 500;
}
.code {
font-family: monospace;
background-color: #f8f9fa;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
}
.terminal-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
max-height: 100%;
overflow: hidden;
}
.terminal-iframe {
flex: 1;
border: none;
width: 100%;
height: 100%;
background-color: #000;
max-height: 100%;
}
.terminal-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
display: flex;
gap: 5px;
opacity: 0.5;
transition: opacity 0.3s ease;
}
.terminal-controls:hover {
opacity: 1;
}
.terminal-controls .btn {
backdrop-filter: blur(4px);
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
color: white;
}
.loader {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
justify-content: center;
align-items: center;
}
.spinner-border {
width: 3rem;
height: 3rem;
}
ul.requirements {
padding-left: 1.5rem;
}
ul.requirements li {
margin-bottom: 0.5rem;
}
.connection-status {
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
font-size: 12px;
z-index: 100;
opacity: 0;
transition: opacity 0.5s;
}
.connection-status.show {
opacity: 1;
}
.panel-divider {
width: 8px;
height: 100%;
background-color: #ddd;
cursor: col-resize;
position: relative;
transition: background-color 0.2s ease;
z-index: 10;
flex-shrink: 0;
}
.panel-divider:hover, .panel-divider.dragging {
background-color: #1e88e5;
}
.panel-divider::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 4px;
height: 30px;
background-color: #999;
border-radius: 2px;
}
.panel-divider:hover::after, .panel-divider.dragging::after {
background-color: #fff;
}
/* Divider hint and tooltip */
.divider-hint {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none; /* Allow events to pass through */
z-index: 11;
width: 100px;
}
.panel-divider:hover .divider-hint {
opacity: 1;
}
.divider-arrows {
font-size: 16px;
color: white;
margin-bottom: 5px;
}
.divider-tooltip {
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
position: absolute;
top: 25px;
}
/* Prevent text selection during dragging */
.no-select {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* Add a class for content transitions */
.content-fade {
opacity: 0.6;
}
/* Question transition indicators */
.question-transition {
position: relative;
}
.question-transition::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background-color: #1e88e5;
animation: pulseIndicator 1s ease-in-out;
}
@keyframes pulseIndicator {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
/* Inline code highlighting for text in single quotes */
.inline-code {
background-color: rgba(0, 0, 0, 0.06);
border-radius: 3px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85em;
padding: 2px 4px;
color: #d63384;
cursor: pointer;
transition: background-color 0.2s;
}
.inline-code:hover {
background-color: rgba(0, 0, 0, 0.12);
}
/* Improved spacing for question content */
.question-body {
padding: 10px 0;
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.question-body p {
margin-bottom: 16px;
line-height: 1.6;
}
.question-body ul,
.question-body ol {
margin-bottom: 16px;
line-height: 1.5;
padding-left: 25px;
}
.question-body li {
margin-bottom: 8px;
}
.question-body .info-box {
background-color: #f5f9ff;
border-left: 4px solid #3498db;
padding: 12px 15px;
margin: 20px 0;
border-radius: 4px;
}
.question-body .requirements {
margin: 16px 0;
}
.ssh-terminal-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
background-color: #1E1E1E;
padding: 5px;
border-radius: 6px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
max-height: 100%;
width: 100%; /* Ensure full width */
max-width: 100%; /* Prevent overflow */
box-sizing: border-box; /* Include padding in width calculation */
overflow: hidden;
}
/* Remove Terminal window bar */
.ssh-terminal-container::before {
display: none;
}
/* Remove Window title */
.ssh-terminal-container::after {
display: none;
}
/* Remove Terminal window controls (macOS style) */
.ssh-terminal-container .terminal-controls::before {
display: none;
}
.terminal {
flex: 1;
width: 100%;
height: 100%;
background-color: #1E1E1E;
color: #F8F8F8;
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', Menlo, Monaco, 'Courier New', monospace;
padding: 10px;
overflow: auto;
border-radius: 6px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
max-height: 100%;
max-width: 100%; /* Prevent overflow */
box-sizing: border-box; /* Include padding in width calculation */
}
/* Hide scrollbar but keep functionality */
.terminal::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.terminal {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Add to existing terminal-container styles */
.terminal-container, .ssh-terminal-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
max-height: 100%;
overflow: hidden;
}
/* Make question action buttons stick to bottom */
.action-buttons-container {
position: sticky;
bottom: 0;
background-color: white;
padding: 10px 0;
border-top: 1px solid #ddd;
z-index: 10;
}
.timer-warning {
background-color: rgba(220, 53, 69, 0.25);
color: #ffc107;
animation: pulseTimer 1.5s infinite alternate;
}
@keyframes pulseTimer {
from { background-color: rgba(220, 53, 69, 0.25); }
to { background-color: rgba(220, 53, 69, 0.5); }
}
/* Toast container customization */
.toast-container {
z-index: 9999;
}
/* Toast customization */
.toast {
min-width: 250px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: none;
opacity: 1 !important; /* Make sure toasts are fully visible */
}
.toast-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.toast.bg-warning {
border-left: 4px solid #ffc107;
}
.toast.bg-danger {
border-left: 4px solid #dc3545;
}
.review-mode-badge {
background-color: #6c757d;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.85rem;
font-weight: bold;
margin-left: 10px;
display: inline-block;
vertical-align: middle;
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}

101
app/public/css/feedback.css Normal file
View File

@@ -0,0 +1,101 @@
.feedback-prompt {
background-color: #f8f9fa;
border-left: 4px solid #5F7FFF;
border-radius: 4px;
padding: 12px 15px;
margin: 20px 0;
font-size: 15px;
text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.feedback-prompt:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
.feedback-prompt a {
color: #5F7FFF;
font-weight: 500;
text-decoration: none;
}
.feedback-prompt a:hover {
text-decoration: underline;
}
/* Toast notification styles */
.toast-notification {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
max-width: 300px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
overflow: hidden;
animation: slideIn 0.5s ease forwards;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.toast-content {
padding: 15px;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.toast-icon {
color: #5F7FFF;
font-size: 24px;
margin-right: 12px;
}
.toast-message {
flex: 1;
min-width: 150px;
}
.toast-message p {
margin: 0;
font-size: 14px;
line-height: 1.4;
}
.toast-button {
display: inline-block;
margin-top: 10px;
padding: 5px 12px;
background: #5F7FFF;
color: white;
border-radius: 4px;
font-size: 14px;
text-decoration: none;
transition: background 0.3s;
}
.toast-button:hover {
background: #4967e6;
}
.toast-close {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #666;
}

582
app/public/css/index.css Normal file
View File

@@ -0,0 +1,582 @@
:root {
--primary-color: #4f46e5;
--primary-dark: #3730a3;
--primary-light: #818cf8;
--dark-color: #111827;
--light-color: #f9fafb;
--gray-color: #6b7280;
--success-color: #10b981;
--border-radius: 12px;
--box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--font-sans: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
font-family: var(--font-sans);
margin: 0;
padding: 0;
background-color: var(--light-color);
color: var(--dark-color);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* Navbar */
.navbar {
padding: 18px 0;
background-color: rgba(17, 24, 39, 0.95) !important;
backdrop-filter: blur(10px);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: background-color 0.3s ease;
}
.navbar.scrolled {
background-color: rgba(17, 24, 39, 0.98) !important;
}
.navbar-brand {
font-weight: 700;
font-size: 1.75rem;
color: white !important;
letter-spacing: -0.025em;
}
.nav-link {
font-weight: 500;
color: rgba(255, 255, 255, 0.9) !important;
margin: 0 5px;
padding: 8px 16px !important;
border-radius: 8px;
transition: var(--transition);
}
.nav-link:hover {
color: white !important;
background-color: rgba(255, 255, 255, 0.1);
transform: translateY(-1px);
}
/* Hero Section */
.hero-section {
background-color: var(--primary-color);
background-image: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
color: white;
padding: 8rem 0;
position: relative;
overflow: hidden;
padding-top: 76px; /* Height of navbar plus some spacing */
}
.hero-section::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='white' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E");
opacity: 0.6;
}
.hero-content {
position: relative;
z-index: 1;
animation: fadeInUp 1s ease-out;
}
.hero-main-content {
margin-bottom: 80px; /* Add space at the bottom to separate from scroll indicator */
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.display-4 {
font-weight: 800 !important;
margin-bottom: 2.5rem !important; /* Increased spacing */
font-size: 3.75rem;
letter-spacing: -0.025em;
line-height: 1.1;
background: linear-gradient(to right, #ffffff, #e0e7ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.lead {
font-size: 1.25rem;
margin-bottom: 3.5rem !important; /* Increased spacing */
opacity: 0.95;
max-width: 700px;
margin-left: auto;
margin-right: auto;
line-height: 1.6;
}
/* Features Section */
#features {
position: relative;
z-index: 2;
background: white;
padding-top: 6rem;
padding-bottom: 6rem;
}
.features-wrapper {
background-color: white;
border-radius: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
padding: 4rem;
max-width: 1100px;
margin: 0 auto;
}
.features-header {
text-align: center;
margin-bottom: 3rem;
}
.section-title {
font-weight: 800;
color: var(--dark-color);
font-size: 2.5rem;
letter-spacing: -0.025em;
position: relative;
display: inline-block;
margin-bottom: 1.5rem;
}
.section-title::after {
content: '';
position: absolute;
bottom: -0.75rem;
left: 50%;
transform: translateX(-50%);
height: 4px;
width: 60px;
background-color: var(--primary-color);
border-radius: 2px;
}
.section-subtitle {
color: var(--gray-color);
font-size: 1.1rem;
max-width: 600px;
margin: 0 auto;
}
/* Feature cards */
#features .row {
margin-top: 2rem;
}
#features .col-lg-4,
#features .col-md-6 {
padding: 1rem;
display: flex;
}
.feature-card {
border: none;
background-color: white;
border-radius: 16px;
padding: 1.5rem;
transition: var(--transition);
height: 100%;
display: flex;
flex-direction: column;
}
.feature-icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
margin-bottom: 1.25rem;
background-color: #eef4ff;
border-radius: 12px;
transition: var(--transition);
}
.feature-card:hover .feature-icon-container {
transform: scale(1.05);
}
.feature-card h4 {
font-weight: 700;
margin-bottom: 0.75rem;
font-size: 1.25rem;
color: var(--dark-color);
letter-spacing: -0.01em;
}
.feature-card p {
color: var(--gray-color);
line-height: 1.6;
font-size: 0.95rem;
margin-bottom: 0;
}
/* Buttons */
.btn {
padding: 0.8rem 2.5rem;
font-weight: 600;
border-radius: 50px;
transition: var(--transition);
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
}
.btn::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.1);
opacity: 0;
transition: var(--transition);
}
.btn:hover::after {
opacity: 1;
}
.btn-light {
background-color: white;
color: var(--primary-color);
border: none;
}
.btn-light:hover {
background-color: white;
transform: translateY(-3px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Buy Me Coffee Button */
.bmc-navbar {
display: inline-flex !important;
align-items: center;
padding: 8px 15px !important;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 50px !important;
margin-left: 10px;
transition: var(--transition);
backdrop-filter: blur(4px);
}
.bmc-navbar:hover {
background-color: rgba(255, 255, 255, 0.2) !important;
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.bmc-navbar svg {
margin-right: 8px;
}
/* Loader */
.loader {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.75);
z-index: 9999;
justify-content: center;
align-items: center;
backdrop-filter: blur(8px);
}
.spinner-border {
width: 3rem;
height: 3rem;
border-width: 0.25em;
}
.loader-message {
color: white;
font-size: 1.2rem;
margin-top: 1rem;
font-weight: 500;
text-align: center;
}
/* Media Queries */
@media (max-width: 992px) {
.features-wrapper {
padding: 3rem 2rem;
}
}
@media (max-width: 768px) {
.features-wrapper {
padding: 2rem 1.5rem;
}
.display-4 {
font-size: 2.75rem;
}
.hero-section {
padding: 5rem 0;
}
.btn {
padding: 0.7rem 1.75rem;
}
.section-title {
font-size: 2rem;
}
.feature-card {
padding: 1.25rem;
}
.feature-icon-container {
width: 56px;
height: 56px;
}
}
/* Custom Background Effect */
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* Start Exam Button */
.start-exam-btn {
background-color: white;
color: var(--primary-color) !important;
font-weight: 700;
padding: 0.9rem 3rem;
font-size: 1.1rem;
border-radius: 50px;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: var(--transition);
display: inline-block;
border: none;
}
.start-exam-btn:hover {
background-color: white !important;
color: var(--primary-color) !important;
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}
/* Add these styles for the modal */
.modal-content {
border-radius: 16px;
border: none;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.modal-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
padding: 1.25rem 1.5rem;
}
.modal-header .modal-title {
font-weight: 700;
color: var(--dark-color);
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid rgba(0, 0, 0, 0.05);
padding: 1.25rem 1.5rem;
}
.form-select:focus,
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(79, 70, 229, 0.25);
}
.alert-info {
background-color: #eef4ff;
border-color: #d1e0ff;
color: var(--primary-dark);
}
/* Button styles for modal */
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover,
.btn-primary:focus {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
/* Exam status badge */
.exam-status {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
text-transform: uppercase;
}
.status-evaluating {
background-color: #f0ad4e;
color: white;
}
.status-evaluated {
background-color: #5cb85c;
color: white;
}
/* View Results button visibility */
.view-results-btn-container {
transition: opacity 0.3s ease;
}
/* Add these styles to the existing index.css file */
.loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.loading-content {
background: white;
padding: 2rem;
border-radius: 8px;
text-align: center;
max-width: 400px;
width: 90%;
}
/* Renamed classes to avoid conflict with Bootstrap */
.custom-progress-bar {
width: 100%;
height: 20px;
background: #f0f0f0;
border-radius: 10px;
margin: 1rem 0;
overflow: hidden;
}
.custom-progress {
width: 0%;
height: 100%;
background: #4CAF50;
transition: width 0.3s ease;
}
.loading-message {
margin: 1rem 0;
color: #666;
}
.exam-info {
font-size: 0.9rem;
color: #666;
margin-top: 0.5rem;
}
/* Exam meta styles that were added dynamically in JavaScript */
.exam-meta {
color: #4a5568;
font-size: 0.9rem;
display: flex;
align-items: center;
}
.exam-meta-container {
border-top-color: rgba(0, 0, 0, 0.1);
}
.exam-details {
word-wrap: break-word;
}
/* Full height hero section */
.full-height {
min-height: 100vh;
padding: 0;
margin: 0;
}
/* Scroll indicator - animation removed */
.scroll-indicator {
position: absolute;
bottom: 30px;
left: 0;
right: 0;
text-align: center;
color: rgba(255, 255, 255, 0.7);
/* animation: bounce 2s infinite; - removed */
z-index: 2;
}
.scroll-indicator p {
margin-bottom: 5px;
font-size: 14px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
}
.scroll-indicator i {
font-size: 20px;
}
/* Position the features section after the full-height hero */
#features {
position: relative;
z-index: 2;
background: white;
padding-top: 6rem;
padding-bottom: 6rem;
}

506
app/public/css/results.css Normal file
View File

@@ -0,0 +1,506 @@
/* Results Page Styles */
.results-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
min-height: calc(100vh - 160px);
}
.page-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #3498db;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
/* Animation for spinner */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
text-align: center;
padding: 40px;
background-color: #fff3f3;
border-radius: 8px;
border: 1px solid #ffcccc;
margin: 20px 0;
}
.error-message i {
font-size: 48px;
color: #e74c3c;
margin-bottom: 15px;
}
.error-message p {
margin-bottom: 20px;
color: #333;
font-size: 18px;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e0e0e0;
}
.exam-info {
text-align: right;
color: #666;
}
.results-summary {
display: flex;
justify-content: space-between;
background-color: #f9f9f9;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.score-display {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.score-display h3 {
font-size: 18px;
font-weight: 500;
margin-bottom: 10px;
color: #333;
}
.score-box {
background-color: #2c3e50;
color: white;
font-size: 28px;
font-weight: bold;
padding: 10px 30px;
border-radius: 4px;
min-width: 150px;
text-align: center;
}
.rank-display {
display: flex;
align-items: center;
}
.rank-badge {
font-size: 24px;
font-weight: bold;
padding: 12px 30px;
border-radius: 4px;
text-align: center;
min-width: 180px;
}
.rank-high {
background-color: #27ae60;
color: white;
}
.rank-medium {
background-color: #f39c12;
color: white;
}
.rank-low {
background-color: #e74c3c;
color: white;
}
.questions-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.question-card {
background-color: #f2f4f8;
border-radius: 8px;
overflow: hidden;
}
.question-header {
background-color: #e9ecef;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.question-title {
font-size: 18px;
font-weight: 500;
margin: 0;
}
.question-score {
font-size: 16px;
}
.verification-items {
padding: 0;
margin: 0;
list-style: none;
}
.verification-item {
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
background-color: white;
}
.verification-item:last-child {
border-bottom: none;
}
.verification-description {
flex-grow: 1;
margin-right: 20px;
}
.verification-status {
font-size: 24px;
color: #27ae60;
}
.status-success {
color: #27ae60;
}
.status-failure {
color: #e74c3c;
}
@media (max-width: 768px) {
.results-header {
flex-direction: column;
align-items: flex-start;
}
.exam-info {
text-align: left;
margin-top: 10px;
}
.results-summary {
flex-direction: column;
gap: 20px;
}
.score-box, .rank-badge {
width: 100%;
}
}
/* Blue header styling */
.blue-header {
background-color: #1d82e2;
color: white;
padding: 15px 0;
}
.blue-header .logo {
display: flex;
align-items: center;
}
.blue-header .logo h1 {
color: white;
margin: 0;
}
.blue-header .nav-link {
color: white;
text-decoration: none;
display: inline-flex;
align-items: center;
font-weight: 500;
}
.blue-header .nav-link:hover {
text-decoration: underline;
}
/* Results actions at top */
.results-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0 30px 0;
justify-content: center;
}
.results-actions .btn {
padding: 10px 15px;
border-radius: 6px;
font-weight: 500;
display: flex;
align-items: center;
transition: all 0.2s ease;
}
.results-actions .btn i {
margin-right: 8px;
}
.results-actions .btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Add styles for answers content */
.answers-content {
width: 100%;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.answers-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.answers-header h2 {
margin: 0;
color: #333;
font-size: 1.8rem;
}
.markdown-content {
background-color: #f9f9f9;
border-radius: 6px;
padding: 20px;
line-height: 1.6;
}
/* Markdown styling */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
color: #333;
}
.markdown-content h1 {
font-size: 2em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
.markdown-content h2 {
font-size: 1.5em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
.markdown-content h3 {
font-size: 1.25em;
}
.markdown-content p {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-content code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
}
.markdown-content pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 3px;
margin-top: 0;
margin-bottom: 16px;
}
.markdown-content pre code {
padding: 0;
background-color: transparent;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
.markdown-content blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
margin: 0 0 16px 0;
}
.markdown-content img {
max-width: 100%;
box-sizing: content-box;
background-color: #fff;
}
.markdown-content table {
display: block;
width: 100%;
overflow: auto;
margin-top: 0;
margin-bottom: 16px;
border-collapse: collapse;
}
.markdown-content table th,
.markdown-content table td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
.markdown-content table tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.markdown-content table tr:nth-child(2n) {
background-color: #f6f8fa;
}
/* Modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
border-radius: 8px;
width: 100%;
max-width: 500px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
animation: modalFadeIn 0.3s ease;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.modal-header h4 {
margin: 0;
font-size: 18px;
color: #333;
}
.modal-close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.modal-close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.warning-text {
color: #e74c3c;
font-weight: 500;
}
/* Ensure the danger button is prominently red */
.btn-danger {
background-color: #e74c3c;
color: white;
border: none;
}
.btn-danger:hover {
background-color: #c0392b;
}
/* Animation for modal fade-in */
.fade-in {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

55
app/public/css/styles.css Normal file
View File

@@ -0,0 +1,55 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: #1e88e5;
color: white;
padding: 1rem;
text-align: center;
}
h1 {
margin: 0;
}
.content {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 20px;
}
.card {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.vnc-container {
height: 600px;
border: 1px solid #ddd;
border-radius: 4px;
}
#vnc-frame {
width: 100%;
height: 100%;
border: none;
}
.btn {
background-color: #1e88e5;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #1565c0;
}

273
app/public/exam.html Normal file
View File

@@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CK-X Simulator - Exam</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- xterm.js CSS -->
<link href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/exam.css">
</head>
<body>
<!-- Loader -->
<div class="loader" id="pageLoader">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<!-- Toast Container for notifications -->
<div class="toast-container position-fixed top-0 end-0 pt-5 pr-3" id="toastContainer">
<!-- Toasts will be added here dynamically -->
</div>
<!-- Exam End Modal -->
<div class="modal fade" id="examEndModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="examEndModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="examEndModalLabel">Exam Time Complete</h5>
</div>
<div class="modal-body">
<div class="alert alert-info mb-3">
<strong>Time's up! ⏳ Your exam has ended. </strong>
</div>
<p>We're now evaluating your results.</p>
<p>Your environment session will remain available for review until you choose to end it.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="viewResultsBtn">View Results</button>
</div>
</div>
</div>
</div>
<!-- Start Exam Modal -->
<div class="modal fade" id="startExamModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="startExamModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="startExamModalLabel">Ready to Begin Your Exam</h5>
</div>
<!-- New Exam Content (default) -->
<div class="modal-body" id="newExamContent">
<div class="alert alert-success mb-3">
<strong>Important:</strong> Please treat this as a real exam. Focus on completing tasks accurately and efficiently as you would in a certification exam.
</div>
<p>When you click "Start Exam", the following will happen:</p>
<ul>
<li>Your exam timer will begin counting down.</li>
<li>You will be connected to the Kubernetes environment.</li>
<li>The application will enter fullscreen mode</li>
<li>When you click on the <span class="inline-code">highlighted</span> content, it will be copied to the clipboard.</li> </ul>
<p class="mb-0">Are you ready to start?</p>
</div>
<!-- Exam In Progress Content (initially hidden) -->
<div class="modal-body" id="examInProgressContent" style="display: none;">
<div class="alert alert-warning mb-3">
<strong>Note:</strong> You already have an exam in progress.
</div>
<p>Your previous session will be restored, including your remaining time.</p>
<p>You can continue where you left off.</p>
</div>
<!-- Exam Completed Content (initially hidden) -->
<div class="modal-body" id="examCompletedContent" style="display: none;">
<div class="alert alert-info mb-3">
<strong>Note:</strong> You've already completed this exam session.
</div>
<p>Your session is still available. You can continue to view the environment, but your answers have already been submitted.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="startExamBtn">Start Exam</button>
</div>
</div>
</div>
</div>
<!-- Header -->
<div class="header">
<div class="header-title-section">
<div class="header-title">CK-X Simulator</div>
<div class="timer" id="examTimer">120:00</div>
</div>
<div class="header-controls">
<div class="dropdown d-inline-block">
<button class="btn-custom dropdown-toggle" type="button" id="examInterfaceDropdown" data-bs-toggle="dropdown" aria-expanded="false">
Exam Interface
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="examInterfaceDropdown">
<li><a class="dropdown-item" href="#" id="resizeTerminalBtn">Resize Terminal</a></li>
<li><a class="dropdown-item" href="#" id="fullscreenBtn">Toggle Fullscreen</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" id="toggleViewBtn">Switch to Terminal</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="https://github.com/nishanb/CKAD-X">Help</a></li>
</ul>
</div>
<div class="dropdown d-inline-block">
<button class="btn-custom dropdown-toggle" type="button" id="examControlsDropdown" data-bs-toggle="dropdown" aria-expanded="false">
Exam Controls
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="examControlsDropdown">
<li><a class="dropdown-item" href="#" id="endExamBtnDropdown">End Exam</a></li>
<li><a class="dropdown-item text-danger" href="#" id="terminateSessionBtn">Terminate Session</a></li>
</ul>
</div>
<div class="dropdown d-inline-block">
<button class="btn-custom dropdown-toggle" type="button" id="examInfoDropdown" data-bs-toggle="dropdown" aria-expanded="false">
Exam Information
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="examInfoDropdown">
<li><a class="dropdown-item" href="https://docs.linuxfoundation.org/tc-docs/certification" target="_blank">Exam Instructions</a></li>
<li><a class="dropdown-item" href="https://kubernetes.io/docs/home/" target="_blank">Kubernetes Documentation</a></li>
</ul>
</div>
</div>
</div>
<!-- Main Container -->
<div class="main-container" id="mainContainer">
<!-- Question Panel (Left) -->
<div class="question-panel" id="questionPanel">
<!-- Question Navigation -->
<div class="question-nav">
<button class="nav-arrow" id="prevBtn">&lt;</button>
<div class="dropdown question-select">
<button class="btn btn-light w-100 dropdown-toggle" type="button" id="questionDropdown" data-bs-toggle="dropdown" aria-expanded="false">
Question 1
</button>
<ul class="dropdown-menu w-100" aria-labelledby="questionDropdown" id="questionDropdownMenu">
<li><a class="dropdown-item" href="#" data-question="1">Question 1</a></li>
<li><a class="dropdown-item" href="#" data-question="2">Question 2</a></li>
<li><a class="dropdown-item" href="#" data-question="3">Question 3</a></li>
<li><a class="dropdown-item" href="#" data-question="4">Question 4</a></li>
<li><a class="dropdown-item" href="#" data-question="5">Question 5</a></li>
</ul>
</div>
<button class="nav-arrow" id="nextBtn">&gt;</button>
</div>
<!-- Question Content -->
<div class="question-content" id="questionContent">
<!-- Question will be loaded here dynamically -->
</div>
</div>
<!-- Resizable Divider -->
<div class="panel-divider" id="panelDivider">
<div class="divider-hint">
<span class="divider-arrows"></span>
<span class="divider-tooltip">Drag to resize</span>
</div>
</div>
<!-- VNC Panel (Right) -->
<div class="vnc-panel" id="vncPanel">
<div class="terminal-container">
<div class="terminal-controls">
<button class="btn btn-sm btn-outline-secondary me-2" id="reconnectVncBtn" title="Reconnect to session">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">
<path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
</button>
<button class="btn btn-sm btn-outline-secondary" id="fullscreenVncBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrows-fullscreen" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
</svg>
</button>
</div>
<iframe id="vnc-frame" class="terminal-iframe" src="about:blank"></iframe>
<div id="connectionStatus" class="connection-status">Connecting to Session...</div>
</div>
<!-- SSH Terminal Container (initially hidden) -->
<div class="ssh-terminal-container" id="sshTerminalContainer" style="display: none;">
<div class="terminal-controls">
<button class="btn btn-sm btn-outline-secondary" id="fullscreenTerminalBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrows-fullscreen" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
</svg>
</button>
</div>
<div id="terminal" class="terminal"></div>
<div id="sshConnectionStatus" class="connection-status">Connecting to SSH...</div>
</div>
</div>
</div>
<!-- Confirm Modal -->
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">End Exam Confirmation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to end the exam? This action cannot be undone.</p>
<div class="alert alert-success mt-2">
<p class="mb-1"><strong>What happens next:</strong></p>
<ul class="mb-0">
<li>Your submissions will be evaluated.</li>
<li>You will be redirected to the results page.</li>
<li>Your session will remain intact until you choose to terminate it.</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" id="confirmEndBtn">End Exam</button>
</div>
</div>
</div>
</div>
<!-- Terminate Session Modal -->
<div class="modal fade" id="terminateModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Terminate Session Warning</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<h6 class="alert-heading">Warning!</h6>
<p>Terminating this session will:</p>
<ul>
<li>Immediately clean up all environment resources</li>
<li>End your exam without evaluating your answers</li>
</ul>
<p>This action <strong>cannot</strong> be undone.</p>
</div>
<p>Would you like to proceed with terminating this session?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmTerminateBtn">Terminate Session</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- xterm.js -->
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<!-- Socket.io -->
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
<!-- Custom JS -->
<script type="module" src="/js/exam.js"></script>
<script src="/js/panel-resizer.js"></script>
</body>
</html>

233
app/public/index.html Normal file
View File

@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<title>CK-X | Kubernetes Certification Simulator</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/index.css">
</head>
<body>
<!-- Loader -->
<div class="loader" id="pageLoader">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div class="loader-message" id="loaderMessage">Lab is getting ready...</div>
</div>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="/">CK-X</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item me-3 view-results-btn-container" style="display: none;">
<a class="nav-link" href="#" id="viewPastResultsBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-clipboard-check" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>
View Result
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/nishanb/CKAD-X" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
GitHub
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Hero Section - Full Height -->
<section class="hero-section full-height d-flex align-items-center text-center position-relative">
<div class="container hero-content">
<h1 class="display-4 mb-4">Kubernetes Certification Exam Simulator</h1>
<p class="lead mb-5">Practice in a realistic environment that mirrors the actual CKA, CKAD, and other Kubernetes certification exams</p>
<a href="#" class="btn btn-light btn-lg start-exam-btn" id="startExamBtn">START EXAM</a>
</div>
<div class="scroll-indicator">
<p>SCROLL TO EXPLORE</p>
<i class="fas fa-chevron-down"></i>
</div>
</section>
<!-- Features Section -->
<section id="features">
<div class="container">
<div class="features-wrapper">
<div class="features-header">
<h2 class="section-title">Key Features</h2>
<p class="section-subtitle">Everything you need to prepare for your Kubernetes certification exams</p>
</div>
<div class="row">
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="feature-icon-container">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26 4H6C4.89543 4 4 4.89543 4 6V22C4 23.1046 4.89543 24 6 24H26C27.1046 24 28 23.1046 28 22V6C28 4.89543 27.1046 4 26 4Z" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 28H20" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 24V28" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 16L15 18L19 14" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h4>Real Exam Environment</h4>
<p>Practice in a simulated environment that closely resembles the actual Kubernetes certification exam interface.</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="feature-icon-container">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4V6" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.51471 7.51472L8.92893 8.92894" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 16H6" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.51471 24.4853L8.92893 23.0711" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 26V28" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24.4853 24.4853L23.0711 23.0711" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26 16H28" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24.4853 7.51472L23.0711 8.92894" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 20C18.2091 20 20 18.2091 20 16C20 13.7909 18.2091 12 16 12C13.7909 12 12 13.7909 12 16C12 18.2091 13.7909 20 16 20Z" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h4>Multiple Certification Support</h4>
<p>Practice for CKA, CKAD, and other Kubernetes certification exams with tailored scenarios for each exam type.</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="feature-icon-container">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="12" stroke="#4f46e5" stroke-width="2.5"/>
<path d="M16 10V16L20 20" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</div>
<h4>Timed Practice</h4>
<p>Prepare for the time pressure of the actual exams with our realistic timer functionality.</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="feature-icon-container">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 16H26" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8H26" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 24H26" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="10" cy="8" r="2" fill="#4f46e5"/>
<circle cx="22" cy="16" r="2" fill="#4f46e5"/>
<circle cx="14" cy="24" r="2" fill="#4f46e5"/>
</svg>
</div>
<h4>Detailed Scoring & Solutions</h4>
<p>Review your performance with comprehensive exam scores and access detailed solutions to enhance your learning.</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="feature-icon-container">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 28C22.6274 28 28 22.6274 28 16C28 9.37258 22.6274 4 16 4C9.37258 4 4 9.37258 4 16C4 22.6274 9.37258 28 16 28Z" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 16L15 20L22 13" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h4>Realistic Kubernetes Cluster</h4>
<p>Practice on actual Kubernetes infrastructure that mimics the exam environment with pre-configured namespaces and resources.</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="feature-icon-container">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26 28V25C26 23.9391 25.5786 22.9217 24.8284 22.1716C24.0783 21.4214 23.0609 21 22 21H10C8.93913 21 7.92172 21.4214 7.17157 22.1716C6.42143 22.9217 6 23.9391 6 25V28" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 17C18.7614 17 21 14.7614 21 12C21 9.23858 18.7614 7 16 7C13.2386 7 11 9.23858 11 12C11 14.7614 13.2386 17 16 17Z" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h4>User-Focused Experience</h4>
<p>Enjoy a streamlined and accessible interface designed to help you focus on mastering Kubernetes concepts without distractions.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Add loading overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-content">
<h2>Preparing Your Lab Environment</h2>
<div class="custom-progress-bar">
<div class="custom-progress" id="progressBar"></div>
</div>
<div class="loading-message" id="loadingMessage">Initializing environment...</div>
<div class="exam-info" id="examInfo"></div>
</div>
</div>
<!-- Exam Selection Modal -->
<div class="modal fade" id="examSelectionModal" tabindex="-1" aria-labelledby="examSelectionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="examSelectionModalLabel">Select Your Exam</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="examSelectionForm">
<div class="mb-3">
<label for="examCategory" class="form-label">Certification Type</label>
<select class="form-select" id="examCategory" required>
<option value="">Select certification type</option>
<option value="CKAD" selected>CKAD - Certified Kubernetes Application Developer</option>
<option value="CKA">CKA - Certified Kubernetes Administrator</option>
<option value="CKS">CKS - Certified Kubernetes Security Specialist</option>
</select>
</div>
<div class="mb-3">
<label for="examName" class="form-label">Exam</label>
<select class="form-select" id="examName" required disabled>
<option value="">Select an exam</option>
</select>
</div>
<div class="mb-4">
<div class="alert alert-info" id="examDescription">
Select an exam to see its description.
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">CANCEL</button>
<button type="button" class="btn btn-primary" id="startSelectedExam" disabled>START EXAM</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script src="/js/index.js"></script>
<!-- Buy Me a Coffee Widget -->
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="nishan.b" data-description="Support me on Buy me a coffee!" data-message="" data-color="#5F7FFF" data-position="Right" data-x_margin="18" data-y_margin="18"></script>
<!-- Footer -->
<footer class="text-center py-4 mt-4">
<p>Made with <span style="color: #ff3366;">❤️</span> | <a href="https://ckx.nishann.com" target="_blank">ckx.nishann.com</a></p>
</footer>
</body>
</html>

117
app/public/js/answers.js Normal file
View File

@@ -0,0 +1,117 @@
document.addEventListener('DOMContentLoaded', function() {
// DOM elements
const pageLoader = document.getElementById('pageLoader');
const errorMessage = document.getElementById('errorMessage');
const errorText = document.getElementById('errorText');
const retryButton = document.getElementById('retryButton');
const answersContent = document.getElementById('answersContent');
const examIdElement = document.getElementById('examId');
const markdownContent = document.getElementById('markdownContent');
const backToResultsBtn = document.getElementById('backToResultsBtn');
// Variables
let currentExamId = null;
// Initialize
function init() {
// Get exam ID from URL
currentExamId = getExamIdFromUrl();
if (!currentExamId) {
showError('No exam ID provided. Please return to the results page.');
return;
}
// Update exam ID display
examIdElement.textContent = `Exam ID: ${currentExamId}`;
// Fetch answers
fetchAnswers(currentExamId);
// Setup event listeners
backToResultsBtn.addEventListener('click', goBackToResults);
retryButton.addEventListener('click', () => fetchAnswers(currentExamId));
}
// Get exam ID from URL
function getExamIdFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('id');
}
// Go back to results page
function goBackToResults() {
if (currentExamId) {
window.location.href = `/results.html?id=${currentExamId}`;
} else {
window.location.href = '/';
}
}
// Show error message
function showError(message) {
pageLoader.style.display = 'none';
errorText.textContent = message;
errorMessage.style.display = 'block';
answersContent.style.display = 'none';
}
// Fetch answers from API
function fetchAnswers(examId) {
// Show loader
pageLoader.style.display = 'flex';
errorMessage.style.display = 'none';
answersContent.style.display = 'none';
// Fetch answers file
fetch(`/facilitator/api/v1/exams/${examId}/answers`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.text(); // Get raw text (Markdown content)
})
.then(markdownText => {
// Render markdown
renderMarkdown(markdownText);
// Hide loader and show content
pageLoader.style.display = 'none';
answersContent.style.display = 'block';
})
.catch(error => {
console.error('Error fetching answers:', error);
showError(`Failed to load answers: ${error.message}`);
});
}
// Render markdown content
function renderMarkdown(markdownText) {
// Configure marked options
marked.setOptions({
highlight: function(code, lang) {
if (hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
} else {
return hljs.highlightAuto(code).value;
}
},
breaks: true,
gfm: true
});
// Convert markdown to HTML
const htmlContent = marked.parse(markdownText);
// Set content
markdownContent.innerHTML = htmlContent;
// Apply syntax highlighting to code blocks
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
// Start the application
init();
});

20
app/public/js/app.js Normal file
View File

@@ -0,0 +1,20 @@
document.addEventListener('DOMContentLoaded', function() {
const vncFrame = document.getElementById('vnc-frame');
const connectBtn = document.getElementById('connect-btn');
const fullscreenBtn = document.getElementById('fullscreen-btn');
connectBtn.addEventListener('click', function() {
// Connect to the VNC server through the service
vncFrame.src = `http://${window.location.hostname}:${window.location.port}/vnc-proxy/`;
});
fullscreenBtn.addEventListener('click', function() {
if (vncFrame.requestFullscreen) {
vncFrame.requestFullscreen();
} else if (vncFrame.webkitRequestFullscreen) {
vncFrame.webkitRequestFullscreen();
} else if (vncFrame.msRequestFullscreen) {
vncFrame.msRequestFullscreen();
}
});
});

View File

@@ -0,0 +1,50 @@
/**
* Clipboard Service
* Handles clipboard-related functionality
*/
/**
* Copy text to remote desktop clipboard via facilitator API
* @param {string} content - Text content to copy
* @private
*/
async function copyToRemoteClipboard(content) {
try {
// Fire and forget API call
fetch('/facilitator/api/v1/remote-desktop/clipboard', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ content })
});
} catch (error) {
console.error('Failed to copy to remote clipboard:', error);
// Don't throw error as this is a non-critical operation
}
}
/**
* Setup click-to-copy functionality for inline code elements
* Uses event delegation to handle all inline-code elements
*/
function setupInlineCodeCopy() {
document.addEventListener('click', function(event) {
if (event.target && event.target.matches('.inline-code')) {
const codeText = event.target.textContent;
// Copy to remote desktop clipboard
copyToRemoteClipboard(codeText);
// Copy to local clipboard
navigator.clipboard.writeText(codeText).catch(err => {
console.error('Could not copy text to clipboard:', err);
});
}
});
}
export {
setupInlineCodeCopy
};

View File

@@ -0,0 +1,144 @@
/**
* Exam API Service
* Handles all API interactions for the exam functionality
*/
// Function to get exam ID from URL
function getExamId() {
// Extract exam ID from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const examId = urlParams.get('id');
if (!examId) {
console.error('No exam ID found in URL');
alert('Error: No exam ID provided. Please return to the dashboard.');
// redirect to dashboard
window.location.href = '/';
}
return examId;
}
// Function to check exam status
function checkExamStatus(examId) {
return fetch(`/facilitator/api/v1/exams/${examId}/status`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
return data.status || null;
});
}
// Function to fetch exam data
function fetchExamData(examId) {
const apiUrl = `/facilitator/api/v1/exams/${examId}/questions`;
return fetch(apiUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
return data;
})
.catch(error => {
console.error('Error loading exam questions:', error);
throw error; // Re-throw to be handled by the calling function
});
}
// Function to fetch current exam information
function fetchCurrentExamInfo() {
return fetch('/facilitator/api/v1/exams/current')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
return data;
});
}
// Function to evaluate exam
function evaluateExam(examId) {
return fetch(`/facilitator/api/v1/exams/${examId}/evaluate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
});
}
// Function to terminate session
function terminateSession(examId) {
return fetch(`/facilitator/api/v1/exams/${examId}/terminate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
});
}
// Function to get VNC info
function getVncInfo() {
return fetch('/api/vnc-info')
.then(response => response.json())
.catch(error => {
console.error('Error fetching VNC info:', error);
throw error;
});
}
// Function to track exam events
function trackExamEvent(examId, events) {
return fetch(`/facilitator/api/v1/exams/${examId}/events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ events })
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('Error tracking exam event:', error);
// Don't throw error to avoid disrupting exam flow
// But still log it for debugging
});
}
// Export the API functions
export {
getExamId,
checkExamStatus,
fetchExamData,
fetchCurrentExamInfo,
evaluateExam,
terminateSession,
getVncInfo,
trackExamEvent
};

View File

@@ -0,0 +1,178 @@
/**
* Question Service
* Handles question display and navigation
*/
// Process question content to improve formatting and highlighting
function processQuestionContent(content) {
// First, preserve existing HTML formatting
let processedContent = content;
// Add highlighting to text in single quotes that isn't already styled
processedContent = processedContent.replace(
/`([^`]+)`/g,
function(match, text) {
// Skip if already inside an HTML tag or existing styled element
if (match.match(/<.*>/) ||
match.includes('class="code"') ||
match.includes('class="highlight"')) {
return match;
}
return `<span class="inline-code">${text}</span>`;
}
);
// Style inline code with backticks if not already styled
processedContent = processedContent.replace(
/`([^`]+)`/g,
'<code class="bg-light px-1 rounded">$1</code>'
);
// Style bold text
processedContent = processedContent.replace(
/\*\*([^*]+)\*\*/g,
'<strong>$1</strong>'
);
// Style italic text
processedContent = processedContent.replace(
/\*([^*]+)\*/g,
'<em>$1</em>'
);
// Convert literal newline characters to HTML line breaks
processedContent = processedContent.replace(/\n/g, '<br>');
// Ensure paragraphs have proper spacing and line breaks
processedContent = processedContent.replace(
/<\/p><p>/g,
'</p>\n<p>'
);
// Add more spacing between list items
processedContent = processedContent.replace(
/<\/li><li>/g,
'</li>\n<li>'
);
return processedContent;
}
// Generate question content HTML
function generateQuestionContent(question) {
try {
// Get original data
const originalData = question.originalData || {};
const machineHostname = originalData.machineHostname || 'N/A';
const namespace = originalData.namespace || 'N/A';
const concepts = originalData.concepts || [];
const conceptsString = concepts.join(', ');
// Format question content with improved styling
const formattedQuestionContent = processQuestionContent(question.content);
// Create formatted content with minimal layout
return `
<div class="d-flex flex-column" style="height: 100%;">
<div class="question-header">
<div class="mb-3">
<strong>Solve this question on instance:</strong> <span class="inline-code">ssh ${machineHostname}</span></span>
</div>
<div class="mb-3">
<strong>Namespace:</strong> <span class="text-primary">${namespace}</span>
</div>
<div class="mb-3">
<strong>Concepts:</strong> <span class="text-primary">${conceptsString}</span>
</div>
<hr class="my-3">
</div>
<div class="question-body">
${formattedQuestionContent}
</div>
<div class="action-buttons-container mt-auto">
<div class="d-flex justify-content-between py-2">
<button class="btn ${question.flagged ? 'btn-warning' : 'btn-outline-warning'}" id="flagQuestionBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-flag${question.flagged ? '-fill' : ''} me-2" viewBox="0 0 16 16">
<path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001"/>
</svg>
${question.flagged ? 'Flagged' : 'Flag for review'}
</button>
<button class="btn btn-success" id="nextQuestionBtn">
Satisfied with answer
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right ms-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg>
</button>
</div>
</div>
</div>
`;
} catch (error) {
console.error('Error generating question content:', error);
return '<div class="alert alert-danger">Error displaying question content. Please try refreshing the page.</div>';
}
}
// Transform API response to question objects
function transformQuestionsFromApi(data) {
if (data.questions && Array.isArray(data.questions)) {
// Transform the questions to match our expected format
return data.questions.map(q => ({
id: q.id,
content: q.question || '', // Map 'question' field to 'content'
title: `Question ${q.id}`, // Create a title from the ID
originalData: q, // Keep original data for reference if needed
flagged: false // Add flagged status property
}));
}
return [];
}
// Update question dropdown
function updateQuestionDropdown(questionsArray, dropdownMenu, currentId, onQuestionSelect) {
// Clear existing dropdown items
dropdownMenu.innerHTML = '';
// Add items for each question
questionsArray.forEach((question) => {
const li = document.createElement('li');
const a = document.createElement('a');
a.className = 'dropdown-item';
a.href = '#';
a.dataset.question = question.id;
a.textContent = `Question ${question.id}`;
// Add flag icon if question is flagged
if (question.flagged) {
const flagIcon = document.createElement('span');
flagIcon.className = 'flag-icon ms-2';
flagIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-flag-fill text-warning" viewBox="0 0 16 16"><path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001"/></svg>';
a.appendChild(flagIcon);
}
// Add click event
a.addEventListener('click', function(e) {
e.preventDefault();
const clickedQuestionId = this.dataset.question;
if (onQuestionSelect) {
onQuestionSelect(clickedQuestionId);
}
});
li.appendChild(a);
dropdownMenu.appendChild(li);
});
}
export {
processQuestionContent,
generateQuestionContent,
transformQuestionsFromApi,
updateQuestionDropdown
};

View File

@@ -0,0 +1,66 @@
/**
* Remote Desktop Service
* Handles remote desktop connection and management
*/
import { getVncInfo } from './exam-api.js';
// Connect to VNC
function connectToRemoteDesktop(vncFrame, statusCallback) {
if (statusCallback) {
statusCallback('Connecting to Remote Desktop...', 'info');
}
// Get VNC server info from API
return getVncInfo()
.then(data => {
console.log('Remote Desktop info:', data);
// Create the VNC URL with proper parameters
const vncUrl = `/vnc-proxy/?autoconnect=true&resize=scale&show_dot=true&reconnect=true&password=${data.defaultPassword}`;
// Set the iframe source to the VNC URL
vncFrame.src = vncUrl;
if (statusCallback) {
statusCallback('Connected to Session', 'success');
}
return vncUrl;
})
.catch(error => {
console.error('Error connecting to Remote Desktop:', error);
if (statusCallback) {
statusCallback('Failed to connect to Remote Desktop. Retrying...', 'error');
}
// Return a promise that will retry
return new Promise(resolve => {
setTimeout(() => {
resolve(connectToRemoteDesktop(vncFrame, statusCallback));
}, 5000);
});
});
}
// Setup Remote Desktop frame event handlers
function setupRemoteDesktopFrameHandlers(vncFrame, statusCallback) {
vncFrame.addEventListener('load', function() {
if (vncFrame.src !== 'about:blank') {
console.log('Remote Desktop frame loaded successfully');
if (statusCallback) {
statusCallback('Connected to Session', 'success');
}
}
});
vncFrame.addEventListener('error', function(e) {
console.error('Error loading Remote Desktop frame:', e);
if (statusCallback) {
statusCallback('Error connecting to Remote Desktop. Retrying...', 'error');
}
// Try to reconnect after a delay
setTimeout(() => connectToRemoteDesktop(vncFrame, statusCallback), 5000);
});
}
export {
connectToRemoteDesktop,
setupRemoteDesktopFrameHandlers
};

View File

@@ -0,0 +1,261 @@
/**
* Terminal Service
* Handles terminal initialization and management
*/
let terminal;
let socket;
let fitAddon;
let isTerminalInitialized = false;
let terminalCallbacks = {};
// Set up terminal callbacks
function setCallbacks(callbacks) {
terminalCallbacks = callbacks || {};
}
// Function to initialize the terminal
function initTerminal(containerElement, isActive = false) {
// If already initialized, just resize
if (isTerminalInitialized) {
console.log('Terminal already initialized, just resizing');
resizeTerminal(containerElement);
return { terminal, fitAddon };
}
console.log('Initializing terminal for the first time');
// Create terminal container if it doesn't exist
let terminalContainer = document.getElementById('terminal');
if (!terminalContainer) {
terminalContainer = document.createElement('div');
terminalContainer.id = 'terminal';
terminalContainer.className = 'terminal';
containerElement.appendChild(terminalContainer);
}
// Set initial container size
terminalContainer.style.width = '100%';
terminalContainer.style.height = `${containerElement.clientHeight}px`;
terminalContainer.style.maxWidth = '100%';
terminalContainer.style.boxSizing = 'border-box';
// Create a new terminal
terminal = new Terminal({
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace",
fontSize: 18,
lineHeight: 1.2,
theme: {
background: '#1E1E1E',
foreground: '#F8F8F8',
cursor: '#A0A0A0',
selectionBackground: '#363B4E'
},
cursorBlink: true,
scrollback: 10000,
allowTransparency: true,
disableStdin: false
});
// Create a fit addon
fitAddon = new FitAddon.FitAddon();
terminal.loadAddon(fitAddon);
// Open the terminal in the container
terminal.open(terminalContainer);
fitAddon.fit();
// Connect to Socket.io server
connectToSocketIO();
// Add window resize listener to keep terminal properly sized
window.addEventListener('resize', () => {
if (isActive) {
resizeTerminal(containerElement);
}
});
isTerminalInitialized = true;
return { terminal, fitAddon };
}
// Resize terminal to fit container
function resizeTerminal(containerElement) {
if (!terminal || !isTerminalInitialized) return;
setTimeout(() => {
// Recalculate terminal container size
const terminalContainer = document.getElementById('terminal');
const containerHeight = containerElement.clientHeight;
const containerWidth = containerElement.clientWidth;
if (terminalContainer && containerHeight && containerWidth) {
terminalContainer.style.height = `${containerHeight}px`;
terminalContainer.style.width = `${containerWidth}px`;
// Ensure the terminal div takes full available space
terminalContainer.style.maxWidth = '100%';
terminalContainer.style.boxSizing = 'border-box';
}
// Use the existing fitAddon instead of creating a new one
if (fitAddon) {
fitAddon.fit();
console.log(`Terminal resized to ${terminal.cols} columns by ${terminal.rows} rows`);
// Update server with new dimensions
if (socket && socket.connected) {
socket.emit('resize', {
cols: terminal.cols,
rows: terminal.rows
});
}
} else {
console.warn('fitAddon not available for resizing');
}
}, 100);
}
// Connect to SSH and handle reconnection
function connectToSSH() {
if (terminalCallbacks.showConnectionStatus) {
terminalCallbacks.showConnectionStatus('Connecting to terminal...', 'info');
}
// Clear terminal if it has content
if (terminal) {
terminal.clear();
}
// Connect to Socket.io server immediately without animation
connectToSocketIO();
}
// Connect to Socket.io
function connectToSocketIO() {
// Don't reconnect if socket already exists and is connected
if (socket && socket.connected) {
console.log('Socket already connected, skipping reconnection');
return;
}
// Disconnect existing socket if it exists
if (socket) {
console.log('Disconnecting existing socket before creating a new one');
socket.off('data');
socket.off('connect');
socket.off('disconnect');
socket.off('error');
socket.disconnect();
}
// Connect to Socket.io server
socket = io('/ssh', {
forceNew: true,
reconnectionAttempts: 5,
timeout: 10000
});
console.log('Creating new socket connection to SSH server');
// Handle connection events
socket.on('connect', () => {
console.log('Connected to SSH server');
if (terminalCallbacks.showConnectionStatus) {
terminalCallbacks.showConnectionStatus('Connected to terminal', 'success');
}
// Send terminal size to server
const dimensions = {
cols: terminal.cols,
rows: terminal.rows
};
socket.emit('resize', dimensions);
});
// Handle disconnect
socket.on('disconnect', () => {
console.log('Disconnected from SSH server');
if (terminalCallbacks.showConnectionStatus) {
terminalCallbacks.showConnectionStatus('Disconnected from terminal. Reconnecting...', 'error');
}
// Show disconnected message in terminal with styling
if (terminal) {
terminal.writeln('\r\n\r\n\x1b[1;31m[CONNECTION LOST]\x1b[0m Terminal disconnected.');
terminal.writeln('\x1b[0;33mAttempting to reconnect...\x1b[0m\r\n');
}
// Try to reconnect after a delay
setTimeout(() => {
if (socket && !socket.connected) {
socket.connect();
}
}, 2000);
});
// Handle connection error
socket.on('error', (err) => {
console.error('Socket connection error:', err);
if (terminal) {
terminal.writeln(`\r\n\x1b[1;31m[ERROR]\x1b[0m ${err.message}\r\n`);
}
});
// Handle SSH data with processing for ANSI codes
socket.on('data', (data) => {
if (terminal) {
terminal.write(data);
}
});
// Clear any existing listeners to prevent duplication
if (terminal) {
// Get all registered event listeners
const existingListeners = terminal._core._events;
// If we have existing data listeners, clear them
if (existingListeners && existingListeners.data) {
// Remove previous data listeners
terminal._core.off('data');
console.log('Removed existing terminal data listeners');
}
// Add our data handler
terminal.onData((data) => {
if (socket && socket.connected) {
socket.emit('data', data);
} else {
// Visual feedback when trying to type while disconnected
terminal.write('\r\n\x1b[1;31m[DISCONNECTED]\x1b[0m Cannot send data. Reconnecting...\r\n');
socket.connect();
}
});
console.log('Added new terminal data listener');
}
}
// Check if terminal is initialized
function isInitialized() {
return isTerminalInitialized;
}
// Get terminal instance
function getTerminal() {
return terminal;
}
// Get socket instance
function getSocket() {
return socket;
}
// Export functions
export {
initTerminal,
resizeTerminal,
connectToSSH,
isInitialized,
getTerminal,
getSocket,
setCallbacks
};

View File

@@ -0,0 +1,218 @@
/**
* Timer Service
* Handles timer functionality for the exam
*/
let timerDuration = 120; // Default timer duration in minutes
let timerInterval = null;
let timerElement = null;
let onTimerEndCallback = null;
let isTimerRunning = false;
const timeThresholds = [30, 10, 0]; // Time thresholds in minutes
let timeThresholdCallbacks = {}; // Callbacks for time thresholds
let processedThresholds = new Set(); // Track which thresholds have been processed
/**
* Set timer duration
* @param {number} duration - Duration in minutes
*/
function setTimerDuration(duration) {
timerDuration = duration;
// Reset processed thresholds when timer duration changes
processedThresholds.clear();
}
/**
* Register callbacks for specific time thresholds
* @param {Object} callbacks - Object with threshold minutes as keys and callback functions as values
*/
function registerTimeThresholdCallbacks(callbacks) {
timeThresholdCallbacks = { ...timeThresholdCallbacks, ...callbacks };
}
/**
* Get remaining time
* @returns {number} Remaining time in minutes
*/
function getRemainingTime() {
return timerDuration;
}
/**
* Calculate remaining exam time based on exam info
* @param {Object} examInfo - The exam information object
* @returns {number} Remaining time in minutes
*/
function calculateRemainingTime(examInfo) {
// If exam hasn't started yet or no duration set, return default
if (!examInfo.info?.examDurationInMinutes) {
return getRemainingTime();
}
// Check if exam has ended
if (examInfo.info?.events?.examEndTime) {
return 0; // No time remaining if exam is finished
}
// If exam has started, calculate remaining time
if (examInfo.info?.events?.examStartTime) {
// Make sure we have a proper number value for startTime
let startTime = examInfo.info.events.examStartTime;
// Parse the timestamp to ensure it's a number (could be stored as string)
if (typeof startTime === 'string') {
startTime = parseInt(startTime, 10);
}
// Debug log to identify the issue
console.log('Start time (epoch):', startTime);
console.log('Current time (epoch):', Date.now());
console.log('Exam duration (minutes):', examInfo.info.examDurationInMinutes);
const durationMs = examInfo.info.examDurationInMinutes * 60 * 1000;
const elapsedMs = Date.now() - startTime;
const remainingMs = Math.max(0, durationMs - elapsedMs);
// Debug log for troubleshooting
console.log('Duration (ms):', durationMs);
console.log('Elapsed (ms):', elapsedMs);
console.log('Remaining (ms):', remainingMs);
console.log('Remaining (minutes):', Math.ceil(remainingMs / (60 * 1000)));
// Convert ms to minutes
return Math.ceil(remainingMs / (60 * 1000));
}
// Default: return full duration if exam hasn't started
return examInfo.info.examDurationInMinutes;
}
/**
* Initialize the timer
* @param {HTMLElement} element - DOM element to display the timer
* @param {number} minutes - Timer duration in minutes
* @param {Object} options - Optional configuration
* @param {Function} options.onTimerEnd - Callback function when timer ends
*/
function initTimer(element, minutes, options = {}) {
timerElement = element;
timerDuration = minutes;
onTimerEndCallback = options.onTimerEnd;
// Reset processed thresholds when initializing new timer
processedThresholds.clear();
updateTimerDisplay(minutes);
}
/**
* Check if a time threshold has been reached and trigger callback
* @param {number} remainingMinutes - Remaining time in minutes
* @param {number} remainingSeconds - Remaining time in seconds
*/
function checkTimeThresholds(remainingMinutes, remainingSeconds) {
// Check each threshold
timeThresholds.forEach(threshold => {
// If we just crossed this threshold and haven't processed it yet
if (remainingMinutes === threshold &&
remainingSeconds === 0 &&
!processedThresholds.has(threshold)) {
// Mark as processed
processedThresholds.add(threshold);
// Call the registered callback if exists
if (timeThresholdCallbacks[threshold]) {
timeThresholdCallbacks[threshold](threshold);
}
}
});
}
/**
* Start the timer
*/
function startTimer() {
if (isTimerRunning) return;
isTimerRunning = true;
let remainingSeconds = timerDuration * 60;
timerInterval = setInterval(() => {
remainingSeconds--;
if (remainingSeconds <= 0) {
stopTimer();
updateTimerDisplay(0);
if (onTimerEndCallback) {
onTimerEndCallback();
}
return;
}
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
// Check if we've hit any time thresholds
checkTimeThresholds(minutes, seconds);
updateTimerDisplay(minutes, seconds);
}, 1000);
}
/**
* Stop the timer
*/
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
isTimerRunning = false;
}
}
/**
* Reset the timer
*/
function resetTimer() {
stopTimer();
timerDuration = 120; // Reset to default
processedThresholds.clear();
if (timerElement) {
updateTimerDisplay(timerDuration);
}
}
/**
* Update the timer display
* @param {number} minutes - Minutes remaining
* @param {number} seconds - Seconds remaining
*/
function updateTimerDisplay(minutes, seconds = 0) {
if (!timerElement) return;
const formattedMinutes = String(minutes).padStart(2, '0');
const formattedSeconds = String(seconds).padStart(2, '0');
timerElement.textContent = `${formattedMinutes}:${formattedSeconds}`;
// Add visual warning when less than 5 minutes remaining
if (minutes < 5) {
timerElement.classList.add('timer-warning');
} else {
timerElement.classList.remove('timer-warning');
}
}
// Export the timer service
export {
setTimerDuration,
getRemainingTime,
calculateRemainingTime,
initTimer,
startTimer,
stopTimer,
resetTimer,
registerTimeThresholdCallbacks
};

View File

@@ -0,0 +1,207 @@
/**
* UI Utilities
* Handles common UI functions and interactions
*/
// Format time as mm:ss
function formatTime(totalSeconds) {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${String(seconds).padStart(2, '0')}`;
}
// Show connection status notification
function showConnectionStatus(element, message, type) {
if (!element) return;
element.textContent = message;
element.className = 'connection-status show';
if (type === 'success') {
element.style.backgroundColor = 'rgba(25, 135, 84, 0.7)';
} else if (type === 'error') {
element.style.backgroundColor = 'rgba(220, 53, 69, 0.7)';
} else {
element.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
}
// Hide the status after a few seconds for success messages
if (type === 'success') {
setTimeout(() => {
element.className = 'connection-status';
}, 3000);
}
}
// Function to request fullscreen mode
function requestFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) { // Firefox
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) { // Chrome, Safari, Opera
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) { // IE/Edge
element.msRequestFullscreen();
}
}
// Function to exit fullscreen mode
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) { // Firefox
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) { // Chrome, Safari, Opera
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { // IE/Edge
document.msExitFullscreen();
}
}
// Function to toggle fullscreen mode
function toggleFullscreen() {
if (!document.fullscreenElement &&
!document.mozFullScreenElement &&
!document.webkitFullscreenElement &&
!document.msFullscreenElement) {
requestFullscreen(document.documentElement);
} else {
exitFullscreen();
}
}
// Show modal for completed exams
function showExamCompletedModal(examId, status) {
// Create a modal to show exam completion and option to connect to session
const completionModalHTML = `
<div class="modal fade" id="examCompletedModal" tabindex="-1" aria-labelledby="examCompletedModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="examCompletedModalLabel">
${status === 'EVALUATED' ? 'Exam Complete' : 'Exam Being Evaluated'}
</h5>
</div>
<div class="modal-body">
${status === 'EVALUATED' ?
'<p>This exam has been completed and your results are ready to view.</p>' :
'<p>This exam has been completed and is currently being evaluated.</p><p>You can view the results once evaluation is complete.</p>'
}
<p>You can still connect to your exam environment to review your work.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="connectToSessionBtn">Connect to Session</button>
</div>
</div>
</div>
</div>
`;
// Check if modal already exists, remove it if it does
const existingModal = document.getElementById('examCompletedModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', completionModalHTML);
// Show modal
const completionModal = new bootstrap.Modal(document.getElementById('examCompletedModal'));
completionModal.show();
// Handle connect to session button click
document.getElementById('connectToSessionBtn').addEventListener('click', () => {
// Close the modal
completionModal.hide();
// Remove the modal from DOM after hiding
setTimeout(() => {
const modalElement = document.getElementById('examCompletedModal');
if (modalElement) {
const modalBackdrop = document.querySelector('.modal-backdrop');
if (modalBackdrop) {
modalBackdrop.remove();
}
modalElement.remove();
}
}, 300);
// Dispatch event to notify exam.js to connect to the session
const examCompleteEvent = new CustomEvent('examCompletedSession', {
detail: { examId }
});
document.dispatchEvent(examCompleteEvent);
});
}
// Update UI based on fullscreen state
function updateFullscreenUI(fullscreenBtn) {
if (fullscreenBtn) {
if (document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement) {
fullscreenBtn.textContent = 'Exit Fullscreen';
} else {
fullscreenBtn.textContent = 'Enter Fullscreen';
}
}
}
// Show a toast notification
function showToast(message, options = {}) {
const toastContainer = document.getElementById('toastContainer');
if (!toastContainer) return;
// Default options
const defaults = {
bgColor: 'bg-primary',
textColor: 'text-white',
autohide: true,
delay: 5000
};
// Merge with custom options
const settings = { ...defaults, ...options };
// Create a unique ID for this toast
const toastId = 'toast-' + Date.now();
// Create toast HTML
const toastHTML = `
<div class="toast ${settings.bgColor} ${settings.textColor}" id="${toastId}" role="alert" aria-live="assertive" aria-atomic="true" ${settings.autohide ? 'data-bs-autohide="true"' : 'data-bs-autohide="false"'} data-bs-delay="${settings.delay}">
<div class="toast-header">
<strong class="me-auto">Exam Notification</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
</div>
`;
// Add toast to container
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
// Initialize and show the toast
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement);
toast.show();
// Return the toast instance for later reference
return toast;
}
export {
formatTime,
showConnectionStatus,
requestFullscreen,
exitFullscreen,
toggleFullscreen,
showExamCompletedModal,
updateFullscreenUI,
showToast
};

View File

@@ -0,0 +1,80 @@
/**
* Wake Lock Service
* Prevents device from sleeping during exam
*/
let wakeLock = null;
/**
* Acquire screen wake lock to prevent device from sleeping
* @returns {Promise<boolean>} True if wake lock acquired successfully
*/
async function acquireWakeLock() {
// Check if Wake Lock API is supported
if ('wakeLock' in navigator) {
try {
// Attempt to acquire wake lock
wakeLock = await navigator.wakeLock.request('screen');
console.log('Wake Lock activated');
// Add release event listener for page visibility change
document.addEventListener('visibilitychange', handleVisibilityChange);
return true;
} catch (err) {
console.error('Failed to acquire Wake Lock:', err);
return false;
}
} else {
console.warn('Wake Lock API not supported in this browser');
return false;
}
}
/**
* Handle visibility change to reacquire wake lock when page becomes visible again
*/
function handleVisibilityChange() {
if (document.visibilityState === 'visible') {
// Reacquire wake lock when tab becomes visible again
acquireWakeLock();
}
}
/**
* Release wake lock manually
*/
async function releaseWakeLock() {
if (wakeLock) {
try {
await wakeLock.release();
wakeLock = null;
console.log('Wake Lock released');
// Remove event listener
document.removeEventListener('visibilitychange', handleVisibilityChange);
return true;
} catch (err) {
console.error('Failed to release Wake Lock:', err);
return false;
}
}
return true;
}
/**
* Check if wake lock is currently active
* @returns {boolean} True if wake lock is active
*/
function isWakeLockActive() {
return wakeLock !== null;
}
// Export the wake lock functions
export {
acquireWakeLock,
releaseWakeLock,
isWakeLockActive
};

903
app/public/js/exam.js Normal file
View File

@@ -0,0 +1,903 @@
/**
* Exam Application
* Main entry point for the exam functionality
*/
// Import services and utilities
import * as ExamApi from './components/exam-api.js';
import * as TerminalService from './components/terminal-service.js';
import * as RemoteDesktopService from './components/remote-desktop-service.js';
import * as QuestionService from './components/question-service.js';
import * as TimerService from './components/timer-service.js';
import * as UiUtils from './components/ui-utils.js';
import * as WakeLockService from './components/wake-lock-service.js';
import * as ClipboardService from './components/clipboard-service.js';
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const pageLoader = document.getElementById('pageLoader');
const endExamBtnDropdown = document.getElementById('endExamBtnDropdown');
const confirmEndBtn = document.getElementById('confirmEndBtn');
const terminateSessionBtn = document.getElementById('terminateSessionBtn');
const confirmTerminateBtn = document.getElementById('confirmTerminateBtn');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const questionDropdown = document.getElementById('questionDropdown');
const questionDropdownMenu = document.getElementById('questionDropdownMenu');
const questionContent = document.getElementById('questionContent');
const examTimer = document.getElementById('examTimer');
const vncFrame = document.getElementById('vnc-frame');
const connectionStatus = document.getElementById('connectionStatus');
const startExamBtn = document.getElementById('startExamBtn');
const toggleViewBtn = document.getElementById('toggleViewBtn');
const sshTerminalContainer = document.getElementById('sshTerminalContainer');
const sshConnectionStatus = document.getElementById('sshConnectionStatus');
const viewResultsBtn = document.getElementById('viewResultsBtn');
// Modals
const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal'));
const terminateModal = new bootstrap.Modal(document.getElementById('terminateModal'));
const startExamModal = new bootstrap.Modal(document.getElementById('startExamModal'));
const examEndModal = new bootstrap.Modal(document.getElementById('examEndModal'));
// State variables
let examInfo = {}; // Store exam information
let currentQuestionId = 1;
let questions = [];
let isTerminalActive = false;
let isCompletedExamMode = false;
// Add event listener for page unload to clean up resources
window.addEventListener('beforeunload', cleanupResources);
// Initialize by fetching questions
fetchExamQuestions();
// Listen for examCompletedSession event and handle connecting to a finished exam session
document.addEventListener('examCompletedSession', function(event) {
const { examId } = event.detail;
console.log('Connecting to completed exam session for exam:', examId);
// Get DOM elements
const pageLoader = document.getElementById('pageLoader');
const vncFrame = document.getElementById('vnc-frame');
const connectionStatus = document.getElementById('connectionStatus');
const examTimer = document.getElementById('examTimer');
// Set completed exam mode
isCompletedExamMode = true;
// Show loader
pageLoader.style.display = 'flex';
// Create a promises array for parallel fetching
const promises = [
// Fetch exam info
ExamApi.fetchCurrentExamInfo(),
// Fetch exam questions
ExamApi.fetchExamData(examId)
];
// Execute all promises in parallel
Promise.all(promises)
.then(([examInfoData, questionsData]) => {
// Store exam info for later use
examInfo = examInfoData;
// Hide timer for completed exam
examTimer.style.display = 'none';
// Add Review Mode badge next to title
const headerTitle = document.querySelector('.header-title');
if (headerTitle) {
// Create the badge
const reviewBadge = document.createElement('span');
reviewBadge.className = 'review-mode-badge';
reviewBadge.textContent = 'Review Mode';
// Add badge next to title
headerTitle.appendChild(reviewBadge);
}
// Hide end exam button if it exists
const endExamItem = document.querySelector('.dropdown-item[href="#"][id="endExamBtnDropdown"]');
if (endExamItem) {
endExamItem.style.display = 'none';
}
if (questionsData) {
// Transform the questions
questions = QuestionService.transformQuestionsFromApi(questionsData);
// Initialize the exam UI with questions
initExamUI();
console.log('Loaded', questions.length, 'questions for completed exam');
// Add "View Results" button to header
const headerControls = document.querySelector('.header-controls');
if (headerControls) {
const viewResultsBtn = document.createElement('button');
viewResultsBtn.className = 'btn-custom';
viewResultsBtn.textContent = 'View Results';
viewResultsBtn.addEventListener('click', () => {
window.location.href = `/results?id=${examId}`;
});
// Add button to beginning of controls
headerControls.prepend(viewResultsBtn);
}
}
// Connect to Remote Desktop
RemoteDesktopService.connectToRemoteDesktop(vncFrame, showVncConnectionStatus);
// Hide loader after a short delay
setTimeout(() => {
pageLoader.style.display = 'none';
}, 1500);
})
.catch(error => {
console.error('Error loading exam environment:', error);
alert('Failed to connect to exam environment. Please try again.');
pageLoader.style.display = 'none';
});
});
// Function declarations
// Function to show VNC connection status
function showVncConnectionStatus(message, type) {
UiUtils.showConnectionStatus(connectionStatus, message, type);
}
// Function to show SSH connection status
function showSshConnectionStatus(message, type) {
UiUtils.showConnectionStatus(sshConnectionStatus, message, type);
}
// Setup callbacks for terminal service
TerminalService.setCallbacks({
showConnectionStatus: showSshConnectionStatus
});
// Load exam environment for completed exams
function loadExamEnvironment(examId) {
// Show loader
pageLoader.style.display = 'flex';
// Fetch exam info for connection details only
ExamApi.fetchCurrentExamInfo()
.then(data => {
examInfo = data;
// Connect to environment
connectToExamSession();
// Hide loader after a short delay
setTimeout(() => {
pageLoader.style.display = 'none';
}, 1500);
})
.catch(error => {
console.error('Error loading exam environment:', error);
alert('Failed to connect to exam environment. Please try again.');
pageLoader.style.display = 'none';
});
}
// Connect to exam session for review
function connectToExamSession() {
console.log('Connecting to exam session in completed exam mode');
// Connect to Remote Desktop
RemoteDesktopService.connectToRemoteDesktop(vncFrame, showVncConnectionStatus);
// Setup Remote Desktop frame handlers
RemoteDesktopService.setupRemoteDesktopFrameHandlers(vncFrame, showVncConnectionStatus);
// Enter fullscreen mode for better visibility
UiUtils.requestFullscreen(document.documentElement);
}
// Fetch questions from API
function fetchExamQuestions() {
// Show loader while fetching questions
pageLoader.style.display = 'flex';
const examId = ExamApi.getExamId();
if (!examId) return;
// First check exam status to handle completed exams
ExamApi.checkExamStatus(examId)
.then(status => {
if (status === 'EVALUATED' || status === 'EVALUATING') {
// Show option to view results for completed exams
UiUtils.showExamCompletedModal(examId, status);
pageLoader.style.display = 'none';
return null; // Skip loading questions
}
// First fetch current exam info
return ExamApi.fetchCurrentExamInfo()
.then(data => {
examInfo = data;
// Set timer based on examDurationInMinutes and examStartTime if available
if (data.info) {
if (data.info.events?.examStartTime && data.info.examDurationInMinutes) {
// Calculate remaining time for in-progress exams
const remainingMinutes = TimerService.calculateRemainingTime(data);
console.log(`Setting timer to ${remainingMinutes} minutes (based on start time)`);
TimerService.setTimerDuration(remainingMinutes);
} else if (data.info.examDurationInMinutes) {
// For new exams, use the full duration
console.log(`Setting timer to ${data.info.examDurationInMinutes} minutes`);
TimerService.setTimerDuration(data.info.examDurationInMinutes);
} else {
console.warn('examDurationInMinutes not found in API response, using default duration');
}
}
return ExamApi.fetchExamData(examId);
})
.then(data => {
if (!data) return;
// Store exam info for later use
examInfo = data.info || examInfo;
// Transform the questions
questions = QuestionService.transformQuestionsFromApi(data);
// Initialize the exam UI after loading questions
initExamUI();
// Show the start exam modal
showStartExamModal();
// Hide loader
pageLoader.style.display = 'none';
})
.catch(error => {
console.error('Error loading exam:', error);
// Show an error message to the user
alert('Failed to load exam data. Please refresh the page or contact support.');
pageLoader.style.display = 'none';
});
})
.catch(error => {
console.error('Error loading exam:', error);
// Show an error message to the user
alert('Failed to load exam data. Please refresh the page or contact support.');
pageLoader.style.display = 'none';
});
}
// Show start exam modal
function showStartExamModal() {
// Check if the exam has already been started or completed
if (examInfo.info?.events?.examStartTime) {
setupContinueExamButton();
} else {
setupNewExamButton();
}
// Always show the modal regardless of exam state
startExamModal.show();
}
// Setup button for continuing an existing exam
function setupContinueExamButton() {
const modalTitle = document.getElementById('startExamModalLabel');
// Hide default content
document.getElementById('newExamContent').style.display = 'none';
if (examInfo.info.events.examEndTime) {
// Exam is finished - show completed content
document.getElementById('examCompletedContent').style.display = 'block';
document.getElementById('examInProgressContent').style.display = 'none';
// Update modal title
modalTitle.textContent = 'Exam Completed';
startExamBtn.textContent = 'Continue to Session';
} else {
// Exam is in progress - show in-progress content
document.getElementById('examInProgressContent').style.display = 'block';
document.getElementById('examCompletedContent').style.display = 'none';
// Update modal title
modalTitle.textContent = 'Exam in Progress';
startExamBtn.textContent = 'Continue Session';
}
// Remove countdown behavior and just continue to session
startExamBtn.addEventListener('click', function() {
startExamModal.hide();
startExam();
}, { once: true });
}
// Setup button for starting a new exam
function setupNewExamButton() {
// Show default content, hide others
document.getElementById('newExamContent').style.display = 'block';
document.getElementById('examInProgressContent').style.display = 'none';
document.getElementById('examCompletedContent').style.display = 'none';
// Reset modal title
document.getElementById('startExamModalLabel').textContent = 'Ready to Begin Your Exam';
// Initialize counter
let countDown = 3;
startExamBtn.innerHTML = `Start Exam (${countDown})`;
// Remove any existing click handlers
startExamBtn.replaceWith(startExamBtn.cloneNode(true));
// Get the fresh reference after cloning
const freshStartExamBtn = document.getElementById('startExamBtn');
// Add event listener to start exam button
if (freshStartExamBtn) {
freshStartExamBtn.addEventListener('click', function handleStartClick() {
// Decrease counter on each click
countDown--;
if (countDown > 0) {
// Update button text with new counter
freshStartExamBtn.innerHTML = `Start Exam (${countDown})`;
} else {
// Start exam when counter reaches 0
freshStartExamBtn.removeEventListener('click', handleStartClick);
startExamModal.hide();
startExam();
}
});
}
}
// Track exam start event
function trackExamStartEvent() {
// Track exam start event only if this is a new exam (not a continuation)
const examId = ExamApi.getExamId();
if (examId && !examInfo.info?.events?.examStartTime) {
const currentTime = Date.now();
console.log('Setting exam start time:', currentTime);
ExamApi.trackExamEvent(examId, {
examStartTime: currentTime,
userAgent: navigator.userAgent,
screenResolution: `${window.screen.width}x${window.screen.height}`
});
}
}
// Track exam end event
function trackExamEndEvent() {
const examId = ExamApi.getExamId();
if (!examId) return;
const currentEndTime = Date.now();
console.log('Setting exam end time:', currentEndTime);
return ExamApi.trackExamEvent(examId, {
examEndTime: currentEndTime
}).catch(error => {
console.error('Error tracking exam end event:', error);
// Continue with exam evaluation regardless of tracking error
});
}
// Setup timer threshold notifications
function setupTimerNotifications() {
TimerService.registerTimeThresholdCallbacks({
// 30 minutes remaining
30: (minutes) => {
UiUtils.showToast('30 minutes left in your exam', {
bgColor: 'bg-warning',
textColor: 'text-dark',
delay: 7000
});
},
// 10 minutes remaining
10: (minutes) => {
UiUtils.showToast('Only 10 minutes remaining! ', {
bgColor: 'bg-danger',
textColor: 'text-white',
delay: 10000
});
},
// Time's up
0: (minutes) => {
handleExamEnd();
}
});
}
// Start exam functionality
function startExam() {
// Show loader while starting exam
pageLoader.style.display = 'flex';
// Enter fullscreen mode
UiUtils.requestFullscreen(document.documentElement);
// Acquire wake lock to prevent device from sleeping
WakeLockService.acquireWakeLock()
.then(success => {
if (success) {
console.log('Wake lock acquired successfully');
} else {
console.warn('Wake lock not acquired, device may sleep during exam');
}
});
// Connect to Remote Desktop
RemoteDesktopService.connectToRemoteDesktop(vncFrame, showVncConnectionStatus);
// Calculate remaining time
const remainingTime = TimerService.calculateRemainingTime(examInfo);
// Handle timer visibility and initialization
if (remainingTime <= 0 || examInfo.info?.events?.examEndTime) {
// Hide timer if time is up or exam has ended
examTimer.style.display = 'none';
} else {
// Show and initialize timer with remaining time
examTimer.style.display = 'block';
// Initialize timer with DOM element
TimerService.initTimer(examTimer, remainingTime, {
onTimerEnd: () => {
handleExamEnd();
}
});
// Set up timer notifications
setupTimerNotifications();
// Start the timer
TimerService.startTimer();
}
// Track exam start
trackExamStartEvent();
// Hide loader after a short delay
setTimeout(() => {
pageLoader.style.display = 'none';
}, 1500);
}
// Handle exam end when timer reaches zero
function handleExamEnd() {
// Release wake lock as exam is ending
WakeLockService.releaseWakeLock()
.then(success => {
if (success) {
console.log('Wake lock released successfully');
}
});
// Show the exam end modal
examEndModal.show();
// Get exam ID
const examId = ExamApi.getExamId();
if (!examId) return;
// Track exam end event
trackExamEndEvent()
.then(() => {
// Make API call to end and evaluate the exam
return ExamApi.evaluateExam(examId);
})
.then(data => {
console.log('Exam evaluation started:', data);
// Set up the View Results button
if (viewResultsBtn) {
viewResultsBtn.addEventListener('click', () => {
window.location.href = `/results?id=${examId}`;
});
}
})
.catch(error => {
console.error('Error ending exam:', error);
alert('There was an error evaluating your exam. Please try manually ending the exam.');
});
}
// Update question content
function updateQuestionContent(questionId) {
const question = questions.find(q => q.id === questionId || q.id === questionId.toString());
if (!question) {
console.error(`Question with ID ${questionId} not found`);
questionContent.innerHTML = '<div class="alert alert-danger">Question not found. Please try another question.</div>';
return;
}
// Add fade effect
questionContent.classList.add('content-fade');
// Use requestAnimationFrame for a smoother transition
requestAnimationFrame(() => {
// Short timeout for the transition to take effect
setTimeout(() => {
try {
// Get formatted content from QuestionService
const formattedContent = QuestionService.generateQuestionContent(question);
// Update content
questionContent.innerHTML = formattedContent;
// Hide action buttons in completed exam review mode
if (isCompletedExamMode) {
const actionButtonsContainer = document.querySelector('.action-buttons-container');
if (actionButtonsContainer) {
actionButtonsContainer.style.display = 'none';
}
} else {
// Add functionality to flag button
const flagQuestionBtn = document.getElementById('flagQuestionBtn');
if (flagQuestionBtn) {
flagQuestionBtn.addEventListener('click', function() {
toggleQuestionFlag(questionId);
});
}
// Add functionality to next button
const nextQuestionBtn = document.getElementById('nextQuestionBtn');
if (nextQuestionBtn) {
nextQuestionBtn.addEventListener('click', function() {
const currentIndex = questions.findIndex(q => q.id === currentQuestionId || q.id === currentQuestionId.toString());
if (currentIndex < questions.length - 1) {
currentQuestionId = questions[currentIndex + 1].id;
updateQuestionContent(currentQuestionId);
updateNavigationButtons();
}
});
}
}
// Update dropdown button text
questionDropdown.textContent = question.title || `Question ${questionId}`;
// Add subtle transition indicator
questionContent.classList.add('question-transition');
// Remove fade effect
requestAnimationFrame(() => {
questionContent.classList.remove('content-fade');
// Remove transition indicator after animation completes
setTimeout(() => {
questionContent.classList.remove('question-transition');
}, 1000);
});
} catch (error) {
console.error('Error updating question content:', error);
questionContent.innerHTML = '<div class="alert alert-danger">Error displaying question content. Please try refreshing the page.</div>';
questionContent.classList.remove('content-fade');
}
}, 100);
});
}
// Toggle between VNC and Terminal views
function toggleView() {
const terminalContainer = document.querySelector('.terminal-container');
if (isTerminalActive) {
// Switch to VNC
sshTerminalContainer.style.display = 'none';
terminalContainer.style.display = 'flex';
toggleViewBtn.textContent = 'Switch to Terminal';
isTerminalActive = false;
} else {
// Switch to Terminal
terminalContainer.style.display = 'none';
sshTerminalContainer.style.display = 'flex';
toggleViewBtn.textContent = 'Switch to Remote Desktop';
isTerminalActive = true;
// Show toast notification about real exam constraints
UiUtils.showToast('Note: In the actual certification exam, only remote desktop access is available. Terminal access is provided here for practice convenience.', {
bgColor: 'bg-info',
textColor: 'text-white',
delay: 8000
});
// Initialize terminal if not already done
if (!TerminalService.isInitialized()) {
TerminalService.initTerminal(sshTerminalContainer, true);
} else {
// Resize terminal to fit container
TerminalService.resizeTerminal(sshTerminalContainer);
}
// Give a little time for the display change to take effect, then resize again
setTimeout(() => {
TerminalService.resizeTerminal(sshTerminalContainer);
}, 300);
}
}
// Initialize the exam UI
function initExamUI() {
// Set the first question
updateQuestionContent(currentQuestionId);
// Dynamically populate question dropdown with fetched questions
updateQuestionDropdown();
// Update navigation buttons
updateNavigationButtons();
// Setup Remote Desktop frame handlers
RemoteDesktopService.setupRemoteDesktopFrameHandlers(vncFrame, showVncConnectionStatus);
// Setup UI event listeners
setupUIEventListeners();
// Setup clipboard copy for inline code elements
ClipboardService.setupInlineCodeCopy();
// If in completed exam mode, ensure the question pane is visible
if (isCompletedExamMode) {
console.log('Setting up completed exam review mode UI');
// Make sure question panel is visible and sized correctly
const questionPanel = document.getElementById('questionPanel');
if (questionPanel) {
questionPanel.style.display = 'block';
// Initialize panel resizer if available to ensure question pane is correctly sized
if (window.panelResizer) {
setTimeout(() => {
window.panelResizer.resetPanels();
}, 500);
}
}
// Add read-only indicator to question panel
const questionContent = document.getElementById('questionContent');
if (questionContent) {
const reviewBanner = document.createElement('div');
reviewBanner.className = 'alert alert-info mb-3';
reviewBanner.textContent = 'Review Mode: This exam has been completed. You can review questions and your environment.';
// Insert at the beginning of question content
if (questionContent.firstChild) {
questionContent.insertBefore(reviewBanner, questionContent.firstChild);
} else {
questionContent.appendChild(reviewBanner);
}
}
}
}
// Setup all UI event listeners
function setupUIEventListeners() {
// Setup fullscreen toggle button for page
const fullscreenBtn = document.getElementById('fullscreenBtn');
if (fullscreenBtn) {
fullscreenBtn.addEventListener('click', function(e) {
e.preventDefault();
UiUtils.toggleFullscreen();
});
}
// Setup fullscreen toggle button for VNC iframe
const fullscreenVncBtn = document.getElementById('fullscreenVncBtn');
if (fullscreenVncBtn) {
fullscreenVncBtn.addEventListener('click', function(e) {
e.preventDefault();
// Request fullscreen for the VNC iframe
UiUtils.requestFullscreen(vncFrame);
});
}
// Setup reconnect button for VNC
const reconnectVncBtn = document.getElementById('reconnectVncBtn');
if (reconnectVncBtn) {
reconnectVncBtn.addEventListener('click', function(e) {
e.preventDefault();
showVncConnectionStatus('Reconnecting to Remote Desktop...', 'info');
RemoteDesktopService.connectToRemoteDesktop(vncFrame, showVncConnectionStatus);
});
}
// Setup fullscreen toggle button for Terminal
const fullscreenTerminalBtn = document.getElementById('fullscreenTerminalBtn');
if (fullscreenTerminalBtn) {
fullscreenTerminalBtn.addEventListener('click', function(e) {
e.preventDefault();
// Request fullscreen for the terminal container
UiUtils.requestFullscreen(sshTerminalContainer);
});
}
// Setup toggle view button
if (toggleViewBtn) {
toggleViewBtn.addEventListener('click', function(e) {
e.preventDefault();
toggleView();
});
}
// Add fullscreen change event listener to update UI accordingly
document.addEventListener('fullscreenchange', () => UiUtils.updateFullscreenUI(fullscreenBtn));
document.addEventListener('webkitfullscreenchange', () => UiUtils.updateFullscreenUI(fullscreenBtn));
document.addEventListener('mozfullscreenchange', () => UiUtils.updateFullscreenUI(fullscreenBtn));
document.addEventListener('MSFullscreenChange', () => UiUtils.updateFullscreenUI(fullscreenBtn));
// Setup resize terminal button
const resizeTerminalBtn = document.getElementById('resizeTerminalBtn');
if (resizeTerminalBtn) {
resizeTerminalBtn.addEventListener('click', function(e) {
e.preventDefault();
// Use the globally exposed PanelResizer instance
if (window.panelResizer) {
window.panelResizer.resetPanels();
}
// Fallback: simulate a double-click on the divider
else {
const divider = document.getElementById('panelDivider');
if (divider) {
const dblClickEvent = new MouseEvent('dblclick', {
bubbles: true,
cancelable: true,
view: window
});
divider.dispatchEvent(dblClickEvent);
}
}
});
}
}
// Event listeners for navigation
prevBtn.addEventListener('click', () => {
const currentIndex = questions.findIndex(q => q.id === currentQuestionId || q.id === currentQuestionId.toString());
if (currentIndex > 0) {
currentQuestionId = questions[currentIndex - 1].id;
updateQuestionContent(currentQuestionId);
updateNavigationButtons();
}
});
nextBtn.addEventListener('click', () => {
const currentIndex = questions.findIndex(q => q.id === currentQuestionId || q.id === currentQuestionId.toString());
if (currentIndex < questions.length - 1) {
currentQuestionId = questions[currentIndex + 1].id;
updateQuestionContent(currentQuestionId);
updateNavigationButtons();
}
});
// End exam button (dropdown option)
endExamBtnDropdown.addEventListener('click', () => {
confirmModal.show();
});
// Confirm end exam
confirmEndBtn.addEventListener('click', () => {
pageLoader.style.display = 'flex';
confirmModal.hide();
// Release wake lock as exam is ending
WakeLockService.releaseWakeLock();
// Get exam ID
const examId = ExamApi.getExamId();
if (!examId) return;
// Track exam end event
trackExamEndEvent()
.then(() => {
// Make API call to end and evaluate the exam
return ExamApi.evaluateExam(examId);
})
.then(data => {
console.log('Exam evaluation started:', data);
TimerService.stopTimer();
// Show loader for 3 seconds before redirecting
setTimeout(() => {
window.location.href = `/results?id=${examId}`;
}, 3000);
})
.catch(error => {
console.error('Error ending exam:', error);
pageLoader.style.display = 'none';
alert('There was an error ending your exam. Please try again or contact support.');
});
});
// Terminate session button
terminateSessionBtn.addEventListener('click', () => {
terminateModal.show();
});
// Confirm terminate session
confirmTerminateBtn.addEventListener('click', () => {
// Show loader
pageLoader.style.display = 'flex';
terminateModal.hide();
// Release wake lock as session is terminating
WakeLockService.releaseWakeLock();
// Get exam ID
const examId = ExamApi.getExamId();
if (!examId) return;
// Make API call to terminate session
ExamApi.terminateSession(examId)
.then(data => {
console.log('Session terminated successfully:', data);
// Stop timer
TimerService.stopTimer();
// Redirect to main page
window.location.href = '/';
})
.catch(error => {
console.error('Error terminating session:', error);
alert('Failed to terminate session. Please try again or contact support.');
// Hide loader
pageLoader.style.display = 'none';
});
});
// Update question dropdown
function updateQuestionDropdown() {
QuestionService.updateQuestionDropdown(questions, questionDropdownMenu, currentQuestionId, (clickedQuestionId) => {
currentQuestionId = clickedQuestionId;
updateQuestionContent(currentQuestionId);
updateNavigationButtons();
});
}
// Update navigation buttons (disabled state)
function updateNavigationButtons() {
const currentIndex = questions.findIndex(q => q.id === currentQuestionId || q.id === currentQuestionId.toString());
// Disable prev button if on first question
prevBtn.disabled = currentIndex <= 0;
prevBtn.classList.toggle('nav-arrow-disabled', currentIndex <= 0);
// Disable next button if on last question
nextBtn.disabled = currentIndex >= questions.length - 1;
nextBtn.classList.toggle('nav-arrow-disabled', currentIndex >= questions.length - 1);
}
// Function to toggle flag for a question
function toggleQuestionFlag(questionId) {
const questionIndex = questions.findIndex(q => q.id === questionId || q.id === questionId.toString());
if (questionIndex !== -1) {
// Toggle flag
questions[questionIndex].flagged = !questions[questionIndex].flagged;
// Update the UI
updateQuestionContent(questionId);
// Update the dropdown display
updateQuestionDropdown();
}
}
// Function to clean up resources when page is unloaded
function cleanupResources() {
// Release wake lock
WakeLockService.releaseWakeLock();
// Stop timer if running
if (TimerService.isTimerActive) {
TimerService.stopTimer();
}
console.log('Resources cleaned up before page unload');
}
});

77
app/public/js/feedback.js Normal file
View File

@@ -0,0 +1,77 @@
/**
* Feedback module for CK-X Simulator
* Handles displaying feedback prompts and notifications
*/
// Wait for DOM to be loaded
document.addEventListener('DOMContentLoaded', function() {
// Show feedback reminder after a delay
setTimeout(function() {
// Check if results have loaded
const resultsContent = document.getElementById('resultsContent');
if (resultsContent && resultsContent.style.display !== 'none') {
showFeedbackReminder();
} else {
// If results haven't loaded yet, wait for them
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.target.style.display !== 'none') {
showFeedbackReminder();
observer.disconnect();
}
});
});
if (resultsContent) {
observer.observe(resultsContent, {
attributes: true,
attributeFilter: ['style']
});
}
}
}, 10 * 1000); // Show after 10 seconds
});
/**
* Display a toast notification prompting for feedback
*/
function showFeedbackReminder() {
// Create toast element
const toast = document.createElement('div');
toast.className = 'toast-notification';
toast.innerHTML = `
<div class="toast-content">
<i class="fas fa-comment-dots toast-icon"></i>
<div class="toast-message">
<p><strong>Your opinion matters!</strong></p>
<p>Please take a moment to share your feedback on CK-X</p>
</div>
<a href="https://forms.gle/Dac9ALQnQb2dH1mw8" target="_blank" class="toast-button">Give Feedback</a>
<button class="toast-close">&times;</button>
</div>
`;
document.body.appendChild(toast);
// Add close functionality
toast.querySelector('.toast-close').addEventListener('click', function() {
toast.style.animation = 'slideOut 0.5s ease forwards';
setTimeout(function() {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 500);
});
// Auto-close after 15 seconds
setTimeout(function() {
if (document.body.contains(toast)) {
toast.style.animation = 'slideOut 0.5s ease forwards';
setTimeout(function() {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 500);
}
}, 15000);
}

547
app/public/js/index.js Normal file
View File

@@ -0,0 +1,547 @@
document.addEventListener('DOMContentLoaded', function() {
const startExamBtn = document.getElementById('startExamBtn');
const pageLoader = document.getElementById('pageLoader');
const loaderMessage = document.getElementById('loaderMessage');
const examSelectionModal = new bootstrap.Modal(document.getElementById('examSelectionModal'));
// Form elements
const examCategorySelect = document.getElementById('examCategory');
const examNameSelect = document.getElementById('examName');
const examDescription = document.getElementById('examDescription');
const startSelectedExamBtn = document.getElementById('startSelectedExam');
const viewPastResultsBtn = document.getElementById('viewPastResultsBtn');
// Hide View Results button by default - only show when current exam is EVALUATING or EVALUATED
if (viewPastResultsBtn) {
viewPastResultsBtn.closest('li').style.display = 'none';
}
let labs = []; // Will store all labs fetched from the API
let selectedLab = null; // Will store the currently selected lab
// Check for current exam status on page load
checkCurrentExamStatus();
console.log('Loading labs on page load...');
// Load labs data when the page loads
fetchLabs(false);
// Function to check current exam status
function checkCurrentExamStatus() {
fetch('/facilitator/api/v1/exams/current')
.then(response => {
if (!response.ok) {
if (response.status !== 404) {
console.error('Error checking current exam status:', response.status);
}
return null;
}
return response.json();
})
.then(data => {
if (data && data.id) {
// Store current exam data in localStorage for the View Results functionality
localStorage.setItem('currentExamData', JSON.stringify(data));
// Show View Results button only if status is EVALUATING or EVALUATED
if (data.status === 'EVALUATING' || data.status === 'EVALUATED') {
if (viewPastResultsBtn) {
viewPastResultsBtn.closest('li').style.display = 'block';
}
}
}
})
.catch(error => {
console.error('Error checking current exam status:', error);
});
}
// Event listener for the "Start Exam" button
startExamBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log('Checking for active exam sessions...');
// First check if there's any active exam
fetch('/facilitator/api/v1/exams/current')
.then(response => {
if (response.status === 404) {
console.log('No active exam found, proceeding with new exam');
// No active exam, proceed as normal
if (labs.length > 0) {
console.log('Using pre-loaded labs data');
examSelectionModal.show();
} else {
console.log('No pre-loaded labs data available, fetching now...');
fetchLabs(true);
}
return null;
}
if (!response.ok) {
console.error('Error checking current exam status:', response.status);
return null;
}
return response.json();
})
.then(data => {
if (data && data.id) {
console.log('Active exam found:', data.id, 'Status:', data.status);
// Active exam found, show warning modal
showActiveExamWarningModal(data);
}
})
.catch(error => {
console.error('Error checking for active exam:', error);
// Proceed anyway in case of error
if (labs.length > 0) {
examSelectionModal.show();
} else {
fetchLabs(true);
}
});
});
// Function to show warning modal for active exam
function showActiveExamWarningModal(examData) {
// Create modal HTML
const modalHTML = `
<div class="modal fade" id="activeExamWarningModal" tabindex="-1" aria-labelledby="activeExamWarningModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded">
<div class="modal-header bg-dark text-white rounded-top">
<h5 class="modal-title text-white" id="activeExamWarningModalLabel">Active Exam Detected</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<p>You already have an active exam session:</p>
<p><strong>${examData.info?.name || 'Unknown Exam'}</strong></p>
<p class="mb-0">Only one active exam session can be present at a time.</p>
</div>
</div>
<div class="modal-footer rounded-bottom">
<button type="button" class="btn btn-sm btn-primary" id="continueSessionBtn">CONTINUE CURRENT SESSION</button>
<button type="button" class="btn btn-sm btn-danger" id="terminateAndProceedBtn">TERMINATE AND PROCEED</button>
</div>
</div>
</div>
</div>
`;
// Add modal to DOM if it doesn't exist
if (!document.getElementById('activeExamWarningModal')) {
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
// Get modal element and create Bootstrap modal
const modalElement = document.getElementById('activeExamWarningModal');
const warningModal = new bootstrap.Modal(modalElement);
// Show the modal
warningModal.show();
// Remove any existing event listeners by cloning and replacing the buttons
const oldTerminateBtn = document.getElementById('terminateAndProceedBtn');
const newTerminateBtn = oldTerminateBtn.cloneNode(true);
oldTerminateBtn.parentNode.replaceChild(newTerminateBtn, oldTerminateBtn);
const oldContinueBtn = document.getElementById('continueSessionBtn');
const newContinueBtn = oldContinueBtn.cloneNode(true);
oldContinueBtn.parentNode.replaceChild(newContinueBtn, oldContinueBtn);
// Add event listener for continue session button
document.getElementById('continueSessionBtn').addEventListener('click', function() {
// Redirect to the current exam
window.location.href = `/exam.html?id=${examData.id}`;
});
// Add event listener for terminate and proceed button
document.getElementById('terminateAndProceedBtn').addEventListener('click', function() {
// Update button to show progress
const terminateBtn = document.getElementById('terminateAndProceedBtn');
terminateBtn.disabled = true;
terminateBtn.innerHTML = '<div class="d-flex align-items-center justify-content-center"><span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span><span>TERMINATING...</span></div>';
// Show loading overlay
showLoadingOverlay();
updateLoadingMessage('Terminating active session...');
console.log('Attempting to terminate exam:', examData.id);
// Call API to terminate the active exam
fetch(`/facilitator/api/v1/exams/${examData.id}/terminate`, {
method: 'POST'
})
.then(response => {
if (!response.ok) {
console.error('Termination failed with status:', response.status);
throw new Error('Failed to terminate exam. Status: ' + response.status);
}
return response.json();
})
.then((data) => {
console.log('Exam terminated successfully:', examData.id);
// Hide the warning modal
warningModal.hide();
// Clear any stored exam data
localStorage.removeItem('currentExamData');
localStorage.removeItem('currentExamId');
// Proceed with starting a new exam
hideLoadingOverlay();
if (labs.length > 0) {
examSelectionModal.show();
} else {
fetchLabs(true);
}
})
.catch(error => {
console.error('Error terminating exam:', error);
hideLoadingOverlay();
// Reset button state
terminateBtn.disabled = false;
terminateBtn.innerHTML = 'Terminate and Proceed';
alert('Failed to terminate the active exam. Please try again later.');
});
// Clean up the modal when it's hidden
modalElement.addEventListener('hidden.bs.modal', function() {
console.log('Modal hidden, cleaning up event listeners');
// Remove event listeners by replacing buttons with clones if they exist
if (document.getElementById('terminateAndProceedBtn')) {
const oldTerminateBtn = document.getElementById('terminateAndProceedBtn');
const newTerminateBtn = oldTerminateBtn.cloneNode(true);
oldTerminateBtn.parentNode.replaceChild(newTerminateBtn, oldTerminateBtn);
}
if (document.getElementById('continueSessionBtn')) {
const oldContinueBtn = document.getElementById('continueSessionBtn');
const newContinueBtn = oldContinueBtn.cloneNode(true);
oldContinueBtn.parentNode.replaceChild(newContinueBtn, oldContinueBtn);
}
});
});
}
// Fetch labs from the facilitator API
function fetchLabs(showLoader = true) {
console.log('Fetching labs, showLoader:', showLoader);
if (showLoader) {
pageLoader.style.display = 'flex';
loaderMessage.textContent = 'Loading labs...';
}
fetch('/facilitator/api/v1/assements/')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch labs. Status: ' + response.status);
}
return response.json();
})
.then(data => {
labs = data;
console.log('Labs loaded successfully, count:', labs.length);
if (showLoader) {
pageLoader.style.display = 'none';
examSelectionModal.show();
}
populateLabCategories();
})
.catch(error => {
console.error('Error fetching labs:', error);
if (showLoader) {
pageLoader.style.display = 'none';
alert('Failed to load labs. Please try again later.');
}
});
}
// Populate the lab categories dropdown
function populateLabCategories() {
// Get unique categories
const categories = [...new Set(labs.map(lab => lab.category))];
// If CKAD is available, select it by default
if (categories.includes('CKAD')) {
examCategorySelect.value = 'CKAD';
filterLabsByCategory('CKAD');
} else if (categories.length > 0) {
examCategorySelect.value = categories[0];
filterLabsByCategory(categories[0]);
}
}
// Filter labs by category and populate the labs dropdown
function filterLabsByCategory(category) {
const filteredLabs = labs.filter(lab => lab.category === category);
// Clear existing options
examNameSelect.innerHTML = '<option value="">Select a lab</option>';
// Add filtered labs to the dropdown
filteredLabs.forEach(lab => {
const option = document.createElement('option');
option.value = lab.id;
option.textContent = lab.name;
examNameSelect.appendChild(option);
});
// Enable the lab name select
examNameSelect.disabled = false;
// If there are labs in this category, select the first one
if (filteredLabs.length > 0) {
examNameSelect.value = filteredLabs[0].id;
updateLabDescription(filteredLabs[0]);
} else {
examDescription.textContent = 'No labs available for this category.';
startSelectedExamBtn.disabled = true;
}
}
// Update the lab description when a lab is selected
function updateLabDescription(lab) {
// Create a nicely formatted description
const difficultyText = lab.difficulty || 'Medium';
const examTimeText = lab.examDurationInMinutes || lab.estimatedTime || '30';
const descriptionHTML = `
<div class="exam-details">
<p class="mb-0">${lab.description || 'No description available.'}</p>
</div>
<div class="exam-meta-container mt-3 pt-2 border-top">
<div class="d-flex justify-content-start align-items-center">
<div class="exam-meta me-4">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart-fill me-1" viewBox="0 0 16 16">
<path d="M1 11a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3zm5-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm5-5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V2z"/>
</svg>
Difficulty: ${difficultyText}
</div>
<div class="exam-meta">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clock me-1" viewBox="0 0 16 16">
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
</svg>
Exam Time: ${examTimeText} minutes
</div>
</div>
</div>
`;
// No need to add styles dynamically anymore since they're in the CSS file
// Use innerHTML to render the HTML content
examDescription.innerHTML = descriptionHTML;
selectedLab = lab;
startSelectedExamBtn.disabled = false;
}
// Event listener for the exam category select
examCategorySelect.addEventListener('change', function() {
filterLabsByCategory(this.value);
});
// Event listener for the exam name select
examNameSelect.addEventListener('change', function() {
if (this.value) {
const lab = labs.find(lab => lab.id === this.value);
if (lab) {
updateLabDescription(lab);
}
} else {
examDescription.textContent = 'No lab selected.';
selectedLab = null;
startSelectedExamBtn.disabled = true;
}
});
// Event listener for the start selected exam button
startSelectedExamBtn.addEventListener('click', function() {
if (selectedLab) {
examSelectionModal.hide();
showLoadingOverlay(); // Show the loading overlay instead of pageLoader
updateLoadingMessage('Starting lab environment...');
updateExamInfo(`Lab: ${selectedLab.name} | Difficulty: ${selectedLab.difficulty || 'Medium'}`);
// Make a POST request to the facilitator API - using exams endpoint for POST
fetch('/facilitator/api/v1/exams/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(selectedLab)
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to start lab. Status: ' + response.status);
}
return response.json();
})
.then(data => {
// Store exam ID in localStorage
localStorage.setItem('currentExamId', data.id);
// Start polling for status
const warmUpTime = data.warmUpTimeInSeconds || 30;
updateLoadingMessage(`Preparing your lab environment (${warmUpTime}s estimated)`);
// Poll for exam status until it's ready
return pollExamStatus(data.id);
})
.then(() => {
// Redirect to the lab page after status is READY
const examId = localStorage.getItem('currentExamId');
window.location.href = `/exam.html?id=${examId}`;
})
.catch(error => {
console.error('Error starting lab:', error);
hideLoadingOverlay();
alert('Failed to start the lab. Please try again later.');
});
}
});
// Add new functions for exam status handling
function showLoadingOverlay() {
document.getElementById('loadingOverlay').style.display = 'flex';
}
function hideLoadingOverlay() {
document.getElementById('loadingOverlay').style.display = 'none';
}
function updateProgressBar(progress) {
document.getElementById('progressBar').style.width = `${progress}%`;
}
function updateLoadingMessage(message) {
document.getElementById('loadingMessage').textContent = message;
}
function updateExamInfo(info) {
document.getElementById('examInfo').textContent = info;
}
async function pollExamStatus(examId) {
const startTime = Date.now();
const pollInterval = 1000; // Poll every 5 seconds
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const response = await fetch(`/facilitator/api/v1/exams/${examId}/status`);
const data = await response.json();
// set warmup time in seconds
const warmUpTimeInSeconds = data.warmUpTimeInSeconds || 30;
if (data.status === 'READY') {
// Set progress to 100% when ready
updateProgressBar(100);
updateLoadingMessage('Lab environment is ready! Redirecting...');
// Wait a moment for the user to see the 100% progress
setTimeout(() => resolve(data), 1000);
return;
}
// Calculate progress based on warm-up time
const elapsedTime = (Date.now() - startTime) / 1000;
const progress = Math.min((elapsedTime / warmUpTimeInSeconds) * 100, 95);
updateProgressBar(progress);
updateLoadingMessage(data.message || 'Preparing lab environment...');
// Continue polling
setTimeout(poll, pollInterval);
} catch (error) {
console.error('Error polling exam status:', error);
// Show error in the loading overlay
updateLoadingMessage(`Error: ${error.message}. Retrying...`);
// Continue polling despite errors
setTimeout(poll, pollInterval);
}
};
poll();
});
}
// Modify the existing startExam function
async function startExam(examId) {
try {
showLoadingOverlay();
updateLoadingMessage('Starting exam environment...');
const response = await fetch('/facilitator/api/v1/exams', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ examId })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to start exam');
}
// Store exam ID in localStorage
localStorage.setItem('currentExamId', data.id);
// Start polling for status
await pollExamStatus(data.id, data.warmUpTimeInSeconds || 30);
// Redirect to exam page when ready
window.location.href = `/exam.html?id=${data.id}`;
} catch (error) {
console.error('Error starting exam:', error);
hideLoadingOverlay();
alert('Failed to start exam: ' + error.message);
}
}
// Modify the dropdown population to include difficulty info
function populateDropdown(labs) {
const dropdown = document.getElementById('examDropdown');
dropdown.innerHTML = '';
labs.forEach(lab => {
const option = document.createElement('option');
option.value = lab.id;
option.textContent = `${lab.name} (${lab.difficulty || 'Medium'})`;
option.title = `${lab.description}\nDifficulty: ${lab.difficulty || 'Medium'}\nEstimated Time: ${lab.estimatedTime || '30'} minutes`;
dropdown.appendChild(option);
});
}
// Add event listener for View Past Results button
viewPastResultsBtn.addEventListener('click', function() {
// Check if we have current exam data
const currentExamDataStr = localStorage.getItem('currentExamData');
if (currentExamDataStr) {
try {
const currentExamData = JSON.parse(currentExamDataStr);
// If the current exam is evaluated or being evaluated, go directly to results
if (currentExamData.status === 'EVALUATED' || currentExamData.status === 'EVALUATING') {
window.location.href = `/results?id=${currentExamData.id}`;
return;
} else {
// If the exam exists but isn't in the right state, show an alert
alert('Exam results are not available yet. The exam must be evaluated first.');
return;
}
} catch (error) {
console.error('Error parsing current exam data:', error);
}
}
// If there's no current exam at all, inform the user
alert('No active exam found. Please start an exam first.');
});
});

View File

@@ -0,0 +1,151 @@
/**
* Panel Resizer - Handles resizing of panels by dragging
*/
class PanelResizer {
constructor(options) {
this.divider = document.getElementById(options.dividerId);
this.leftPanel = document.getElementById(options.leftPanelId);
this.rightPanel = document.getElementById(options.rightPanelId);
this.container = document.getElementById(options.containerId) || this.leftPanel.parentElement;
this.minLeftWidth = options.minLeftWidth || 200;
this.minRightWidth = options.minRightWidth || 200;
this.isDragging = false;
this.startX = 0;
this.startLeftWidth = 0;
// Store initial panel width in local storage if it exists
this.storageKey = options.storageKey || 'panelWidth';
// Add debug option
this.debug = options.debug || false;
this.init();
}
init() {
if (this.debug) console.log('Initializing panel resizer');
// Try to restore previous panel width from localStorage
const savedWidth = localStorage.getItem(this.storageKey);
if (savedWidth) {
this.leftPanel.style.width = savedWidth;
if (this.debug) console.log('Restored width from localStorage:', savedWidth);
}
// Mouse events for desktop
this.divider.addEventListener('mousedown', this.startDrag.bind(this));
window.addEventListener('mousemove', this.drag.bind(this));
window.addEventListener('mouseup', this.stopDrag.bind(this));
// Touch events for mobile
this.divider.addEventListener('touchstart', this.startDrag.bind(this));
window.addEventListener('touchmove', this.drag.bind(this));
window.addEventListener('touchend', this.stopDrag.bind(this));
// Double click to reset
this.divider.addEventListener('dblclick', this.resetPanels.bind(this));
}
startDrag(e) {
// Exit if the divider doesn't exist
if (!this.divider) return;
this.isDragging = true;
this.divider.classList.add('dragging');
// Get the starting horizontal position
this.startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
this.startLeftWidth = this.leftPanel.offsetWidth;
// Prevent text selection during drag
document.body.classList.add('no-select');
if (this.debug) {
console.log('Drag started:', {
startX: this.startX,
startLeftWidth: this.startLeftWidth
});
}
// Prevent default behavior
e.preventDefault();
}
drag(e) {
if (!this.isDragging) return;
// Calculate the new width based on mouse/touch position
const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
const deltaX = clientX - this.startX;
// Calculate container width
const containerWidth = this.container.offsetWidth;
// Calculate new widths
let newLeftWidth = this.startLeftWidth + deltaX;
// Apply min width constraints
if (newLeftWidth < this.minLeftWidth) {
newLeftWidth = this.minLeftWidth;
} else if (containerWidth - newLeftWidth < this.minRightWidth) {
newLeftWidth = containerWidth - this.minRightWidth;
}
if (this.debug) {
console.log('Dragging:', {
clientX: clientX,
deltaX: deltaX,
containerWidth: containerWidth,
newLeftWidth: newLeftWidth
});
}
// Apply the new width
this.leftPanel.style.width = `${newLeftWidth}px`;
// Prevent default behavior to avoid text selection
e.preventDefault();
e.stopPropagation();
}
stopDrag() {
if (!this.isDragging) return;
this.isDragging = false;
this.divider.classList.remove('dragging');
document.body.classList.remove('no-select');
// Save the current width to localStorage
localStorage.setItem(this.storageKey, this.leftPanel.style.width);
if (this.debug) {
console.log('Drag stopped. New width:', this.leftPanel.style.width);
}
}
resetPanels() {
// Reset to default width (30%)
this.leftPanel.style.width = '30%';
localStorage.setItem(this.storageKey, '30%');
if (this.debug) {
console.log('Panels reset to default width');
}
}
}
// Initialize the panel resizer when the DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Create a global instance of PanelResizer
window.panelResizer = new PanelResizer({
dividerId: 'panelDivider',
leftPanelId: 'questionPanel',
rightPanelId: 'vncPanel',
containerId: 'mainContainer',
minLeftWidth: 200, // Minimum width for question panel
minRightWidth: 300, // Minimum width for VNC panel
storageKey: 'examPanelWidth',
debug: true // Enable debug for troubleshooting
});
});

465
app/public/js/results.js Normal file
View File

@@ -0,0 +1,465 @@
document.addEventListener('DOMContentLoaded', function() {
// DOM elements
const pageLoader = document.getElementById('pageLoader');
const errorMessage = document.getElementById('errorMessage');
const errorText = document.getElementById('errorText');
const retryButton = document.getElementById('retryButton');
const resultsContent = document.getElementById('resultsContent');
const examIdElement = document.getElementById('examId');
const completedAtElement = document.getElementById('completedAt');
const totalScoreElement = document.getElementById('totalScore');
const totalPossibleScoreElement = document.getElementById('totalPossibleScore');
const rankTextElement = document.getElementById('rankText');
const rankBadgeElement = document.getElementById('rankBadge');
const questionsContainer = document.getElementById('questionsContainer');
const dashboardBtn = document.getElementById('dashboardBtn');
const reEvaluateBtn = document.getElementById('reEvaluateBtn');
const currentExamBtn = document.getElementById('currentExamBtn');
const terminateBtn = document.getElementById('terminateBtn');
const viewAnswersBtn = document.getElementById('viewAnswersBtn');
// Configuration for polling
const POLLING_INTERVAL = 2000; // 5 seconds
const MAX_POLLING_TIME = 100000; // 10 minutes (600 seconds)
let pollingStartTime = 0;
let pollingTimer = null;
let currentExamId = null; // Store the current exam ID
// Add DOM elements for modal
const terminateModal = document.getElementById('terminateModal');
const closeModalBtn = document.getElementById('closeModalBtn');
const cancelTerminateBtn = document.getElementById('cancelTerminateBtn');
const confirmTerminateBtn = document.getElementById('confirmTerminateBtn');
// Add event listeners for action buttons
dashboardBtn.addEventListener('click', function() {
window.location.href = '/';
});
currentExamBtn.addEventListener('click', () => {
if (currentExamId) {
window.location.href = `/exam.html?id=${currentExamId}`;
} else {
showError('No exam ID available for redirection.');
}
});
viewAnswersBtn.addEventListener('click', () => {
if (currentExamId) {
window.location.href = `/answers.html?id=${currentExamId}`;
} else {
showError('No exam ID available for viewing answers.');
}
});
reEvaluateBtn.addEventListener('click', function() {
if (currentExamId) {
// Disable the button while re-evaluating
reEvaluateBtn.disabled = true;
reEvaluateBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Evaluating...';
// Call the API to re-evaluate the exam
initiateReEvaluation(currentExamId);
} else {
showError('No exam ID available for re-evaluation.');
}
});
terminateBtn.addEventListener('click', function() {
if (!currentExamId) {
showError('No exam ID available for termination.');
return;
}
// Show modal instead of confirm dialog
terminateModal.style.display = 'flex';
});
// Close modal when clicking the close button
closeModalBtn.addEventListener('click', function() {
terminateModal.style.display = 'none';
});
// Close modal when clicking the cancel button
cancelTerminateBtn.addEventListener('click', function() {
terminateModal.style.display = 'none';
});
// Handle confirm termination
confirmTerminateBtn.addEventListener('click', function() {
// Disable the button while terminating
confirmTerminateBtn.disabled = true;
confirmTerminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i> Terminating...';
// Call the API to terminate the session
fetch(`/facilitator/api/v1/exams/${currentExamId}/terminate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Session terminated:', data);
// Redirect to dashboard after successful termination
window.location.href = '/';
})
.catch(error => {
console.error('Error terminating session:', error);
confirmTerminateBtn.disabled = false;
confirmTerminateBtn.innerHTML = '<i class="fas fa-power-off me-2"></i>Terminate Session';
terminateModal.style.display = 'none';
showError('Failed to terminate session: ' + error.message);
});
});
// Close modal when clicking outside of it
window.addEventListener('click', function(event) {
if (event.target === terminateModal) {
terminateModal.style.display = 'none';
}
});
// Function to initiate re-evaluation
function initiateReEvaluation(examId) {
// Hide results and show loading
resultsContent.style.display = 'none';
pageLoader.style.display = 'flex';
// Update loader message
updateLoaderMessage('Re-evaluation in progress...');
fetch(`/facilitator/api/v1/exams/${examId}/evaluate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Re-evaluation started:', data);
// Start polling for results
startPolling(examId);
})
.catch(error => {
console.error('Error starting re-evaluation:', error);
reEvaluateBtn.disabled = false;
reEvaluateBtn.innerHTML = '<i class="fas fa-sync-alt me-2"></i>Re-evaluate Exam';
showError('Failed to start re-evaluation: ' + error.message);
});
}
// Get exam ID from URL
function getExamId() {
const urlParams = new URLSearchParams(window.location.search);
const examId = urlParams.get('id');
if (!examId) {
showError('No exam ID provided. Please return to the dashboard.');
return null;
}
currentExamId = examId; // Store the exam ID for later use
return examId;
}
// Format date for display
function formatDate(dateString) {
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
return new Date(dateString).toLocaleDateString(undefined, options);
}
// Show error message
function showError(message) {
pageLoader.style.display = 'none';
errorText.textContent = message;
errorMessage.style.display = 'block';
resultsContent.style.display = 'none';
}
// Update loader message
function updateLoaderMessage(message) {
const loaderMessage = document.getElementById('loaderMessage');
if (loaderMessage) {
loaderMessage.textContent = message;
}
}
// Start polling for exam results
function startPolling(examId) {
// Save the start time for tracking elapsed time
pollingStartTime = Date.now();
// Update UI to show evaluation in progress
updateLoaderMessage('Evaluation in progress... (typically takes 2-3 minutes)');
// Start polling for status changes
pollingTimer = setInterval(() => {
checkExamStatus(examId);
}, POLLING_INTERVAL);
}
// Stop polling
function stopPolling() {
if (pollingTimer) {
clearInterval(pollingTimer);
pollingTimer = null;
}
}
// Update progress indicator
function updateProgressIndicator(elapsedSeconds) {
// Simplified - just show static message
updateLoaderMessage('Evaluation in progress... (typically takes 2-3 minutes)');
}
// Check exam status (for polling)
function checkExamStatus(examId) {
// Check if we've exceeded maximum polling time
if (Date.now() - pollingStartTime > MAX_POLLING_TIME) {
stopPolling();
showError('Exam evaluation is taking longer than expected. Please try again later.');
return;
}
// Calculate elapsed time for tracking only (not displayed)
const elapsedSeconds = Math.floor((Date.now() - pollingStartTime) / 1000);
// Check if exam status and results are available
fetch(`/facilitator/api/v1/exams/${examId}/status`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
// If evaluation is complete, fetch and display results
if (data.status === 'EVALUATED') {
stopPolling();
fetchExamResults(examId);
} else if (data.status === 'EVALUATING') {
// Keep showing static message without progress updates
updateLoaderMessage('Evaluation in progress... (typically takes 2-3 minutes)');
} else if (data.status === 'EVALUATION_FAILED') {
stopPolling();
showError('Exam evaluation failed. Please contact support.');
} else {
// For any other status, show appropriate message
updateLoaderMessage(`Waiting for evaluation to start... Current status: ${data.status}`);
}
})
.catch(error => {
console.error('Error checking exam status:', error);
// Don't stop polling on network errors - will retry on next interval
});
}
// Fetch exam results from API
function fetchExamResults(examId = null) {
// If no exam ID provided, get it from the URL
if (!examId) {
examId = getExamId();
if (!examId) {
showError('No exam ID provided. Please return to the dashboard and try again.');
return;
}
}
// Store current exam ID for later use
currentExamId = examId;
// Show loader
pageLoader.style.display = 'flex';
updateLoaderMessage('Loading exam results...');
errorMessage.style.display = 'none';
resultsContent.style.display = 'none';
const apiUrl = `/facilitator/api/v1/exams/${examId}/result`;
fetch(apiUrl)
.then(response => {
if (!response.ok) {
if (response.status === 404) {
// If results not found, check status to see if we need to start polling
return checkResultsStatus(examId);
}
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data && data.data) {
renderExamResults(data.data);
// Reset the Re-evaluate button state if it was disabled
if (reEvaluateBtn.disabled) {
reEvaluateBtn.disabled = false;
reEvaluateBtn.innerHTML = '<i class="fas fa-sync-alt me-2"></i>Re-evaluate Exam';
}
} else if (data && data.status === 'polling_started') {
// This is a special status returned by checkResultsStatus
// Polling has been started, so we just wait
} else {
throw new Error('Invalid response format');
}
})
.catch(error => {
console.error('Error loading exam results:', error);
showError(error.message || 'Failed to load exam results');
// Reset the Re-evaluate button state if it was disabled
if (reEvaluateBtn.disabled) {
reEvaluateBtn.disabled = false;
reEvaluateBtn.innerHTML = '<i class="fas fa-sync-alt me-2"></i>Re-evaluate Exam';
}
});
}
// Check if we need to poll for results
function checkResultsStatus(examId) {
return fetch(`/facilitator/api/v1/exams/${examId}/status`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.status === 'EVALUATING') {
// Start polling if exam is being evaluated
startPolling(examId);
return { status: 'polling_started' };
} else if (data.status === 'EVALUATION_FAILED') {
throw new Error('Exam evaluation failed');
} else if (data.status !== 'EVALUATED') {
throw new Error(`Exam results not available. Current status: ${data.status}`);
} else {
// If status is EVALUATED but no results found, there might be a delay
// Start polling anyway in case results are being finalized
startPolling(examId);
return { status: 'polling_started' };
}
});
}
// Render exam results
function renderExamResults(results) {
// Update basic info
examIdElement.textContent = `Exam ID: ${results.examId}`;
completedAtElement.textContent = `Completed: ${formatDate(results.completedAt)}`;
// Update score
totalScoreElement.textContent = results.totalScore;
totalPossibleScoreElement.textContent = results.totalPossibleScore;
// Update rank
rankTextElement.textContent = results.rank === 'high' ? 'High Score' :
results.rank === 'medium' ? 'Medium Score' : 'Low Score';
// Apply rank class
rankBadgeElement.className = 'rank-badge';
rankBadgeElement.classList.add(`rank-${results.rank}`);
// Clear questions container
questionsContainer.innerHTML = '';
// Add questions and verifications
if (results.evaluationResults && results.evaluationResults.length > 0) {
// Sort questions by ID (convert to number for proper numeric sorting)
const sortedQuestions = [...results.evaluationResults].sort((a, b) => {
const idA = parseInt(a.id);
const idB = parseInt(b.id);
return idA - idB;
});
sortedQuestions.forEach(question => {
// Sort verification steps by ID
const sortedVerifications = [...question.verificationResults].sort((a, b) => {
const idA = parseInt(a.id);
const idB = parseInt(b.id);
return idA - idB;
});
// Calculate question score
const questionScore = sortedVerifications.reduce((total, v) => total + v.score, 0);
const questionPossibleScore = sortedVerifications.reduce((total, v) => total + v.weightage, 0);
// Create question card
const questionCard = document.createElement('div');
questionCard.className = 'question-card';
// Create question header
const questionHeader = document.createElement('div');
questionHeader.className = 'question-header';
questionHeader.innerHTML = `
<h3 class="question-title">Question ${question.id}</h3>
<div class="question-score">${questionScore} of ${questionPossibleScore}</div>
`;
// Create verification list
const verificationList = document.createElement('ul');
verificationList.className = 'verification-items';
// Add verification items
sortedVerifications.forEach(verification => {
const item = document.createElement('li');
item.className = 'verification-item';
item.innerHTML = `
<div class="verification-description">${verification.description}</div>
<div class="verification-status ${verification.validAnswer ? 'status-success' : 'status-failure'}">
${verification.validAnswer ?
'<i class="fas fa-check-circle"></i>' :
'<i class="fas fa-times-circle"></i>'}
</div>
`;
verificationList.appendChild(item);
});
// Assemble question card
questionCard.appendChild(questionHeader);
questionCard.appendChild(verificationList);
// Add to container
questionsContainer.appendChild(questionCard);
});
} else {
questionsContainer.innerHTML = '<p>No evaluation results available</p>';
}
// Hide loader and show content
pageLoader.style.display = 'none';
resultsContent.style.display = 'block';
}
// Add retry button event listener
retryButton.addEventListener('click', fetchExamResults);
// Start by fetching exam results
fetchExamResults();
// Clean up when leaving the page
window.addEventListener('beforeunload', () => {
stopPolling();
});
});

116
app/public/results.html Normal file
View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exam Results - CK-X Simulator</title>
<link rel="stylesheet" href="/css/styles.css">
<link rel="stylesheet" href="/css/results.css">
<link rel="stylesheet" href="/css/feedback.css">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container">
<!-- Action buttons moved to top -->
<div class="results-actions">
<button id="dashboardBtn" class="btn btn-secondary">
<i class="fas fa-home me-2"></i>Dashboard
</button>
<button id="currentExamBtn" class="btn btn-info">
<i class="fas fa-tasks me-2"></i>Current Exam
</button>
<button id="viewAnswersBtn" class="btn btn-success">
<i class="fas fa-check-square me-2"></i>View Answers
</button>
<button id="reEvaluateBtn" class="btn btn-primary">
<i class="fas fa-sync-alt me-2"></i>Re-evaluate Exam
</button>
<button id="terminateBtn" class="btn btn-danger">
<i class="fas fa-power-off me-2"></i>Terminate Session
</button>
<a href="https://forms.gle/Dac9ALQnQb2dH1mw8" target="_blank" class="btn btn-outline-primary" id="feedbackBtn">
<i class="fas fa-comment-dots me-2"></i>Give Feedback
</a>
</div>
<main class="results-container">
<!-- Loading state -->
<div id="pageLoader" class="page-loader">
<div class="spinner"></div>
<p id="loaderMessage">Loading exam results...</p>
</div>
<!-- Error message -->
<div id="errorMessage" class="error-message" style="display: none;">
<i class="fas fa-exclamation-circle"></i>
<p id="errorText">An error occurred while loading the results.</p>
<button id="retryButton" class="button">Retry</button>
</div>
<!-- Results content -->
<div id="resultsContent" class="results-content" style="display: none;">
<div class="results-header">
<h2>Exam Results</h2>
<div class="exam-info">
<p id="examId">Exam ID: Loading...</p>
<p id="completedAt">Completed: Loading...</p>
</div>
</div>
<div class="results-summary">
<div class="score-display">
<h3>Solved subtasks:</h3>
<div class="score-box">
<span id="totalScore">0</span> / <span id="totalPossibleScore">0</span>
</div>
</div>
<div class="rank-display">
<div id="rankBadge" class="rank-badge">
<span id="rankText">Score</span>
</div>
</div>
</div>
<div id="questionsContainer" class="questions-container">
<!-- Questions will be added here dynamically -->
</div>
</div>
</main>
<footer class="app-footer">
<p style="text-align: center;">&copy; <span id="currentYear"></span> CK-X Simulator.</p>
</footer>
</div>
<script src="/js/results.js"></script>
<script src="/js/feedback.js"></script>
<script>
// Set the current year for copyright
document.getElementById('currentYear').textContent = new Date().getFullYear();
</script>
<!-- Terminate Session Confirmation Modal -->
<div id="terminateModal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h4>Terminate Session</h4>
<button id="closeModalBtn" class="modal-close-btn">&times;</button>
</div>
<div class="modal-body">
<p>Are you sure you want to terminate this exam session?</p>
<p>This will <strong>clear all your progress</strong> and your <strong>environment will be cleaned up</strong> immediately.</p>
<p class="warning-text">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button id="cancelTerminateBtn" class="btn btn-secondary">Cancel</button>
<button id="confirmTerminateBtn" class="btn btn-danger">
<i class="fas fa-power-off me-2"></i> Terminate Session
</button>
</div>
</div>
</div>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="nishan.b" data-description="Support me on Buy me a coffee!" data-message="CK-X helped you prep? A coffee helps it grow !!" data-color="#5F7FFF" data-position="Right" data-x_margin="18" data-y_margin="18"></script>
</body>
</html>

24
app/routes/api.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* API Routes module
* Defines all API endpoints for the application
*/
const express = require('express');
const router = express.Router();
const config = require('../config/config');
/**
* GET /api/vnc-info
* Returns information about the VNC server
*/
router.get('/vnc-info', (req, res) => {
res.json({
host: config.VNC_SERVICE_HOST,
port: config.VNC_SERVICE_PORT,
wsUrl: `/websockify`,
defaultPassword: config.VNC_PASSWORD,
status: 'connected'
});
});
module.exports = router;

238
app/server.js Normal file
View File

@@ -0,0 +1,238 @@
const express = require('express');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const { createProxyMiddleware } = require('http-proxy-middleware');
const http = require('http');
const socketio = require('socket.io');
const { Client } = require('ssh2');
// Server configuration
const PORT = process.env.PORT || 3000;
// VNC service configuration from environment variables
const VNC_SERVICE_HOST = process.env.VNC_SERVICE_HOST || 'remote-desktop-service';
const VNC_SERVICE_PORT = process.env.VNC_SERVICE_PORT || 6901;
const VNC_PASSWORD = process.env.VNC_PASSWORD || 'bakku-the-wizard'; // Default password
// SSH service configuration
const SSH_HOST = process.env.SSH_HOST || 'remote-terminal'; // Use remote-terminal service
const SSH_PORT = process.env.SSH_PORT || 22;
const SSH_USER = process.env.SSH_USER || 'candidate';
const SSH_PASSWORD = process.env.SSH_PASSWORD || 'password';
const app = express();
const server = http.createServer(app);
const io = socketio(server);
// SSH terminal namespace
const sshIO = io.of('/ssh');
// Handle SSH connections
sshIO.on('connection', (socket) => {
console.log('New SSH terminal connection established');
let ssh = new Client();
// Connect to the SSH server
ssh.on('ready', () => {
console.log('SSH connection established');
// Create shell session
ssh.shell((err, stream) => {
if (err) {
console.error('SSH shell error:', err);
socket.emit('data', `Error: ${err.message}\r\n`);
socket.disconnect();
return;
}
// Handle incoming data from SSH server
stream.on('data', (data) => {
socket.emit('data', data.toString('utf-8'));
});
// Handle errors on stream
stream.on('close', () => {
console.log('SSH stream closed');
ssh.end();
socket.disconnect();
});
stream.on('error', (err) => {
console.error('SSH stream error:', err);
socket.emit('data', `Error: ${err.message}\r\n`);
});
// Handle incoming data from browser
socket.on('data', (data) => {
stream.write(data);
});
// Handle resize events
socket.on('resize', (dimensions) => {
if (dimensions && dimensions.cols && dimensions.rows) {
stream.setWindow(dimensions.rows, dimensions.cols, 0, 0);
}
});
// Handle socket disconnection
socket.on('disconnect', () => {
console.log('Client disconnected from SSH terminal');
stream.close();
ssh.end();
});
});
});
// Handle SSH connection errors
ssh.on('error', (err) => {
console.error('SSH connection error:', err);
socket.emit('data', `SSH connection error: ${err.message}\r\n`);
socket.disconnect();
});
// Connect to SSH server
ssh.connect({
host: SSH_HOST,
port: SSH_PORT,
username: SSH_USER,
password: SSH_PASSWORD,
readyTimeout: 30000,
keepaliveInterval: 10000
});
});
// Create the public directory if it doesn't exist
const publicDir = path.join(__dirname, 'public');
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
console.log('Created public directory');
}
// Copy index.html to public directory if it doesn't exist
const indexHtmlSrc = path.join(__dirname, 'index.html');
const indexHtmlDest = path.join(publicDir, 'index.html');
if (fs.existsSync(indexHtmlSrc) && !fs.existsSync(indexHtmlDest)) {
fs.copyFileSync(indexHtmlSrc, indexHtmlDest);
console.log('Copied index.html to public directory');
}
// Enable CORS
app.use(cors());
// Serve static files from the public directory
app.use(express.static(path.join(__dirname, 'public')));
// Configure VNC proxy middleware
const vncProxyConfig = {
target: `http://${VNC_SERVICE_HOST}:${VNC_SERVICE_PORT}`,
changeOrigin: true,
ws: true,
secure: false,
pathRewrite: {
'^/vnc-proxy': ''
},
onProxyReq: (proxyReq, req, res) => {
// Log HTTP requests being proxied
console.log(`Proxying HTTP request to VNC server: ${req.url}`);
},
onProxyReqWs: (proxyReq, req, socket, options, head) => {
// Log WebSocket connections
console.log(`WebSocket connection established to VNC server: ${req.url}`);
},
onProxyRes: (proxyRes, req, res) => {
// Log the responses from VNC server
console.log(`Received response from VNC server for: ${req.url}`);
},
onError: (err, req, res) => {
console.error(`Proxy error: ${err.message}`);
if (res && res.writeHead) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end(`Proxy error: ${err.message}`);
}
}
};
// Middleware to enhance VNC URLs with authentication if needed
app.use('/vnc-proxy', (req, res, next) => {
// Check if the URL already has a password parameter
if (!req.query.password) {
// If no password provided, add default password
console.log('Adding default VNC password to request');
const separator = req.url.includes('?') ? '&' : '?';
req.url = `${req.url}${separator}password=${VNC_PASSWORD}`;
}
next();
}, createProxyMiddleware(vncProxyConfig));
// Direct WebSocket proxy to handle the websockify endpoint
app.use('/websockify', createProxyMiddleware({
...vncProxyConfig,
pathRewrite: {
'^/websockify': '/websockify'
},
ws: true,
onProxyReqWs: (proxyReq, req, socket, options, head) => {
// Log WebSocket connections to websockify
console.log(`WebSocket connection to websockify established: ${req.url}`);
// Add additional headers if needed
proxyReq.setHeader('Origin', `http://${VNC_SERVICE_HOST}:${VNC_SERVICE_PORT}`);
},
onError: (err, req, res) => {
console.error(`Websockify proxy error: ${err.message}`);
if (res && res.writeHead) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end(`Websockify proxy error: ${err.message}`);
}
}
}));
// API endpoint to get VNC server info
app.get('/api/vnc-info', (req, res) => {
res.json({
host: VNC_SERVICE_HOST,
port: VNC_SERVICE_PORT,
wsUrl: `/websockify`,
defaultPassword: VNC_PASSWORD,
status: 'connected'
});
});
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', message: 'Service is healthy' });
});
// Catch-all route to serve index.html for any other requests
app.get('*', (req, res) => {
// Special handling for exam page
if (req.path === '/exam') {
res.sendFile(path.join(__dirname, 'public', 'exam.html'));
}
// Special handling for results page
else if (req.path === '/results') {
res.sendFile(path.join(__dirname, 'public', 'results.html'));
}
else {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
}
});
// Handle errors
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).sendFile(path.join(__dirname, 'public', '50x.html'));
});
// Start the server
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`VNC proxy configured to ${VNC_SERVICE_HOST}:${VNC_SERVICE_PORT}`);
console.log(`SSH service configured to ${SSH_HOST}:${SSH_PORT}`);
});

31
app/utils/staticFiles.js Normal file
View File

@@ -0,0 +1,31 @@
/**
* Static files utility
* Handles the setup of necessary directories and files
*/
const fs = require('fs');
const path = require('path');
/**
* Sets up the necessary static file structure
* Creates public directory if it doesn't exist
* Copies index.html to public directory if needed
*/
function setupStaticFiles() {
// Create the public directory if it doesn't exist
const publicDir = path.join(__dirname, '..', 'public');
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
console.log('Created public directory');
}
// Copy index.html to public directory if it doesn't exist
const indexHtmlSrc = path.join(__dirname, '..', 'index.html');
const indexHtmlDest = path.join(publicDir, 'index.html');
if (fs.existsSync(indexHtmlSrc) && !fs.existsSync(indexHtmlDest)) {
fs.copyFileSync(indexHtmlSrc, indexHtmlDest);
console.log('Copied index.html to public directory');
}
}
module.exports = setupStaticFiles;

235
compose-deploy.sh Executable file
View File

@@ -0,0 +1,235 @@
#!/usr/bin/env bash
# ===============================================================================
#
# ██████╗██╗ ██╗ █████╗ ██████╗ ███████╗██╗███╗ ███╗██╗ ██╗██╗ █████╗ ████████╗ ██████╗ ██████╗
# ██╔════╝██║ ██╔╝██╔══██╗██╔══██╗ ██╔════╝██║████╗ ████║██║ ██║██║ ██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
# ██║ █████╔╝ ███████║██║ ██║ ███████╗██║██╔████╔██║██║ ██║██║ ███████║ ██║ ██║ ██║██████╔╝
# ██║ ██╔═██╗ ██╔══██║██║ ██║ ╚════██║██║██║╚██╔╝██║██║ ██║██║ ██╔══██║ ██║ ██║ ██║██╔══██╗
# ╚██████╗██║ ██╗██║ ██║██████╔╝ ███████║██║██║ ╚═╝ ██║╚██████╔╝███████╗██║ ██║ ██║ ╚██████╔╝██║ ██║
# ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
#
# ===============================================================================
# Docker Compose CKAD Deployment Script
# Version: 1.0.0
# Author: Nishan B
# ===============================================================================
set -e
# Define colors for better readability
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
GRAY='\033[0;90m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Define symbols
CHECK="${GREEN}${NC}"
CROSS="${RED}${NC}"
INFO="${BLUE}${NC}"
WARN="${YELLOW}${NC}"
ARROW="${CYAN}${NC}"
STAR="${PURPLE}${NC}"
CLOCK="${YELLOW}${NC}"
# Define variables
SCRIPT_START_TIME=$(date +%s)
# ===============================================================================
# UTILITY FUNCTIONS
# ===============================================================================
# Print timestamp
print_timestamp() {
echo -e "${GRAY}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}
# Print section header
print_header() {
local title="$1"
local title_length=${#title}
local total_length=80
local side_length=$(( (total_length - title_length - 2) / 2 ))
local side_line=$(printf '%*s' "$side_length" | tr ' ' '═')
echo -e "\n${BOLD}${PURPLE}$side_line${NC} ${BOLD}${CYAN}$title${NC} ${BOLD}${PURPLE}$side_line${NC}\n"
}
# Print success message
print_success() {
print_timestamp "${CHECK} ${GREEN}$1${NC}"
}
# Print info message
print_info() {
print_timestamp "${INFO} $1"
}
# Print warning message
print_warning() {
print_timestamp "${WARN} ${YELLOW}$1${NC}"
}
# Print error message
print_error() {
print_timestamp "${CROSS} ${RED}$1${NC}" >&2
}
# Print progress
print_progress() {
echo -e " ${ARROW} ${GRAY}$1${NC}"
}
# Error handler
handle_error() {
print_error "An error occurred at line $1"
print_error "Deployment failed!"
exit 1
}
# Calculate elapsed time
elapsed_time() {
local end_time=$(date +%s)
local elapsed=$((end_time - SCRIPT_START_TIME))
local minutes=$((elapsed / 60))
local seconds=$((elapsed % 60))
echo "${minutes}m ${seconds}s"
}
# Setup error handling
trap 'handle_error $LINENO' ERR
# ===============================================================================
# MAIN SCRIPT
# ===============================================================================
print_header "DEPLOYMENT STARTED"
print_info "Starting deployment process for CKAD Simulator with Docker Compose"
# ===============================================================================
# DOCKER IMAGE BUILDING
# ===============================================================================
print_header "DOCKER IMAGE BUILDING"
print_progress "Building Docker images via Docker Compose..."
COMPOSE_BAKE=true docker compose build
print_success "All Docker images built successfully"
# ===============================================================================
# DOCKER COMPOSE DEPLOYMENT
# ===============================================================================
print_header "DOCKER COMPOSE DEPLOYMENT"
print_progress "Starting Docker Compose services..."
docker compose up -d --remove-orphans
print_success "All services started successfully"
# ===============================================================================
# SERVICE AVAILABILITY CHECK
# ===============================================================================
print_header "SERVICE AVAILABILITY CHECK"
print_progress "${CLOCK} Waiting for services to be ready..."
sleep 15 # Give some time for services to start
# Check if the VNC service is running
if docker compose ps remote-desktop | grep "Up"; then
print_success "VNC service is running"
else
print_warning "VNC service may not be running properly"
fi
# Check if the webapp service is running
if docker compose ps webapp | grep "Up"; then
print_success "Webapp service is running"
else
print_warning "Webapp service may not be running properly"
fi
# Check if the Nginx service is running
if docker compose ps nginx | grep "Up"; then
print_success "Nginx service is running"
else
print_warning "Nginx service may not be running properly"
fi
# Check if the jumphost service is running
if docker compose ps jumphost | grep "Up"; then
print_success "Jump host service is running"
else
print_warning "Jump host service may not be running properly"
fi
# Check if the Kubernetes cluster service is running
if docker compose ps k8s-api-server | grep "Up"; then
print_success "Kubernetes cluster is running"
# Wait for the KIND cluster to be fully ready
print_progress "${CLOCK} Waiting for Kubernetes cluster to be fully initialized..."
sleep 30
# Check if cluster is accessible
if docker compose exec k8s-api-server kind get clusters | grep "kind-cluster"; then
print_success "KIND cluster is operational and accessible"
else
print_warning "KIND cluster may still be initializing"
fi
else
print_warning "Kubernetes cluster may not be running properly"
fi
# ===============================================================================
# DEPLOYMENT SUMMARY
# ===============================================================================
TOTAL_TIME=$(elapsed_time)
print_header 'DEPLOYMENT SUMMARY'
echo -e "${STAR} ${GREEN}Deployment completed successfully!${NC}"
echo -e "${INFO} ${CYAN}Environment:${NC} CKAD Simulator (Docker Compose)"
echo -e "${INFO} ${CYAN}Services deployed:${NC} 5 (remote-desktop, webapp, nginx, jumphost, k8s-api-server)"
echo -e "${INFO} ${CYAN}Total elapsed time:${NC} ${YELLOW}${TOTAL_TIME}${NC}"
echo -e "\n${STAR} ${GREEN}Your CKAD simulator is ready to use!${NC} ${STAR}\n"
# ===============================================================================
# ACCESS INFORMATION
# ===============================================================================
print_header "ACCESS INFORMATION"
# Get the host IP address
HOST_IP=$(hostname -I | awk '{print $1}')
if [ -z "$HOST_IP" ]; then
HOST_IP="localhost"
fi
echo -e "${CYAN}The following services are available:${NC}"
echo -e "\n${STAR} ${GREEN}Access Simulator here:${NC} ${BOLD}http://${HOST_IP}:30080${NC}"
#open browser on host machine
open http://${HOST_IP}:30080
echo -e "${INFO} ${GRAY}Note: All other services (VNC, jumphost, K8s) are only accessible internally through the web application.${NC}"
# ===============================================================================
# HELPFUL COMMANDS
# ===============================================================================
print_header "HELPFUL COMMANDS"
echo -e "${CYAN}To stop the environment:${NC}"
echo -e " ${GREEN}docker compose down --volumes --remove-orphans${NC}"
echo -e "\n${CYAN}To restart the environment:${NC}"
echo -e " ${GREEN}docker compose restart${NC}"
echo -e "\n${CYAN}To view logs:${NC}"
echo -e " ${GREEN}docker compose logs -f${NC}"

245
compose.yaml Normal file
View File

@@ -0,0 +1,245 @@
services:
# VNC Server (Ubuntu)
remote-desktop:
image: nishanb/ck-x-simulator-remote-desktop:latest
build:
context: ./remote-desktop
hostname: terminal
expose:
- "5901" # VNC port (internal only)
- "6901" # Web VNC port (internal only)
environment:
- VNC_PW=bakku-the-wizard
- VNC_PASSWORD=bakku-the-wizard
- VNC_VIEW_ONLY=false
- VNC_RESOLUTION=1280x800
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6901/"]
interval: 10s
timeout: 5s
retries: 3
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
networks:
- ckx-network
# Web Application (not directly exposed to users)
webapp:
image: nishanb/ck-x-simulator-webapp:latest
build:
context: ./app
expose:
- "3000" # Only exposed to internal network
environment:
- VNC_SERVICE_HOST=remote-desktop
- VNC_SERVICE_PORT=6901
- VNC_PASSWORD=bakku-the-wizard
- SSH_HOST=remote-terminal
- SSH_PORT=22
- SSH_USER=candidate
- SSH_PASSWORD=password
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.2'
memory: 256M
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:3000/"]
interval: 10s
timeout: 5s
retries: 3
networks:
- ckx-network
# Nginx Reverse Proxy - ONLY SERVICE EXPOSED TO USERS
nginx:
image: nishanb/ck-x-simulator-nginx:latest
build:
context: ./nginx
depends_on:
- webapp
- remote-desktop
- remote-terminal
- facilitator
- k8s-api-server
ports:
- "30080:80" # Expose Nginx on port 30080
deploy:
resources:
limits:
cpus: '0.2'
memory: 256M
reservations:
cpus: '0.1'
memory: 128M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 10s
timeout: 5s
retries: 3
networks:
- ckx-network
# Jump Host (ckad9999 and ckad9988)
jumphost:
image: nishanb/ck-x-simulator-jumphost:latest
build:
context: ./jumphost
hostname: ckad9999
privileged: true
# No external port mappings - only accessible internally
expose:
- "22" # SSH port (internal only)
volumes:
- kube-config:/home/candidate/.kube # Shared volume for Kubernetes config
deploy:
resources:
limits:
cpus: '1'
reservations:
cpus: '0.5'
memory: 512M
networks:
- ckx-network
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "22"]
interval: 10s
timeout: 5s
retries: 3
# Remote Terminal Service
remote-terminal:
image: nishanb/ck-x-simulator-remote-terminal:latest
build:
context: ./remote-terminal
hostname: remote-terminal
expose:
- "22" # SSH port (internal only)
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.2'
memory: 256M
networks:
- ckx-network
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "22"]
interval: 10s
timeout: 5s
retries: 3
# KIND Kubernetes Cluster
k8s-api-server: # Service name that will be used for DNS resolution
image: nishanb/ck-x-simulator-cluster:latest
build:
context: ./kind-cluster
container_name: kind-cluster
hostname: k8s-api-server
privileged: true # Required for running containers inside KIND
expose:
- "6443:6443"
- "22"
volumes:
- kube-config:/home/candidate/.kube # Shared volume for Kubernetes config
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '1'
memory: 2G
networks:
- ckx-network
healthcheck:
test: ["CMD", "ls", "/ready"]
interval: 30s
timeout: 5s
retries: 20
start_period: 60s
# Redis Database for Facilitator
redis:
image: redis:alpine
hostname: redis
command: ["redis-server", "--appendonly", "yes"]
expose:
- "6379"
volumes:
- redis-data:/data
restart: unless-stopped
networks:
- ckx-network
deploy:
resources:
limits:
cpus: '0.3'
memory: 256M
reservations:
cpus: '0.1'
memory: 128M
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# Facilitator Service
facilitator:
image: nishanb/ck-x-simulator-facilitator:latest
build:
context: ./facilitator
dockerfile: Dockerfile
hostname: facilitator
expose:
- "3000"
environment:
- PORT=3000
- NODE_ENV=prod
- SSH_HOST=jumphost
- SSH_PORT=22
- SSH_USERNAME=candidate
- LOG_LEVEL=info
- REDIS_HOST=redis
- REDIS_PORT=6379
- TRACK_METRICS=true # based on the environment variable TRACK_METRICS, the facilitator will send metrics to the metric server
restart: unless-stopped
depends_on:
- jumphost
- redis
networks:
- ckx-network
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.2'
memory: 256M
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:3000"]
interval: 10s
timeout: 5s
retries: 3
networks:
ckx-network:
name: ckx-network
driver: bridge
volumes:
kube-config: # Shared volume for Kubernetes configuration
redis-data: # Persistent volume for Redis data

42
docs/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,42 @@
# Contributing to CK-X Simulator
Thank you for your interest in contributing! Here's how you can help:
## Quick Start
1. Fork and clone the [repository](https://github.com/nishanb/ck-x)
2. Follow our [Development Setup Guide](docs/development-setup.md)
3. Create a new branch for your changes
4. Submit a Pull Request
## Community
- Join our [Telegram Community](https://t.me/ckxdev)
- Star the repository if you find it helpful
## Important Rules
### No Plagiarism
- Do not copy official exam questions
- Create original practice scenarios
- Focus on teaching concepts
### Lab Guidelines
- Follow [Lab Creation Guide](docs/how-to-add-new-labs.md)
- Include verification scripts
- Test thoroughly
- Provide clear instructions
### Code Quality
- Follow existing code style
- Write clear commit messages
- Add tests for new features
- Update documentation
## Questions?
Check our [FAQ](docs/FAQ.md) or join our [Telegram Community](https://t.me/ckxdev).
## License
By contributing, you agree that your contributions will be licensed under the project's MIT License.

30
docs/PRIVACY_POLICY.md Normal file
View File

@@ -0,0 +1,30 @@
# Privacy Policy
**Effective Date:** [Insert Date]
## 1. Introduction
CK-X is an independent Kubernetes certification exam simulator. This Privacy Policy explains how we handle user data and ensure transparency regarding data collection and usage.
## 2. No Affiliation
CK-X is **not affiliated with, endorsed by, or associated with the Cloud Native Computing Foundation (CNCF), the Linux Foundation, or PSI.** Any references to Kubernetes, CKAD, CKA, or CKS are for descriptive purposes only, and all trademarks belong to their respective owners.
## 3. Data Collection
We collect minimal data necessary to improve CK-X. This may include:
- Basic user preferences and settings.
- Anonymous analytics for performance improvements.
We **do not** collect personal information, sell user data, or share it with third parties.
## 4. Use of Third-Party Tools
CK-X may utilize open-source components and third-party tools, which are subject to their respective licenses. Users are responsible for reviewing those licenses where applicable.
## 5. No Guarantees
CK-X is provided for educational purposes only. We do **not** guarantee:
- That the content reflects actual CKAD, CKA, or CKS exam questions.
- That using CK-X will result in passing any certification exam.
## 6. Question Content
All questions in CK-X are either **originally created** for educational purposes or **contributed by community contributors**. We do **not copy or reproduce** actual exam questions from any official sources.
## 7. Policy Changes
We may update this Privacy Policy from time to time. Continued use of CK-X after any changes implies acceptance of the updated terms.

278
docs/development-setup.md Normal file
View File

@@ -0,0 +1,278 @@
# CK-X Simulator: Development Setup Guide
This document provides step-by-step instructions for setting up and running the CK-X Simulator locally for development purposes.
## Prerequisites
### System Requirements
- **Operating System**: Linux, macOS, or Windows 10/11 with WSL2
- **CPU**: 4+ cores recommended (minimum 2 cores)
- **RAM**: 8GB minimum, 16GB recommended
- **Storage**: 20GB free space
### Required Software
- **Docker**: v20.10.0 or newer
- **Docker Compose**: v2.0.0 or newer
## Installation Steps
### 1. Clone the Repository
```bash
git clone https://github.com/your-org/ck-x-simulator.git
cd ck-x-simulator
```
### 2. Configure the Environment
Review the `compose.yaml` file to understand the service configuration. Key services include:
- **remote-desktop**: VNC server (Ubuntu)
- **webapp**: Web Application frontend
- **nginx**: Reverse proxy (only service exposed to users)
- **jumphost**: SSH access host
- **remote-terminal**: Remote terminal service
- **k8s-api-server**: KIND Kubernetes cluster
- **redis**: Redis database for Facilitator
- **facilitator**: Backend service
### 3. Start the Services
```bash
# Build and start all services
docker-compose up --build
```
This command will build and start all the services defined in the compose.yaml file.
## Accessing the Application
### 1. Web Interface
Once all services are running, access the web interface via Nginx:
```
http://localhost:30080
```
### 2. VNC Remote Desktop
The VNC server is not directly exposed outside the container network. To access it:
1. The web interface proxies VNC connections through Nginx
2. Default VNC password: `bakku-the-wizard` (configured in compose.yaml)
3. VNC resolution: 1280x800
### 3. SSH Access
SSH access is provided through the jumphost service:
- **Hostname**: ckad9999
- **Username**: candidate
- **Password**: password (configured in compose.yaml)
The SSH service is not directly exposed outside the container network and is accessed through the webapp.
## Service Details
### 1. Remote Desktop (VNC)
```yaml
# From compose.yaml
remote-desktop:
image: nishanb/ck-x-simulator-remote-desktop:latest
hostname: terminal
expose:
- "5901" # VNC port (internal only)
- "6901" # Web VNC port (internal only)
environment:
- VNC_PW=bakku-the-wizard
- VNC_PASSWORD=bakku-the-wizard
- VNC_VIEW_ONLY=false
- VNC_RESOLUTION=1280x800
```
The remote desktop provides a graphical interface for the exam environment.
### 2. Web Application
```yaml
# From compose.yaml
webapp:
image: nishanb/ck-x-simulator-webapp:latest
expose:
- "3000" # Only exposed to internal network
environment:
- VNC_SERVICE_HOST=remote-desktop
- VNC_SERVICE_PORT=6901
- VNC_PASSWORD=bakku-the-wizard
- SSH_HOST=remote-terminal
- SSH_PORT=22
- SSH_USER=candidate
- SSH_PASSWORD=password
```
The web application serves as the frontend interface for the simulator.
### 3. Nginx (Reverse Proxy)
```yaml
# From compose.yaml
nginx:
image: nishanb/ck-x-simulator-nginx:latest
ports:
- "30080:80" # Expose Nginx on port 30080
```
Nginx is the only service directly exposed to users and handles routing to internal services.
### 4. Kubernetes Cluster
```yaml
# From compose.yaml
k8s-api-server:
image: nishanb/ck-x-simulator-cluster:latest
container_name: kind-cluster
hostname: k8s-api-server
privileged: true # Required for running containers inside KIND
expose:
- "6443:6443"
- "22"
volumes:
- kube-config:/home/candidate/.kube # Shared volume for Kubernetes config
```
The Kubernetes cluster runs in a KIND container with shared kube-config volume.
### 5. Facilitator Service
```yaml
# From compose.yaml
facilitator:
image: nishanb/ck-x-simulator-facilitator:latest
hostname: facilitator
expose:
- "3000"
ports:
- "3001:3000"
environment:
- PORT=3000
- NODE_ENV=prod
- SSH_HOST=jumphost
- SSH_PORT=22
- SSH_USERNAME=candidate
- LOG_LEVEL=info
- REDIS_HOST=redis
- REDIS_PORT=6379
```
The facilitator service handles backend operations and communicates with the Kubernetes cluster.
## Development Workflow
### 1. Modifying Services
To modify a service, edit its corresponding directory and then rebuild:
```bash
# Edit files in the respective service directory
# Then rebuild and restart the service
docker-compose up --build <service-name>
```
### 2. Inspecting Logs
```bash
# View logs for all services
docker-compose logs
# View logs for a specific service
docker-compose logs <service-name>
# Follow logs
docker-compose logs -f <service-name>
```
### 3. Accessing Containers
```bash
# Get shell access to a container
docker-compose exec <service-name> bash
# Examples:
docker-compose exec webapp bash
docker-compose exec facilitator bash
docker-compose exec k8s-api-server bash
```
## Troubleshooting
### 1. Container Startup Issues
If containers fail to start, check the logs:
```bash
docker-compose logs <service-name>
```
### 2. VNC Connection Issues
```bash
# Check if VNC server is running
docker-compose exec remote-desktop ps aux | grep vnc
# Restart VNC service
docker-compose restart remote-desktop
```
### 3. Kubernetes Cluster Issues
```bash
# Check cluster status
docker-compose exec k8s-api-server kubectl cluster-info
# Restart the cluster
docker-compose restart k8s-api-server
```
### 4. Resource Constraints
If your system cannot handle the resource requirements, adjust the limits in compose.yaml:
```yaml
deploy:
resources:
limits:
cpus: '1' # Reduce CPU allocation
memory: 1G # Reduce memory allocation
```
## Network Architecture
All services are connected through the `ckx-network` bridge network:
```yaml
networks:
ckx-network:
name: ckx-network
driver: bridge
```
Services can communicate with each other using their service names as hostnames.
## Volume Management
The system uses persistent volumes for:
```yaml
volumes:
kube-config: # Shared volume for Kubernetes configuration
redis-data: # Persistent volume for Redis data
```
## Reference Links
- [Docker Compose Documentation](https://docs.docker.com/compose/)
- [Kubernetes Documentation](https://kubernetes.io/docs/)
- [KIND Documentation](https://kind.sigs.k8s.io/)
- [VNC Documentation](https://www.realvnc.com/en/connect/docs/)

261
docs/how-to-add-new-labs.md Normal file
View File

@@ -0,0 +1,261 @@
# Contributing Labs to CK-X Simulator
This guide explains how to create and contribute your own practice labs for the CK-X Simulator. By following these steps, you can create custom assessment scenarios for Kubernetes certification preparation (CKAD, CKA, CKS) or other container-related topics.
## Lab Structure Overview
Each lab in CK-X Simulator consists of:
1. **Lab Entry** in the main labs registry
2. **Configuration File** for lab settings
3. **Assessment File** containing questions and verification steps
4. **Setup and Verification Scripts** to prepare environments and validate student solutions
5. **Answers File** with solution documentation
## Step 1: Create Lab Directory Structure
First, create a directory structure for your lab using this pattern:
```
facilitator/
└── assets/
└── exams/
└── [category]/
└── [id]/
├── config.json
├── assessment.json
├── answers.md
└── scripts/
├── setup/
│ └── [setup scripts]
└── validation/
└── [verification scripts]
```
Where:
- `[category]` is the certification type (e.g., `ckad`, `cka`, `cks`, `other`)
- `[id]` is a numeric identifier (e.g., `001`, `002`)
For example, to create a new CKAD lab with ID 003:
```
facilitator/assets/exams/ckad/003/
```
## Step 2: Create Configuration File
Create a `config.json` file in your lab directory with the following structure:
```json
{
"lab": "ckad-003",
"workerNodes": 1,
"answers": "assets/exams/ckad/003/answers.md",
"questions": "assessment.json",
"totalMarks": 100,
"lowScore": 40,
"mediumScore": 60,
"highScore": 90
}
```
Parameters:
- `lab`: Unique identifier for the lab (should match directory structure)
- `workerNodes`: Number of worker nodes required for this lab
- `answers`: Path to answers markdown file
- `questions`: Path to assessment JSON file
- `totalMarks`: Maximum possible score
- `lowScore`, `mediumScore`, `highScore`: Score thresholds for result categorization
## Step 3: Create Assessment File
Create an `assessment.json` file that defines questions, namespaces, and verification steps:
```json
{
"questions": [
{
"id": "1",
"namespace": "default",
"machineHostname": "node01",
"question": "Create a deployment named `nginx-deploy` with 3 replicas using the nginx:1.19 image.\n\nEnsure the deployment is created in the `default` namespace.",
"concepts": ["deployments", "replication"],
"verification": [
{
"id": "1",
"description": "Deployment exists",
"verificationScriptFile": "q1_s1_validate_deployment.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "Deployment has 3 replicas",
"verificationScriptFile": "q1_s2_validate_replicas.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "Deployment uses correct image",
"verificationScriptFile": "q1_s3_validate_image.sh",
"expectedOutput": "0",
"weightage": 1
}
]
}
// Add more questions...
]
}
```
Each question should include:
- `id`: Unique question identifier
- `namespace`: Kubernetes namespace for the question
- `machineHostname`: The hostname to display for SSH connection
- `question`: The actual task description with formatting:
- Use `\n` for line breaks to improve readability
- Put code references, commands, or file paths in backtick (e.g., `nginx:1.19`) which will be highlighted in the UI
- Structure your question with clear paragraphs separated by blank lines
- `concepts`: Array of concepts/topics covered
- `verification`: Array of verification steps
Each verification step includes:
- `id`: Unique step identifier
- `description`: Human-readable description of what's being checked
- `verificationScriptFile`: Script file path to validate the step (present in /scripts/validation directory)
- `expectedOutput`: Expected return code (usually "0" for success)
- `weightage`: Point value for this verification step
## Step 4: Create Setup and Verification Scripts
The CK-X Simulator uses two types of scripts:
### Setup Scripts
Create setup scripts in the `scripts/setup/` directory to prepare the environment for each question. These scripts run before the student starts the exam to ensure the necessary resources are available.
Example setup script (`scripts/setup/q1_setup.sh`):
```bash
#!/bin/bash
# Setup environment for Question 1
# Create namespace if it doesn't exist
kubectl create namespace default --dry-run=client -o yaml | kubectl apply -f -
# Create any prerequisite resources
kubectl apply -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
namespace: default
data:
nginx.conf: |
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
EOF
echo "Environment setup complete for Question 1"
exit 0
```
### Verification Scripts
Create verification scripts in the `scripts/validation/` directory to validate student solutions. Each script should:
1. Check a specific aspect of the solution
2. Return exit code 0 for success, non-zero for failure
3. Output useful information for student feedback
Example verification script (`scripts/validation/q1_s1_validate_deployment.sh`):
```bash
#!/bin/bash
# Check if deployment exists
DEPLOYMENT_NAME="nginx-deploy"
NAMESPACE="default"
kubectl get deployment $DEPLOYMENT_NAME -n $NAMESPACE &> /dev/null
if [ $? -eq 0 ]; then
echo "✅ Deployment '$DEPLOYMENT_NAME' exists in namespace '$NAMESPACE'"
exit 0
else
echo "❌ Deployment '$DEPLOYMENT_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
```
## Step 5: Create Answers File
Create an `answers.md` file containing solutions to your questions. This file will be displayed directly to students when they view the exam answers.
Focus on providing clear, educational solutions with detailed explanations. The file is rendered as standard Markdown, so you can use all Markdown formatting features. Include complete solution commands, explanations of why certain approaches work, and any relevant tips or best practices.
For each question, provide the question text as a heading followed by a comprehensive solution that would help someone understand not just what to do but why that approach is correct.
## Step 6: Register Your Lab
Finally, add your lab to the main `labs.json` file:
```json
{
"labs": [
// ... existing labs ...
{
"id": "ckad-003",
"assetPath": "assets/exams/ckad/003",
"name": "CKAD Practice Lab - Advanced Deployments",
"category": "CKAD",
"description": "Practice advanced deployment patterns and strategies",
"warmUpTimeInSeconds": 60,
"difficulty": "medium"
}
]
}
```
Parameters:
- `id`: Unique identifier (should match directory structure)
- `assetPath`: Path to lab resources
- `name`: Display name for the lab
- `category`: Lab category (CKAD, CKA, CKS, etc.)
- `description`: Brief description of the lab content
- `warmUpTimeInSeconds`: Preparation time before exam starts
- `difficulty`: Difficulty level (easy, medium, hard)
## Best Practices
1. **Realistic Scenarios**: Design questions that mimic real certification exam tasks
2. **Clear Instructions**: Write concise, unambiguous question descriptions
3. **Thorough Verification**: Create scripts that verify all aspects of the solution
4. **Comprehensive Answers**: Provide complete, educational solutions
5. **Progressive Difficulty**: Arrange questions from simple to complex
6. **Namespaces**: Use separate namespaces for different questions to avoid conflicts
7. **Resource Requirements**: Keep resource requirements reasonable
## Testing Your Lab
Before submitting your lab:
1. Build and deploy the simulator with your new lab
2. Go through each question as a student would
3. Verify that all verification scripts work correctly
4. Ensure the answers solve the questions as expected
5. Check that scoring and evaluation work properly
## Contribution Process
1. Fork the CK-X Simulator repository
2. Add your lab following these guidelines
3. Test thoroughly
4. Submit a pull request with a description of your lab
Thank you for contributing to the CK-X community!

23
docs/local-setup-guide.md Normal file
View File

@@ -0,0 +1,23 @@
# Local Setup Guide for CK-X Simulator
## Quick Setup
1. Clone the repository:
```bash
git clone https://github.com/nishanb/ck-x.git
cd ck-x
```
2. Run the deployment script:
```bash
./scripts/compose-deploy.sh
```
The script will deploy all services locally and open the application in your browser.
After making any changes to the code, you can redeploy with:
```bash
docker compose up -d
```
This setup has been tested on Mac and Linux environments.

View File

@@ -0,0 +1,788 @@
# CK-X Simulator: exam.html Functionality Documentation
This document provides a detailed technical overview of the `exam.html` file, focusing on its structure, interactions, and API calls.
## Table of Contents
1. [HTML Structure](#html-structure)
2. [Functionality Overview](#functionality-overview)
3. [Component Interactions](#component-interactions)
4. [API Integration](#api-integration)
5. [Event Handlers](#event-handlers)
6. [State Management](#state-management)
7. [Error Handling](#error-handling)
## HTML Structure
The `exam.html` file serves as the main exam interface for the CK-X Simulator. Here's a detailed breakdown of its structure:
### 1. Head Section
```html
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CK-X Simulator - Exam</title>
<!-- External Dependencies -->
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/exam.css">
</head>
```
### 2. Body Components
#### Loader Component
```html
<div class="loader" id="pageLoader">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
```
- **Purpose**: Displays during initial page load and environment setup
- **State Management**: Controlled by `showLoader()` and `hideLoader()` functions
#### Toast Container
```html
<div class="toast-container position-fixed top-0 end-0 p-3" id="toastContainer">
<!-- Toasts will be added here dynamically -->
</div>
```
- **Purpose**: Displays notifications and alerts
- **Dynamic Content**: Toasts added/removed based on events
#### Exam End Modal
```html
<div class="modal fade" id="examEndModal" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<!-- Modal content -->
</div>
</div>
</div>
```
- **Purpose**: Handles exam completion
- **Static Backdrop**: Prevents accidental dismissal
- **Results Button**: Triggers results page navigation
#### Start Exam Modal
```html
<div class="modal fade" id="startExamModal" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<!-- Different content states -->
<div class="modal-body" id="newExamContent">
<!-- New exam content -->
</div>
<div class="modal-body" id="examInProgressContent">
<!-- In-progress exam content -->
</div>
<div class="modal-body" id="examCompletedContent">
<!-- Completed exam content -->
</div>
</div>
</div>
</div>
```
- **Multiple States**: Handles different exam scenarios
- **Static Backdrop**: Prevents accidental dismissal
- **Dynamic Content**: Updates based on exam state
#### Header Section
```html
<div class="header">
<div class="header-title-section">
<div class="header-title">CK-X Simulator</div>
<div class="timer" id="examTimer">120:00</div>
</div>
<div class="header-controls">
<!-- Dropdown menus -->
</div>
</div>
```
- **Timer Display**: Shows remaining exam time
- **Control Menus**: Interface, controls, and information options
#### Main Container
```html
<div class="main-container" id="mainContainer">
<!-- Question Panel -->
<div class="question-panel" id="questionPanel">
<!-- Question navigation and content -->
</div>
<!-- Resizable Divider -->
<div class="panel-divider" id="panelDivider">
<!-- Resize controls -->
</div>
<!-- VNC Panel -->
<div class="vnc-panel" id="vncPanel">
<!-- Terminal and VNC containers -->
</div>
</div>
```
- **Split Layout**: Questions and environment panels
- **Resizable**: Adjustable panel sizes
- **Dual Interface**: VNC and terminal options
## Functionality Overview
The `exam.html` file provides the core exam interface for the CK-X Simulator. Here's a detailed breakdown of its functionality:
### 1. Exam Interface Components
#### Header Controls
1. **Exam Interface Dropdown**
- Terminal resize options
- Fullscreen toggle
- View switching
- Help resources
2. **Exam Controls Dropdown**
- End exam option
- Session termination
- Emergency controls
3. **Exam Information Dropdown**
- Exam instructions
- Documentation links
- Help resources
#### Main Interface
1. **Question Panel**
- Question navigation
- Question content display
- Progress tracking
- Answer submission
2. **Environment Panel**
- VNC connection
- Terminal access
- View switching
- Fullscreen options
### 2. Exam Flow Management
#### Initial Setup
1. **Session Validation**
- Exam ID verification
- Session state check
- Environment preparation
2. **Interface Initialization**
- Panel setup
- Timer configuration
- Connection establishment
3. **State Restoration**
- Previous session recovery
- Time remaining calculation
- Question state restoration
#### During Exam
1. **Timer Management**
- Countdown tracking
- Time warnings
- Auto-submission
2. **Question Navigation**
- Next/Previous controls
- Question selection
- Progress tracking
3. **Environment Control**
- View switching
- Panel resizing
- Fullscreen management
### 3. State Management
#### Exam State
- Timer tracking
- Question progress
- Answer submissions
- Environment state
#### UI State
- Panel sizes
- View selection
- Modal states
- Loading states
### 4. API Integration
#### Endpoints
1. **Exam Status**
- `/facilitator/api/v1/exams/{id}`
- Status updates
- Time tracking
2. **Question Data**
- `/facilitator/api/v1/exams/{id}/questions`
- Question loading
- Answer submission
3. **Environment Control**
- `/facilitator/api/v1/exams/{id}/environment`
- Connection management
- State updates
### 5. Error Handling
#### Connection Issues
- VNC connection failures
- Terminal disconnections
- API communication errors
#### State Recovery
- Session restoration
- Timer synchronization
- Question state recovery
### 6. Security Features
#### Session Management
- Secure connections
- State validation
- Access control
#### Data Protection
- Secure communication
- State encryption
- Access restrictions
### 7. Performance Optimization
#### Resource Management
- Efficient rendering
- Connection pooling
- State caching
#### UI Responsiveness
- Smooth transitions
- Non-blocking operations
- Efficient updates
### 8. Browser Compatibility
#### Cross-browser Support
- Modern browser features
- Fallback mechanisms
- Consistent rendering
#### Mobile Support
- Responsive design
- Touch controls
- Adaptive layout
### 9. User Experience
#### Interface Feedback
- Loading states
- Progress indicators
- Status messages
#### Navigation
- Intuitive controls
- Clear indicators
- Easy access
### 10. Maintenance and Updates
#### Code Organization
- Modular structure
- Clear separation
- Maintainable components
#### Future Enhancements
- Feature addition points
- Integration capabilities
- Scalability options
## Component Interactions
### 1. Initial Page Load Sequence
```mermaid
sequenceDiagram
participant User
participant Browser
participant UI
participant API
participant Environment
User->>Browser: Access exam.html
Browser->>UI: DOMContentLoaded
UI->>UI: Show Loader
UI->>API: Get examId from URL
API-->>UI: Return examId
UI->>API: initializeExamSession(examId)
API-->>UI: Return session data
UI->>Environment: initializeEnvironment()
Environment-->>UI: Environment ready
UI->>UI: setupUIComponents()
UI->>UI: startExamTimer()
UI->>UI: Hide Loader
UI-->>User: Display exam interface
```
### 2. Exam Start Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant Environment
participant Timer
User->>UI: Click "Start Exam"
UI->>UI: showStartExamModal()
User->>UI: Confirm start
UI->>UI: updateModalContent('new')
UI->>API: Create exam session
API-->>UI: Session created
UI->>Environment: Initialize VNC/Terminal
Environment-->>UI: Connection established
UI->>Timer: startExamTimer()
Timer-->>UI: Timer running
UI->>UI: Enter fullscreen mode
UI-->>User: Exam interface ready
```
### 3. Question Navigation Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant State
User->>UI: Navigate to question
UI->>State: saveCurrentQuestion()
UI->>API: loadQuestion(questionId)
API-->>UI: Question data
UI->>UI: displayQuestion()
UI->>State: updateProgress()
UI-->>User: Display new question
```
### 4. Environment Control Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant VNC
participant Terminal
participant State
User->>UI: Switch view mode
alt VNC Mode
UI->>VNC: initializeVNC()
VNC-->>UI: VNC ready
UI->>Terminal: hide()
else Terminal Mode
UI->>Terminal: initializeTerminal()
Terminal-->>UI: Terminal ready
UI->>VNC: hide()
end
UI->>State: saveViewPreference()
UI-->>User: View switched
```
### 5. Panel Resizing Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant State
User->>UI: Start dragging divider
UI->>UI: isResizing = true
loop While dragging
User->>UI: Mouse move
UI->>UI: calculateNewWidth()
UI->>UI: updatePanelSize()
end
User->>UI: Release mouse
UI->>UI: isResizing = false
UI->>State: savePanelSizes()
UI-->>User: Panels resized
```
### 6. Error Recovery Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant Environment
participant State
Note over UI: Connection error detected
UI->>UI: showError(message)
UI->>Environment: attemptReconnect()
alt Reconnection successful
Environment-->>UI: Connection restored
UI->>State: restoreState()
UI-->>User: Recovery complete
else Reconnection failed
Environment-->>UI: Connection failed
UI->>API: reportError()
UI-->>User: Show recovery options
end
```
### 7. Exam End Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant Environment
participant State
User->>UI: Click "End Exam"
UI->>UI: showExamEndModal()
User->>UI: Confirm end
UI->>API: submitExam()
API-->>UI: Submission successful
UI->>State: clearExamState()
UI->>Environment: cleanup()
Environment-->>UI: Cleanup complete
UI->>UI: redirectToResults()
UI-->>User: Show results page
```
### 8. State Management Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant State
participant localStorage
Note over UI: Periodic state save
UI->>State: saveExamState()
State->>localStorage: setItem('examState')
Note over UI: Page reload
UI->>localStorage: getItem('examState')
localStorage-->>UI: State data
UI->>State: restoreExamState()
State->>UI: Update UI components
UI-->>User: State restored
```
### 9. Timer Management Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant Timer
participant State
UI->>Timer: startExamTimer()
loop Every second
Timer->>Timer: decrementTime()
Timer->>UI: updateTimerDisplay()
Timer->>State: saveTimeRemaining()
alt Time <= 0
Timer->>UI: triggerExamEnd()
else Time <= warningThreshold
Timer->>UI: showWarning()
end
end
```
### 10. Environment Initialization Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant VNC
participant Terminal
participant State
UI->>API: requestEnvironment()
API-->>UI: Environment config
UI->>VNC: initializeVNC()
VNC-->>UI: VNC ready
UI->>Terminal: initializeTerminal()
Terminal-->>UI: Terminal ready
UI->>State: saveEnvironmentState()
UI-->>User: Environment ready
```
## API Integration
### 1. Exam Session Management
```javascript
async function initializeExamSession(examId) {
try {
const response = await fetch(`/facilitator/api/v1/exams/${examId}`);
if (!response.ok) throw new Error('Failed to load exam session');
const data = await response.json();
setupExamSession(data);
return data;
} catch (error) {
console.error('Error initializing exam session:', error);
showError('Failed to load exam session');
return null;
}
}
```
### 2. Question Management
```javascript
async function loadQuestion(questionId) {
try {
const response = await fetch(`/facilitator/api/v1/exams/${examId}/questions/${questionId}`);
if (!response.ok) throw new Error('Failed to load question');
const data = await response.json();
displayQuestion(data);
return data;
} catch (error) {
console.error('Error loading question:', error);
showError('Failed to load question');
return null;
}
}
```
### 3. Environment Control
```javascript
async function initializeEnvironment() {
try {
const response = await fetch(`/facilitator/api/v1/exams/${examId}/environment`);
if (!response.ok) throw new Error('Failed to initialize environment');
const data = await response.json();
setupEnvironment(data);
return data;
} catch (error) {
console.error('Error initializing environment:', error);
showError('Failed to initialize environment');
return null;
}
}
```
## Event Handlers
### 1. Modal Management
```javascript
// Show Exam End Modal
function showExamEndModal() {
const modal = new bootstrap.Modal(document.getElementById('examEndModal'));
modal.show();
}
// Show Start Exam Modal
function showStartExamModal() {
const modal = new bootstrap.Modal(document.getElementById('startExamModal'));
modal.show();
}
// Handle Modal State
function updateModalContent(state) {
const newExamContent = document.getElementById('newExamContent');
const inProgressContent = document.getElementById('examInProgressContent');
const completedContent = document.getElementById('examCompletedContent');
// Hide all content
[newExamContent, inProgressContent, completedContent].forEach(el => {
el.style.display = 'none';
});
// Show relevant content
switch (state) {
case 'new':
newExamContent.style.display = 'block';
break;
case 'in_progress':
inProgressContent.style.display = 'block';
break;
case 'completed':
completedContent.style.display = 'block';
break;
}
}
```
### 2. View Management
```javascript
// Switch to Terminal View
function switchToTerminal() {
document.getElementById('vncPanel').style.display = 'none';
document.getElementById('sshTerminalContainer').style.display = 'block';
initializeTerminal();
}
// Switch to VNC View
function switchToVNC() {
document.getElementById('sshTerminalContainer').style.display = 'none';
document.getElementById('vncPanel').style.display = 'block';
initializeVNC();
}
// Toggle Fullscreen
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
}
```
## State Management
### 1. Exam State
```javascript
// Save Exam State
function saveExamState() {
const state = {
currentQuestion: currentQuestionId,
timeLeft: timeLeft,
answers: submittedAnswers,
environment: environmentState
};
localStorage.setItem('examState', JSON.stringify(state));
}
// Load Exam State
function loadExamState() {
const state = localStorage.getItem('examState');
if (state) {
const parsedState = JSON.parse(state);
restoreExamState(parsedState);
}
}
// Clear Exam State
function clearExamState() {
localStorage.removeItem('examState');
}
```
### 2. UI State
```javascript
// Update UI State
function updateUIState() {
updateQuestionNavigation();
updateTimerDisplay();
updateEnvironmentStatus();
updatePanelSizes();
}
// Save UI Preferences
function saveUIPreferences() {
const preferences = {
panelSizes: {
question: document.getElementById('questionPanel').offsetWidth,
environment: document.getElementById('vncPanel').offsetWidth
},
viewMode: currentViewMode,
fullscreen: document.fullscreenElement !== null
};
localStorage.setItem('uiPreferences', JSON.stringify(preferences));
}
// Load UI Preferences
function loadUIPreferences() {
const preferences = localStorage.getItem('uiPreferences');
if (preferences) {
const parsedPreferences = JSON.parse(preferences);
applyUIPreferences(parsedPreferences);
}
}
```
## Error Handling
### 1. Connection Error Handling
```javascript
// Handle VNC Connection Error
function handleVNCError(error) {
console.error('VNC Connection Error:', error);
showError('Failed to connect to exam environment');
// Attempt recovery
attemptVNCReconnect();
}
// Handle Terminal Connection Error
function handleTerminalError(error) {
console.error('Terminal Connection Error:', error);
showError('Failed to connect to terminal');
// Attempt recovery
attemptTerminalReconnect();
}
// Show Error Message
function showError(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerHTML = `
<div class="toast-header">
<strong class="me-auto">Error</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">${message}</div>
`;
document.getElementById('toastContainer').appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
}
```
### 2. State Recovery
```javascript
// Attempt State Recovery
async function attemptStateRecovery() {
try {
// Check exam status
const examStatus = await checkExamStatus();
if (examStatus.isActive) {
// Restore exam state
await restoreExamState(examStatus);
return true;
}
return false;
} catch (error) {
console.error('State Recovery Error:', error);
return false;
}
}
// Handle Session Timeout
function handleSessionTimeout() {
showError('Session timeout. Attempting to reconnect...');
attemptStateRecovery();
}
```
## Conclusion
The `exam.html` file provides a comprehensive exam interface for the CK-X Simulator, implementing robust features for exam delivery, environment management, and user interaction. The modular design ensures maintainability and extensibility while providing a secure and reliable exam experience.
Key aspects of the implementation include:
- Split-panel interface for questions and environment
- Real-time timer and progress tracking
- Dual interface support (VNC and Terminal)
- Comprehensive error handling and recovery
- Secure session management
- Responsive and adaptive design

View File

@@ -0,0 +1,618 @@
# CK-X Simulator: index.html Functionality Documentation
This document provides a detailed technical overview of the `index.html` file, focusing on its structure, interactions, and API calls.
## Table of Contents
1. [HTML Structure](#html-structure)
2. [Functionality Overview](#functionality-overview)
3. [Component Interactions](#component-interactions)
4. [API Integration](#api-integration)
5. [Event Handlers](#event-handlers)
6. [State Management](#state-management)
7. [Error Handling](#error-handling)
## HTML Structure
The `index.html` file serves as the main landing page for the CK-X Simulator. Here's a detailed breakdown of its structure:
### 1. Head Section
```html
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<title>CK-X | Kubernetes Certification Simulator</title>
<!-- External Dependencies -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/index.css">
</head>
```
### 2. Body Components
#### Loader Component
```html
<div class="loader" id="pageLoader">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div class="loader-message" id="loaderMessage">Lab is getting ready...</div>
</div>
```
- **Purpose**: Displays during API calls and lab initialization
- **State Management**: Controlled by `showLoader()` and `hideLoader()` functions
- **Message Updates**: Dynamic updates via `updateLoaderMessage()`
#### Navigation Bar
```html
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="/">CK-X</a>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item me-3 view-results-btn-container" style="display: none;">
<a class="nav-link" href="#" id="viewPastResultsBtn">
<!-- SVG Icon -->
View Result
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/nishanb/CKAD-X" target="_blank">
<!-- GitHub Icon -->
GitHub
</a>
</li>
</ul>
</div>
</div>
</nav>
```
- **Dynamic Elements**: Results button visibility controlled by exam state
- **Event Handling**: Results button click triggers navigation to results page
#### Hero Section
```html
<section class="hero-section full-height d-flex align-items-center text-center position-relative">
<div class="container hero-content">
<h1 class="display-4 mb-4">Kubernetes Certification Exam Simulator</h1>
<p class="lead mb-5">Practice in a realistic environment...</p>
<a href="#" class="btn btn-light btn-lg start-exam-btn" id="startExamBtn">START EXAM</a>
</div>
<div class="scroll-indicator">
<p>SCROLL TO EXPLORE</p>
<i class="fas fa-chevron-down"></i>
</div>
</section>
```
- **Main CTA**: "START EXAM" button triggers exam selection flow
- **Scroll Indicator**: Visual cue for content below
#### Features Section
```html
<section id="features">
<div class="container">
<div class="features-wrapper">
<!-- Feature Cards -->
</div>
</div>
</section>
```
- **Static Content**: Displays six feature cards
- **Responsive Layout**: Bootstrap grid system for responsive design
#### Loading Overlay
```html
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-content">
<h2>Preparing Your Lab Environment</h2>
<div class="custom-progress-bar">
<div class="custom-progress" id="progressBar"></div>
</div>
<div class="loading-message" id="loadingMessage">Initializing environment...</div>
<div class="exam-info" id="examInfo"></div>
</div>
</div>
```
- **Progress Tracking**: Visual feedback during lab initialization
- **Dynamic Updates**: Progress bar and message updates via API responses
#### Exam Selection Modal
```html
<div class="modal fade" id="examSelectionModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Select Your Exam</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="examSelectionForm">
<!-- Exam Selection Form -->
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">CANCEL</button>
<button type="button" class="btn btn-primary" id="startSelectedExam" disabled>START EXAM</button>
</div>
</div>
</div>
</div>
```
- **Form Elements**: Dynamic exam selection options
- **Validation**: Start button enabled only when valid selection made
## Functionality Overview
The `index.html` file serves as the main entry point for the CK-X Simulator, providing a comprehensive interface for exam selection and management. Here's a detailed breakdown of its functionality:
### 1. User Interface Flow
#### Initial Load
1. **Page Initialization**
- Checks for existing exam sessions
- Preloads available labs data
- Initializes UI components
- Sets up event listeners
2. **Navigation Bar**
- Displays CK-X branding
- Shows/hides "View Result" button based on exam state
- Provides GitHub repository link
- Fixed position for easy access
3. **Hero Section**
- Main landing area with exam simulator title
- "START EXAM" call-to-action button
- Scroll indicator for content discovery
4. **Features Section**
- Displays six key features of the simulator
- Responsive grid layout
- Visual icons and descriptions
### 2. Exam Selection Process
#### Start Exam Flow
1. **Initial Check**
- Validates current exam status
- Checks for active sessions
- Verifies system requirements
2. **Exam Selection Modal**
- Displays available exam categories
- Shows exam descriptions
- Validates user selection
- Enables/disables start button
3. **Lab Environment Setup**
- Shows loading overlay
- Displays progress bar
- Provides status updates
- Handles initialization errors
### 3. State Management
#### Local Storage
- Stores current exam data
- Manages exam session state
- Handles user preferences
- Maintains UI state
#### UI State
- Controls loading indicators
- Manages modal visibility
- Updates button states
- Handles responsive layout
### 4. API Integration
#### Endpoints
1. **Exam Status**
- `/facilitator/api/v1/exams/current`
- Checks active exam sessions
- Returns exam details
2. **Labs Data**
- `/facilitator/api/v1/assements/`
- Fetches available labs
- Updates exam options
3. **Exam Creation**
- `/facilitator/api/v1/exams`
- Creates new exam sessions
- Handles session initialization
### 5. Error Handling
#### User Feedback
- Displays error messages
- Shows loading states
- Provides network status
- Handles API failures
#### Recovery Mechanisms
- Auto-dismissing alerts
- Network status monitoring
- Session state recovery
- Graceful error handling
### 6. Security Features
#### Access Control
- Validates exam sessions
- Checks user permissions
- Manages secure redirects
- Handles session timeouts
#### Data Protection
- Secure API communication
- Safe data storage
- Protected user information
- Secure state management
### 7. Performance Optimization
#### Loading Strategy
- Lazy loading of components
- Preloading of essential data
- Efficient state updates
- Optimized resource loading
#### UI Responsiveness
- Smooth transitions
- Non-blocking operations
- Efficient DOM updates
- Responsive design
### 8. Browser Compatibility
#### Cross-browser Support
- Modern browser features
- Fallback mechanisms
- Consistent rendering
- Progressive enhancement
#### Mobile Support
- Responsive design
- Touch-friendly interface
- Mobile-optimized layout
- Adaptive UI elements
### 9. User Experience
#### Feedback Mechanisms
- Loading indicators
- Progress updates
- Status messages
- Error notifications
#### Navigation
- Clear call-to-actions
- Intuitive flow
- Easy access to features
- Consistent navigation
### 10. Maintenance and Updates
#### Code Organization
- Modular structure
- Clear separation of concerns
- Maintainable components
- Extensible design
#### Future Enhancements
- Feature addition points
- Integration capabilities
- Scalability considerations
- Update mechanisms
## Component Interactions
### 1. Initial Page Load
```javascript
document.addEventListener('DOMContentLoaded', async () => {
// Check for existing exam
await checkCurrentExamStatus();
// Preload labs data
await fetchLabs(false);
// Initialize UI elements
initializeUI();
});
```
### 2. Exam Selection Flow
```javascript
// Start Exam Button Click
document.getElementById('startExamBtn').addEventListener('click', async () => {
// Check for active exam
const currentExam = await checkCurrentExamStatus();
if (currentExam) {
showActiveExamWarningModal(currentExam);
} else {
showExamSelectionModal();
}
});
// Exam Category Selection
document.getElementById('examCategory').addEventListener('change', async (e) => {
const category = e.target.value;
await loadExamOptions(category);
updateExamDescription();
});
// Start Selected Exam
document.getElementById('startSelectedExam').addEventListener('click', async () => {
const examId = document.getElementById('examName').value;
await startSelectedExam(examId);
});
```
### 3. Loading State Management
```javascript
// Show Loading Overlay
function showLoadingOverlay(message) {
document.getElementById('loadingOverlay').style.display = 'flex';
document.getElementById('loadingMessage').textContent = message;
}
// Update Progress
function updateProgress(progress) {
document.getElementById('progressBar').style.width = `${progress}%`;
}
// Hide Loading Overlay
function hideLoadingOverlay() {
document.getElementById('loadingOverlay').style.display = 'none';
}
```
## API Integration
### 1. Exam Status Check
```javascript
async function checkCurrentExamStatus() {
try {
const response = await fetch('/facilitator/api/v1/exams/current');
if (response.ok) {
const data = await response.json();
return data;
}
return null;
} catch (error) {
console.error('Error checking exam status:', error);
return null;
}
}
```
### 2. Labs Data Fetching
```javascript
async function fetchLabs(showLoader = true) {
try {
if (showLoader) showLoadingOverlay('Loading available labs...');
const response = await fetch('/facilitator/api/v1/assements/');
const data = await response.json();
// Update exam options
updateExamOptions(data);
return data;
} catch (error) {
console.error('Error fetching labs:', error);
showError('Failed to load available labs');
return null;
} finally {
if (showLoader) hideLoadingOverlay();
}
}
```
### 3. Exam Session Creation
```javascript
async function startSelectedExam(examId) {
try {
showLoadingOverlay('Creating exam session...');
const response = await fetch('/facilitator/api/v1/exams', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ examId })
});
if (!response.ok) throw new Error('Failed to create exam session');
const data = await response.json();
await pollExamStatus(data.id);
// Redirect to exam page
window.location.href = `/exam.html?id=${data.id}`;
} catch (error) {
console.error('Error starting exam:', error);
showError('Failed to start exam session');
}
}
```
## Event Handlers
### 1. Modal Management
```javascript
// Show Exam Selection Modal
function showExamSelectionModal() {
const modal = new bootstrap.Modal(document.getElementById('examSelectionModal'));
modal.show();
}
// Show Active Exam Warning
function showActiveExamWarningModal(exam) {
const modal = new bootstrap.Modal(document.getElementById('activeExamWarningModal'));
modal.show();
// Update modal content
document.getElementById('examInfo').textContent =
`You have an active ${exam.type} exam session.`;
}
```
### 2. Form Validation
```javascript
// Validate Exam Selection
function validateExamSelection() {
const category = document.getElementById('examCategory').value;
const exam = document.getElementById('examName').value;
const startButton = document.getElementById('startSelectedExam');
startButton.disabled = !category || !exam;
}
// Update Exam Description
function updateExamDescription() {
const exam = document.getElementById('examName').value;
const description = document.getElementById('examDescription');
if (exam) {
const examData = getExamData(exam);
description.textContent = examData.description;
} else {
description.textContent = 'Select an exam to see its description.';
}
}
```
## State Management
### 1. Local Storage
```javascript
// Save Current Exam
function saveCurrentExam(examData) {
localStorage.setItem('currentExamData', JSON.stringify(examData));
localStorage.setItem('currentExamId', examData.id);
}
// Get Current Exam
function getCurrentExam() {
const examData = localStorage.getItem('currentExamData');
return examData ? JSON.parse(examData) : null;
}
// Clear Current Exam
function clearCurrentExam() {
localStorage.removeItem('currentExamData');
localStorage.removeItem('currentExamId');
}
```
### 2. UI State
```javascript
// Update Results Button Visibility
function updateResultsButtonVisibility() {
const container = document.querySelector('.view-results-btn-container');
const currentExam = getCurrentExam();
if (currentExam && currentExam.status === 'EVALUATED') {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
}
// Update Loading State
function updateLoadingState(isLoading, message = '') {
const loader = document.getElementById('pageLoader');
const loaderMessage = document.getElementById('loaderMessage');
if (isLoading) {
loader.style.display = 'flex';
if (message) loaderMessage.textContent = message;
} else {
loader.style.display = 'none';
}
}
```
## Error Handling
### 1. API Error Handling
```javascript
// Show Error Message
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger alert-dismissible fade show';
errorDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').prepend(errorDiv);
// Auto-dismiss after 5 seconds
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
// Handle API Errors
function handleApiError(error, context) {
console.error(`Error in ${context}:`, error);
let message = 'An unexpected error occurred.';
if (error.response) {
switch (error.response.status) {
case 404:
message = 'Resource not found.';
break;
case 403:
message = 'Access denied.';
break;
case 500:
message = 'Server error. Please try again later.';
break;
default:
message = error.response.data.message || message;
}
}
showError(message);
}
```
### 2. Network Error Handling
```javascript
// Check Network Status
function checkNetworkStatus() {
if (!navigator.onLine) {
showError('No internet connection. Please check your network.');
return false;
}
return true;
}
// Network Status Event Listeners
window.addEventListener('online', () => {
showError('Connection restored. You can continue.');
});
window.addEventListener('offline', () => {
showError('No internet connection. Please check your network.');
});
```
## Conclusion
The `index.html` file serves as the entry point for the CK-X Simulator, providing a user-friendly interface for exam selection and management. It implements robust error handling, state management, and API integration to ensure a smooth user experience. The modular design allows for easy maintenance and future enhancements.
Key aspects of the implementation include:
- Clean separation of concerns between UI and business logic
- Comprehensive error handling and user feedback
- Efficient state management using localStorage
- Responsive design for various screen sizes
- Clear user flow for exam selection and management

View File

@@ -0,0 +1,571 @@
# CK-X Simulator: results.html Functionality Documentation
This document provides a detailed technical overview of the `results.html` file, focusing on its structure, interactions, and API calls.
## Table of Contents
1. [HTML Structure](#html-structure)
2. [Functionality Overview](#functionality-overview)
3. [Component Interactions](#component-interactions)
4. [API Integration](#api-integration)
5. [Event Handlers](#event-handlers)
6. [State Management](#state-management)
7. [Error Handling](#error-handling)
## HTML Structure
The `results.html` file serves as the results interface for the CK-X Simulator. Here's a detailed breakdown of its structure:
### 1. Head Section
```html
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exam Results - CK-X Simulator</title>
<!-- External Dependencies -->
<link rel="stylesheet" href="/css/styles.css">
<link rel="stylesheet" href="/css/results.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
```
### 2. Body Components
#### Action Buttons
```html
<div class="results-actions">
<button id="dashboardBtn" class="btn btn-secondary">
<i class="fas fa-home me-2"></i>Dashboard
</button>
<button id="currentExamBtn" class="btn btn-info">
<i class="fas fa-tasks me-2"></i>Current Exam
</button>
<button id="viewAnswersBtn" class="btn btn-success">
<i class="fas fa-check-square me-2"></i>View Answers
</button>
<button id="reEvaluateBtn" class="btn btn-primary">
<i class="fas fa-sync-alt me-2"></i>Re-evaluate Exam
</button>
<button id="terminateBtn" class="btn btn-danger">
<i class="fas fa-power-off me-2"></i>Terminate Session
</button>
</div>
```
- **Purpose**: Main navigation and action controls
- **Features**: Dashboard access, exam management, answer review, re-evaluation, session termination
#### Loading State
```html
<div id="pageLoader" class="page-loader">
<div class="spinner"></div>
<p id="loaderMessage">Loading exam results...</p>
</div>
```
- **Purpose**: Displays during results loading
- **State Management**: Controlled by `showLoader()` and `hideLoader()` functions
#### Error Message
```html
<div id="errorMessage" class="error-message" style="display: none;">
<i class="fas fa-exclamation-circle"></i>
<p id="errorText">An error occurred while loading the results.</p>
<button id="retryButton" class="button">Retry</button>
</div>
```
- **Purpose**: Displays error states with retry option
- **Error Handling**: Controlled by `showError()` and `hideError()` functions
#### Results Content
```html
<div id="resultsContent" class="results-content" style="display: none;">
<div class="results-header">
<h2>Exam Results</h2>
<div class="exam-info">
<p id="examId">Exam ID: Loading...</p>
<p id="completedAt">Completed: Loading...</p>
</div>
</div>
<!-- Results content -->
</div>
```
- **Purpose**: Displays exam results and statistics
- **Dynamic Content**: Updated based on API responses
#### Terminate Modal
```html
<div id="terminateModal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<!-- Modal content -->
</div>
</div>
```
- **Purpose**: Confirms session termination
- **Safety Features**: Clear warning messages and confirmation required
## Functionality Overview
The `results.html` file provides a comprehensive results interface for the CK-X Simulator. Here's a detailed breakdown of its functionality:
### 1. Results Display Components
#### Score Summary
1. **Score Display**
- Total score calculation
- Maximum possible score
- Percentage calculation
- Visual score representation
2. **Rank Badge**
- Performance level indicator
- Visual status representation
- Dynamic color coding
#### Question Breakdown
1. **Question List**
- Individual question scores
- Correct/incorrect indicators
- Detailed feedback
- Answer review options
2. **Performance Metrics**
- Time taken per question
- Difficulty level indicators
- Success rate analysis
### 2. Action Management
#### Navigation Controls
1. **Dashboard Access**
- Return to main dashboard
- Session history access
- Performance overview
2. **Exam Management**
- Current exam access
- Session continuation
- Environment restoration
#### Review Options
1. **Answer Review**
- Detailed answer display
- Correct solution comparison
- Learning resources
2. **Re-evaluation**
- Score recalculation
- Performance reassessment
- Result updates
### 3. State Management
#### Results State
- Score tracking
- Question status
- Performance metrics
- Session information
#### UI State
- Loading states
- Error states
- Modal visibility
- Content display
### 4. API Integration
#### Endpoints
1. **Results Data**
- `/facilitator/api/v1/exams/{id}/results`
- Score retrieval
- Performance data
2. **Question Details**
- `/facilitator/api/v1/exams/{id}/questions`
- Answer review
- Solution access
3. **Session Management**
- `/facilitator/api/v1/exams/{id}/session`
- Status updates
- Environment control
## Component Interactions
### 1. Initial Results Load Sequence
```mermaid
sequenceDiagram
participant User
participant Browser
participant UI
participant API
participant State
User->>Browser: Access results.html
Browser->>UI: DOMContentLoaded
UI->>UI: Show Loader
UI->>API: Get examId from URL
API-->>UI: Return examId
UI->>API: fetchExamResults(examId)
API-->>UI: Return results data
UI->>State: processResults()
State-->>UI: Processed data
UI->>UI: renderResults()
UI->>UI: Hide Loader
UI-->>User: Display results
```
### 2. Results Display Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant State
UI->>API: requestDetailedResults()
API-->>UI: Return detailed data
UI->>State: updateResultsState()
State-->>UI: Updated state
UI->>UI: updateScoreDisplay()
UI->>UI: updateQuestionList()
UI->>UI: updatePerformanceMetrics()
UI-->>User: Updated display
```
### 3. Answer Review Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant State
User->>UI: Click "View Answers"
UI->>API: requestQuestionDetails()
API-->>UI: Return question data
UI->>State: saveCurrentQuestion()
UI->>UI: displayAnswerDetails()
UI->>UI: showSolution()
UI-->>User: Display answer review
```
### 4. Re-evaluation Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant State
User->>UI: Click "Re-evaluate"
UI->>UI: showConfirmation()
User->>UI: Confirm re-evaluation
UI->>API: requestReEvaluation()
API-->>UI: Return new results
UI->>State: updateResults()
UI->>UI: refreshDisplay()
UI-->>User: Updated results
```
### 5. Session Termination Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant State
User->>UI: Click "Terminate"
UI->>UI: showTerminateModal()
User->>UI: Confirm termination
UI->>API: requestTermination()
API-->>UI: Confirmation
UI->>State: clearSessionData()
UI->>UI: redirectToDashboard()
UI-->>User: Redirected
```
### 6. Error Recovery Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant API
participant State
Note over UI: Error detected
UI->>UI: showError(message)
User->>UI: Click "Retry"
UI->>API: retryRequest()
alt Success
API-->>UI: Return data
UI->>State: updateState()
UI-->>User: Display results
else Failure
API-->>UI: Error response
UI->>UI: showError()
end
```
### 7. State Management Flow
```mermaid
sequenceDiagram
participant User
participant UI
participant State
participant localStorage
Note over UI: Save state
UI->>State: saveResultsState()
State->>localStorage: setItem('resultsState')
Note over UI: Page reload
UI->>localStorage: getItem('resultsState')
localStorage-->>UI: State data
UI->>State: restoreState()
State->>UI: Update display
UI-->>User: State restored
```
## API Integration
### 1. Results Fetching
```javascript
async function fetchExamResults(examId) {
try {
const response = await fetch(`/facilitator/api/v1/exams/${examId}/results`);
if (!response.ok) throw new Error('Failed to fetch results');
const data = await response.json();
processResults(data);
return data;
} catch (error) {
console.error('Error fetching results:', error);
showError('Failed to load exam results');
return null;
}
}
```
### 2. Question Details
```javascript
async function fetchQuestionDetails(questionId) {
try {
const response = await fetch(`/facilitator/api/v1/exams/${examId}/questions/${questionId}`);
if (!response.ok) throw new Error('Failed to fetch question details');
const data = await response.json();
displayQuestionDetails(data);
return data;
} catch (error) {
console.error('Error fetching question details:', error);
showError('Failed to load question details');
return null;
}
}
```
### 3. Session Management
```javascript
async function terminateSession() {
try {
const response = await fetch(`/facilitator/api/v1/exams/${examId}/terminate`, {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to terminate session');
clearSessionData();
redirectToDashboard();
return true;
} catch (error) {
console.error('Error terminating session:', error);
showError('Failed to terminate session');
return false;
}
}
```
## Event Handlers
### 1. Button Actions
```javascript
// Dashboard Navigation
document.getElementById('dashboardBtn').addEventListener('click', () => {
window.location.href = '/dashboard';
});
// Current Exam Access
document.getElementById('currentExamBtn').addEventListener('click', () => {
window.location.href = `/exam?id=${examId}`;
});
// Answer Review
document.getElementById('viewAnswersBtn').addEventListener('click', async () => {
await loadQuestionDetails();
showAnswerReview();
});
// Re-evaluation
document.getElementById('reEvaluateBtn').addEventListener('click', async () => {
if (confirm('Are you sure you want to re-evaluate the exam?')) {
await reEvaluateExam();
}
});
// Session Termination
document.getElementById('terminateBtn').addEventListener('click', () => {
showTerminateModal();
});
```
### 2. Modal Management
```javascript
// Show Terminate Modal
function showTerminateModal() {
document.getElementById('terminateModal').style.display = 'block';
}
// Hide Terminate Modal
function hideTerminateModal() {
document.getElementById('terminateModal').style.display = 'none';
}
// Handle Modal Actions
document.getElementById('confirmTerminateBtn').addEventListener('click', async () => {
await terminateSession();
hideTerminateModal();
});
document.getElementById('cancelTerminateBtn').addEventListener('click', () => {
hideTerminateModal();
});
```
## State Management
### 1. Results State
```javascript
// Save Results State
function saveResultsState() {
const state = {
examId: currentExamId,
results: currentResults,
timestamp: Date.now()
};
localStorage.setItem('resultsState', JSON.stringify(state));
}
// Load Results State
function loadResultsState() {
const state = localStorage.getItem('resultsState');
if (state) {
const parsedState = JSON.parse(state);
restoreResultsState(parsedState);
}
}
// Clear Results State
function clearResultsState() {
localStorage.removeItem('resultsState');
}
```
### 2. UI State
```javascript
// Update UI State
function updateUIState() {
updateScoreDisplay();
updateQuestionList();
updatePerformanceMetrics();
updateActionButtons();
}
// Save UI Preferences
function saveUIPreferences() {
const preferences = {
showDetails: showDetailedResults,
sortOrder: currentSortOrder,
filterSettings: currentFilters
};
localStorage.setItem('uiPreferences', JSON.stringify(preferences));
}
// Load UI Preferences
function loadUIPreferences() {
const preferences = localStorage.getItem('uiPreferences');
if (preferences) {
const parsedPreferences = JSON.parse(preferences);
applyUIPreferences(parsedPreferences);
}
}
```
## Error Handling
### 1. Error Display
```javascript
// Show Error Message
function showError(message) {
const errorElement = document.getElementById('errorMessage');
const errorText = document.getElementById('errorText');
errorText.textContent = message;
errorElement.style.display = 'block';
// Auto-hide after 5 seconds
setTimeout(() => {
hideError();
}, 5000);
}
// Hide Error Message
function hideError() {
document.getElementById('errorMessage').style.display = 'none';
}
// Handle Retry
document.getElementById('retryButton').addEventListener('click', async () => {
hideError();
await loadResults();
});
```
### 2. State Recovery
```javascript
// Attempt State Recovery
async function attemptStateRecovery() {
try {
const state = loadResultsState();
if (state && state.examId) {
await restoreResultsState(state);
return true;
}
return false;
} catch (error) {
console.error('State Recovery Error:', error);
return false;
}
}
// Handle Session Timeout
function handleSessionTimeout() {
showError('Session timeout. Attempting to recover...');
attemptStateRecovery();
}
```
## Conclusion
The `results.html` file provides a comprehensive results interface for the CK-X Simulator, implementing robust features for displaying exam results, managing sessions, and facilitating review and re-evaluation. The modular design ensures maintainability and extensibility while providing a clear and informative results experience.
Key aspects of the implementation include:
- Detailed score and performance display
- Comprehensive question breakdown
- Flexible review options
- Robust error handling
- Secure session management
- Responsive and intuitive interface

57
facilitator/.dockerignore Normal file
View File

@@ -0,0 +1,57 @@
# Dependency directories
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
# Build artifacts
dist/
build/
*.tar.gz
*.tgz
# Environment files
.env
.env.local
.env.development
.env.test
.env.production
# Docker files
Dockerfile
docker-compose*.yml
compose*.yaml
.dockerignore
# Logs
logs/
*.log
# Testing
coverage/
.nyc_output/
.coverage
.pytest_cache/
__pycache__/
*.pyc
*.pyo
*.pyd
# OS specific
.DS_Store
Thumbs.db
# Editor directories and files
.idea/
.vscode/
*.swp
*.swo
*~
# Other unnecessary files
*.gz
*.zip
*.tar
*.rar
tmp/
temp/

32
facilitator/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Dependencies
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs/
*.log
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage
coverage/
# Misc
.DS_Store
.idea/
.vscode/
*.swp
*.swo

48
facilitator/Dockerfile Normal file
View File

@@ -0,0 +1,48 @@
FROM node:18-alpine
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies and SSH client for connecting to jumphost
RUN apk add --no-cache openssh-client
# Install dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Bundle app source
COPY . .
# Create logs directory
RUN mkdir -p logs
# Create a non-root user and switch to it
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeuser -u 1001 -G nodejs
# Setup SSH for passwordless authentication
RUN mkdir -p /home/nodeuser/.ssh && \
chmod 700 /home/nodeuser/.ssh && \
echo "Host jumphost\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /home/nodeuser/.ssh/config && \
chmod 600 /home/nodeuser/.ssh/config && \
chown -R nodeuser:nodejs /home/nodeuser/.ssh
# Change ownership of the app files to the non-root user
RUN chown -R nodeuser:nodejs /usr/src/app
# Expose the service port
EXPOSE 3000
# Copy entrypoint.sh to /usr/src/app
COPY entrypoint.sh /home/nodeuser/entrypoint.sh
# Make entrypoint.sh executable
RUN chmod +x /home/nodeuser/entrypoint.sh
# Switch to non-root user
USER nodeuser
ENTRYPOINT ["/bin/sh", "/home/nodeuser/entrypoint.sh"]

125
facilitator/README.md Normal file
View File

@@ -0,0 +1,125 @@
# Facilitator Service
A Node.js service that provides SSH jumphost functionality and exam management capabilities via a REST API.
## Features
- Execute commands on a remote SSH jumphost
- Support for both password and passwordless SSH authentication
- Exam management API endpoints (some implemented, others are placeholders)
- Secure and modular architecture
- Comprehensive logging
- Containerization with Docker
- Integration with Docker Compose for multi-service deployment
## Prerequisites
- Node.js 18+
- npm or yarn
- SSH access to the target jumphost
## Installation
### Local Development
1. Clone the repository
2. Navigate to the project directory
3. Install dependencies:
```bash
npm install
```
4. Create a `.env` file based on the provided example:
```
PORT=3000
NODE_ENV=development
# SSH Jumphost Configuration
SSH_HOST=<your-ssh-host>
SSH_PORT=22
SSH_USERNAME=<your-ssh-username>
SSH_PASSWORD=<your-ssh-password>
# Alternatively, use SSH key authentication
# SSH_PRIVATE_KEY_PATH=/path/to/private/key
# Logging Configuration
LOG_LEVEL=info
```
5. Start the development server:
```bash
npm run dev
```
### Docker Deployment
#### Standalone
1. Build the Docker image:
```bash
docker build -t facilitator-service .
```
2. Run the container:
```bash
docker run -p 3001:3000 --env-file .env facilitator-service
```
#### Docker Compose
The facilitator service is integrated into the main Docker Compose configuration at the project root. To run it with the full stack:
```bash
cd ..
docker compose up -d
```
This will start the facilitator service along with all other services, including the jumphost that the facilitator connects to for SSH command execution. In Docker Compose, the service is configured to use passwordless SSH authentication with the jumphost.
The service is accessible at:
- URL: http://localhost:3001
- Internal network name: facilitator
## API Endpoints
### SSH Command Execution
- **POST /api/v1/execute**
- Execute a command on the SSH jumphost
- Request body: `{ "command": "your-command-here" }`
- Response: `{ "exitCode": 0, "stdout": "output", "stderr": "errors" }`
### Exam Management
- **GET /api/v1/exams/**
- Get a list of all exams
- Returns an array of exam objects containing id, name, category, description, etc.
- **POST /api/v1/exams/**
- Create a new exam
- Returns exam ID and type (placeholder)
- **GET /api/v1/exams/:examId/assets**
- Get assets for a specific exam
- Returns empty object (placeholder)
- **GET /api/v1/exams/:examId/questions/**
- Get questions for a specific exam
- Returns empty object (placeholder)
- **POST /api/v1/exams/:examId/evaluate/**
- Evaluate an exam
- Returns empty object (placeholder)
- **POST /api/v1/exams/:examId/end**
- End an exam
- Returns empty object (placeholder)
## License
ISC

View File

@@ -0,0 +1,564 @@
# CKAD Practice Exam - Answers
## Question 1: Create a deployment called nginx-deployment in the namespace dev with 3 replicas and image nginx:latest
```bash
# Create the namespace if it doesn't exist
kubectl create namespace dev
# Create the deployment with 3 replicas
kubectl create deployment nginx-deployment -n dev --image=nginx:latest --replicas=3
```
## Question 2: Create a PersistentVolume named 'pv-storage' with 1Gi capacity, access mode ReadWriteOnce, hostPath type at /mnt/data, and reclaim policy Retain
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-storage
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data
```
Save this as `pv-storage.yaml` and apply:
```bash
kubectl apply -f pv-storage.yaml
```
## Question 3: Create a StorageClass named 'fast-storage' with provisioner 'kubernetes.io/no-provisioner' and volumeBindingMode 'WaitForFirstConsumer'
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
```
Save this as `storage-class.yaml` and apply:
```bash
kubectl apply -f storage-class.yaml
```
## Question 4: Create a PersistentVolumeClaim named 'pvc-app' that requests 500Mi of storage with ReadWriteOnce access mode and uses the 'fast-storage' StorageClass
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-app
namespace: storage-test
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
storageClassName: fast-storage
```
Save this as `pvc.yaml` and apply:
```bash
kubectl apply -f pvc.yaml -n storage-test
```
## Question 5: The deployment 'broken-app' in namespace 'troubleshooting' is failing to start. Identify and fix the issue
Troubleshooting steps:
```bash
# Check the pods in the troubleshooting namespace
kubectl get pods -n troubleshooting
# Check the pod details for errors
kubectl describe pod -l app=broken-app -n troubleshooting
# Check logs of the failing pod
kubectl logs <pod-name> -n troubleshooting
```
Potential fixes:
1. If the image is incorrect:
```bash
kubectl set image deployment/broken-app container-name=correct-image:tag -n troubleshooting
```
2. If environment variables are missing:
```bash
kubectl edit deployment broken-app -n troubleshooting
```
3. If resource limits are too low:
```bash
kubectl patch deployment broken-app -n troubleshooting -p '{"spec":{"template":{"spec":{"containers":[{"name":"container-name","resources":{"limits":{"memory":"512Mi"}}}]}}}}'
```
## Question 6: The kubelet on node 'worker-1' is not functioning properly. Diagnose and fix the issue
Troubleshooting steps:
```bash
# Check node status
kubectl get nodes
# Describe the node for more information
kubectl describe node worker-1
# SSH into the worker node
ssh worker-1
# Check kubelet status
systemctl status kubelet
# Check kubelet logs
journalctl -u kubelet -n 100
# Restart kubelet if needed
systemctl restart kubelet
# Check kubelet configuration
cat /var/lib/kubelet/config.yaml
```
Common kubelet issues:
1. Service not running: `systemctl start kubelet`
2. Configuration errors: Edit `/var/lib/kubelet/config.yaml`
3. Certificate issues: Renew certificates if needed
4. Disk space issues: `df -h` to check and clean up if needed
## Question 7: Service 'web-service' in namespace 'troubleshooting' is not routing traffic to pods properly. Identify and fix the issue
Troubleshooting steps:
```bash
# Check the service
kubectl get svc web-service -n troubleshooting
# Describe the service to check selector labels
kubectl describe svc web-service -n troubleshooting
# Check if there are pods matching the selector
kubectl get pods -l <service-selector-label> -n troubleshooting
```
Common fixes:
1. Fix service selector to match pod labels:
```bash
kubectl edit svc web-service -n troubleshooting
```
2. Fix pod labels to match service selector:
```bash
kubectl label pods <pod-name> key=value -n troubleshooting
```
3. Fix service port mapping:
```bash
kubectl edit svc web-service -n troubleshooting
```
## Question 8: Pod 'logging-pod' in namespace 'troubleshooting' is experiencing high CPU usage. Identify the container causing the issue and take appropriate action to limit its CPU usage
```bash
# Check current resource usage
kubectl top pod logging-pod -n troubleshooting
kubectl top pod logging-pod -n troubleshooting --containers
# Add CPU limits to the container
kubectl patch pod logging-pod -n troubleshooting -p '{"spec":{"containers":[{"name":"<container-name>","resources":{"limits":{"cpu":"200m"}}}]}}'
```
Or edit the deployment if the pod is managed by one:
```bash
kubectl edit deployment <deployment-name> -n troubleshooting
```
Add the following to the container spec:
```yaml
resources:
limits:
cpu: 200m
requests:
cpu: 100m
```
## Question 9: Create a ConfigMap named 'app-config' in namespace 'workloads' containing the following key-value pairs: APP_ENV=production, LOG_LEVEL=info. Then create a Pod named 'config-pod' using 'nginx' image that mounts these configurations as environment variables
```bash
# Create the ConfigMap
kubectl create configmap app-config -n workloads --from-literal=APP_ENV=production --from-literal=LOG_LEVEL=info
```
Create the Pod with ConfigMap environment variables:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: config-pod
namespace: workloads
spec:
containers:
- name: nginx
image: nginx
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
```
Save as `config-pod.yaml` and apply:
```bash
kubectl apply -f config-pod.yaml
```
## Question 10: Create a Secret named 'db-credentials' in namespace 'workloads' containing username=admin and password=securepass. Then create a Pod named 'secure-pod' using 'mysql:5.7' image with these credentials set as environment variables DB_USER and DB_PASSWORD
```bash
# Create the Secret
kubectl create secret generic db-credentials -n workloads --from-literal=username=admin --from-literal=password=securepass
```
Create the Pod with Secret environment variables:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: workloads
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
```
Save as `secure-pod.yaml` and apply:
```bash
kubectl apply -f secure-pod.yaml
```
## Question 11: Create a Horizontal Pod Autoscaler for the deployment 'web-app' in namespace 'workloads' that scales between 2 and 6 replicas based on 70% CPU utilization
```bash
# Create the HPA
kubectl autoscale deployment web-app -n workloads --min=2 --max=6 --cpu-percent=70
```
Or using YAML:
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app
namespace: workloads
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
```
Save as `hpa.yaml` and apply:
```bash
kubectl apply -f hpa.yaml
```
## Question 12: Create a Pod named 'health-pod' in namespace 'workloads' using 'nginx' image with a liveness probe that checks the path /healthz on port 80 every 15 seconds, and a readiness probe that checks port 80 every 10 seconds
```yaml
apiVersion: v1
kind: Pod
metadata:
name: health-pod
namespace: workloads
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 30
periodSeconds: 15
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 5
periodSeconds: 10
```
Save as `health-pod.yaml` and apply:
```bash
kubectl apply -f health-pod.yaml
```
## Question 13: Create a ClusterRole named 'pod-reader' that allows getting, watching, and listing pods. Then create a ClusterRoleBinding named 'read-pods' that grants this role to the user 'jane' in the namespace 'cluster-admin'
```bash
# Create the ClusterRole
kubectl create clusterrole pod-reader --verb=get,watch,list --resource=pods
```
Or using YAML:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
```
Create the ClusterRoleBinding:
```bash
kubectl create clusterrolebinding read-pods --clusterrole=pod-reader --user=jane --namespace=cluster-admin
```
Or using YAML:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: read-pods
subjects:
- kind: User
name: jane
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: pod-reader
apiGroup: rbac.authorization.k8s.io
```
Save YAML files and apply them:
```bash
kubectl apply -f cluster-role.yaml
kubectl apply -f cluster-role-binding.yaml
```
## Question 14: Install Helm and use it to deploy the Prometheus monitoring stack in the 'monitoring' namespace
```bash
# Install Helm
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
# Create the namespace
kubectl create namespace monitoring
# Add Prometheus repo
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# Install Prometheus stack
helm install prometheus prometheus-community/kube-prometheus-stack --namespace monitoring
```
## Question 15: Create a CRD (CustomResourceDefinition) for a new resource type 'Backup' in API group 'data.example.com' with version 'v1alpha1' that includes fields 'spec.source' and 'spec.destination'
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: backups.data.example.com
spec:
group: data.example.com
names:
kind: Backup
listKind: BackupList
plural: backups
singular: backup
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
source:
type: string
destination:
type: string
required: ["source", "destination"]
```
Save as `backup-crd.yaml` and apply:
```bash
kubectl apply -f backup-crd.yaml
```
## Question 16: Create a NetworkPolicy named 'allow-traffic' in namespace 'networking' that allows traffic to pods with label 'app=web' only from pods with label 'tier=frontend' on port 80
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-traffic
namespace: networking
spec:
podSelector:
matchLabels:
app: web
ingress:
- from:
- podSelector:
matchLabels:
tier: frontend
ports:
- protocol: TCP
port: 80
```
Save as `network-policy.yaml` and apply:
```bash
kubectl apply -f network-policy.yaml
```
## Question 17: Create a ClusterIP service named 'internal-app' in namespace 'networking' that routes traffic to pods with label 'app=backend' on port 8080, exposing the service on port 80
```bash
kubectl create service clusterip internal-app --tcp=80:8080 -n networking --selector=app=backend
```
Or using YAML:
```yaml
apiVersion: v1
kind: Service
metadata:
name: internal-app
namespace: networking
spec:
type: ClusterIP
selector:
app: backend
ports:
- port: 80
targetPort: 8080
protocol: TCP
```
Save as `internal-service.yaml` and apply:
```bash
kubectl apply -f internal-service.yaml
```
## Question 18: Create a LoadBalancer service named 'public-web' in namespace 'networking' that exposes port 80 for the deployment 'web-frontend'
```bash
kubectl expose deployment web-frontend --type=LoadBalancer --port=80 --name=public-web -n networking
```
Or using YAML:
```yaml
apiVersion: v1
kind: Service
metadata:
name: public-web
namespace: networking
spec:
type: LoadBalancer
selector:
app: web-frontend
ports:
- port: 80
targetPort: 80
protocol: TCP
```
Save as `loadbalancer-service.yaml` and apply:
```bash
kubectl apply -f loadbalancer-service.yaml
```
## Question 19: Create an Ingress resource named 'api-ingress' in namespace 'networking' that routes traffic from 'api.example.com' to the service 'api-service' on port 80
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: networking
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
```
Save as `ingress.yaml` and apply:
```bash
kubectl apply -f ingress.yaml
```
## Question 20: Configure CoreDNS to add a custom entry that resolves 'database.local' to the IP address 10.96.0.20
```bash
# Edit the CoreDNS ConfigMap
kubectl edit configmap coredns -n kube-system
```
Add the following to the Corefile data:
```
hosts {
10.96.0.20 database.local
fallthrough
}
```
Restart CoreDNS pods:
```bash
kubectl delete pod -l k8s-app=kube-dns -n kube-system
```
Or create a custom ConfigMap with hosts entries and mount it in the CoreDNS deployment.

View File

@@ -0,0 +1,613 @@
{
"questions": [
{
"id": "1",
"namespace": "dev",
"machineHostname": "ckad9999",
"question": "In this task, you need to set up a basic web application deployment. \n\nCreate a deployment called `nginx-deployment` in the namespace `dev` with `3` replicas using the image `nginx:latest`. \n\nEnsure that the namespace exists before creating the deployment. The deployment should maintain exactly 3 pods running at all times, and all pods should be using the specified nginx image version.",
"concepts": ["deployments", "namespaces", "replicas"],
"verification": [
{
"id": "1",
"description": "Namespace is created",
"verificationScriptFile": "q1_s1_validate_namespace.sh",
"expectedOutput": "0",
"weightage": "1"
},
{
"id": "2",
"description": "Deployment is created",
"verificationScriptFile": "q1_s2_validate_deployment.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "3",
"description": "Deployment is is using correct image nginx",
"verificationScriptFile": "q1_s3_validate_deployment_running.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "4",
"description": "Deployment has 3 replicas",
"verificationScriptFile": "q1_s4_validate_deployment_replicas.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "2",
"namespace": "storage-test",
"machineHostname": "ckad9999",
"question": "For this storage-related task, you need to provision persistent storage resources. \n\nCreate a PersistentVolume named `pv-storage` with exactly `1Gi` capacity. Configure it with access mode `ReadWriteOnce` to allow read-write access by a single node. \n\nUse `hostPath` type pointing to the directory `/mnt/data` on the node. \n\nSet the reclaim policy to `Retain` so that the storage resource is not automatically deleted when the PV is released. \n\nThis PV will be used by applications requiring persistent storage in the cluster.",
"concepts": ["persistent-volumes", "storage", "hostPath"],
"verification": [
{
"id": "1",
"description": "PersistentVolume is created",
"verificationScriptFile": "q2_s1_validate_pv_created.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "PersistentVolume has correct capacity",
"verificationScriptFile": "q2_s2_validate_pv_capacity.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "PersistentVolume has correct access mode",
"verificationScriptFile": "q2_s3_validate_pv_access_mode.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "4",
"description": "PersistentVolume has correct reclaim policy",
"verificationScriptFile": "q2_s4_validate_pv_reclaim_policy.sh",
"expectedOutput": "0",
"weightage": 1
}
]
},
{
"id": "3",
"namespace": "storage-test",
"machineHostname": "ckad9999",
"question": "As part of optimizing storage resources in the cluster, create a StorageClass named `slow-storage` that will dynamically provision storage resources. \n\nUse the provisioner `kubernetes.io/no-provisioner` for this local storage class. \n\nSet the volumeBindingMode to `WaitForFirstConsumer` to delay volume binding until a pod using the PVC is created. \n\nThis storage class will be used for applications that require optimized local storage performance.",
"concepts": ["storage-class", "provisioners", "volume-binding"],
"verification": [
{
"id": "1",
"description": "StorageClass is created",
"verificationScriptFile": "q3_s1_validate_storageclass_created.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "StorageClass has correct provisioner",
"verificationScriptFile": "q3_s2_validate_storageclass_provisioner.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "StorageClass has correct volumeBindingMode",
"verificationScriptFile": "q3_s3_validate_storageclass_binding_mode.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "4",
"namespace": "storage-test",
"machineHostname": "ckad9999",
"question": "An application team needs storage for their database. \n\nCreate a PersistentVolumeClaim named `pvc-app` in the `storage-test` namespace that will request storage from the previously created StorageClass. \n\nThe PVC should request exactly 500Mi of storage capacity and use the `ReadWriteOnce` access mode to ensure data consistency. \n\nMake sure to specify the `fast-storage` StorageClass that you created earlier as the storage class for this claim. \n\nThis PVC will be used by a database application that requires persistent storage.",
"concepts": ["persistent-volume-claims", "storage-class", "access-modes"],
"verification": [
{
"id": "1",
"description": "PersistentVolumeClaim is created",
"verificationScriptFile": "q4_s1_validate_pvc_created.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "PersistentVolumeClaim requests correct storage size",
"verificationScriptFile": "q4_s2_validate_pvc_size.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "PersistentVolumeClaim has correct access mode",
"verificationScriptFile": "q4_s3_validate_pvc_access_mode.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "4",
"description": "PersistentVolumeClaim uses correct StorageClass",
"verificationScriptFile": "q4_s4_validate_pvc_storageclass.sh",
"expectedOutput": "0",
"weightage": 1
}
]
},
{
"id": "5",
"namespace": "troubleshooting",
"machineHostname": "ckad9999",
"question": "The development team has reported an issue with their application deployment. \n\nThe deployment `broken-app` in namespace `troubleshooting` is consistently failing to start and maintain running pods. \n\nYour task is to investigate this deployment, identify the root cause of the failure, and implement the necessary fixes to make the deployment operational. \n\nCheck for issues such as incorrect container image references, resource constraints, configuration problems, or any other factors preventing successful deployment. \n\nDocument your findings and the changes you make to fix the issue.",
"concepts": ["deployments", "troubleshooting", "debugging"],
"verification": [
{
"id": "1",
"description": "Deployment pods are running",
"verificationScriptFile": "q5_s1_validate_pods_running.sh",
"expectedOutput": "0",
"weightage": 3
},
{
"id": "2",
"description": "Deployment has correct container image",
"verificationScriptFile": "q5_s2_validate_container_image.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "6",
"namespace": "troubleshooting",
"machineHostname": "ckad9999",
"question": "The DevOps team needs a specialized pod to help with monitoring and logging. \n\nCreate a multi-container pod named `sidecar-pod` in the `troubleshooting` namespace with the following specifications: \n\n1. Main container: \n - Image: `nginx` \n - Purpose: Serve web content \n\n2. Sidecar container: \n - Image: `busybox` \n - Command: [`sh`, `-c`, `while true; do date >> /var/log/date.log; sleep 10; done`] \n\n3. Shared volume configuration: \n - Volume name: `log-volume` \n - Mount path: `/var/log` in both containers \n\nThis demonstrates the sidecar container pattern for extending application functionality.",
"concepts": ["pods", "multi-container", "volumes", "sidecar-pattern"],
"verification": [
{
"id": "1",
"description": "Pod is created with two containers",
"verificationScriptFile": "q6_s1_validate_multicontainer_pod.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "Pod has shared volume mounted in both containers",
"verificationScriptFile": "q6_s2_validate_shared_volume.sh",
"expectedOutput": "0",
"weightage": 3
}
]
},
{
"id": "7",
"namespace": "troubleshooting",
"machineHostname": "ckad9999",
"question": "Users are reporting connectivity issues with an application. \n\nThe Service `web-service` in namespace `troubleshooting` is supposed to route traffic to backend pods, but it is not functioning correctly. \n\nInvestigate this service and identify what is preventing proper traffic routing. Possible issues could include: \n\n- Mismatched selectors between service and pods \n- Incorrect port configurations \n- Problems with the underlying pods \n\nAfter identifying the issue, implement the necessary fixes to ensure the service correctly routes traffic to the appropriate pods. \n\nVerify your fix by ensuring that service endpoints are properly populated and traffic is forwarded as expected.",
"concepts": ["services", "selectors", "networking", "troubleshooting"],
"verification": [
{
"id": "1",
"description": "Service selector matches pod labels",
"verificationScriptFile": "q7_s1_validate_service_selector.sh",
"expectedOutput": "0",
"weightage": 3
},
{
"id": "2",
"description": "Service ports match container ports",
"verificationScriptFile": "q7_s2_validate_service_ports.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "8",
"namespace": "troubleshooting",
"machineHostname": "ckad9999",
"question": "The operations team has reported performance degradation in the cluster. \n\nPod `logging-pod` in namespace `troubleshooting` is consuming excessive CPU resources, affecting other workloads in the cluster. \n\nYour task is to: \n\n1. Identify which container within the pod is causing the high CPU usage \n\n2. Configure appropriate CPU limits for that container to prevent resource abuse while ensuring the application can still function \n\n3. Implement your solution by modifying the pod specification with the necessary resource constraints \n\nEnsure that the pod continues to run successfully after your changes, but with its CPU usage kept within reasonable bounds as defined by the limits you set.",
"concepts": ["resource-limits", "resource-requests", "cpu-management", "troubleshooting"],
"verification": [
{
"id": "1",
"description": "CPU limits are set for container",
"verificationScriptFile": "q8_s1_validate_cpu_limits.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "Pod is running",
"verificationScriptFile": "q8_s2_validate_pod_running.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "CPU usage under threshold",
"verificationScriptFile": "q8_s3_validate_cpu_usage.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "9",
"namespace": "workloads",
"machineHostname": "ckad9999",
"question": "A development team needs environment-specific configuration for their application. \n\nFirst, create a ConfigMap named `app-config` in namespace `workloads` containing exactly two key-value pairs: `APP_ENV=production` and `LOG_LEVEL=info`. \n\nNext, create a Pod named `config-pod` using the `nginx` image that consumes these configurations as environment variables. \n\nThe pod should be resource-efficient but have guaranteed resources, so configure it with a CPU request of `100m`, a CPU limit of `200m`, a memory request of `128Mi`, and a memory limit of `256Mi`.",
"concepts": ["configmaps", "pods", "environment-variables", "resource-limits"],
"verification": [
{
"id": "1",
"description": "ConfigMap is created with correct data",
"verificationScriptFile": "q9_s1_validate_configmap.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "Pod is created and running",
"verificationScriptFile": "q9_s2_validate_pod_running.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "Pod has environment variables from ConfigMap",
"verificationScriptFile": "q9_s3_validate_pod_env_vars.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "4",
"description": "Pod has correct resource requirements",
"verificationScriptFile": "q9_s4_validate_resources.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "10",
"namespace": "workloads",
"machineHostname": "ckad9999",
"question": "The database team needs to securely deploy a MySQL instance with proper credential management. \n\nCreate a Secret named `db-credentials` in namespace `workloads` containing two sensitive values: `username=admin` and `password=securepass`. \n\nThen create a Pod named `secure-pod` using the `mysql:5.7` image that uses these credentials. \n\nConfigure the pod to access the Secret values as environment variables named `DB_USER` and `DB_PASSWORD` respectively. \n\nThis pattern demonstrates secure handling of sensitive information in Kubernetes without hardcoding credentials in the pod specification. Ensure the MySQL container is properly configured to use these environment variables for authentication.",
"concepts": ["secrets", "pods", "environment-variables", "mysql"],
"verification": [
{
"id": "1",
"description": "Secret is created with correct data",
"verificationScriptFile": "q10_s1_validate_secret.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "Pod is created and running",
"verificationScriptFile": "q10_s2_validate_pod_running.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "Pod has environment variables from Secret",
"verificationScriptFile": "q10_s3_validate_pod_env_vars.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "11",
"namespace": "workloads",
"machineHostname": "ckad9999",
"question": "The system administration team needs an automated solution for log file cleanup. \n\nCreate a CronJob named `log-cleaner` in namespace `workloads` that will automatically manage log files based on age. \n\nConfigure it to run precisely every hour (using a standard cron expression) and use the `busybox` image. The job should execute the command `find /var/log -type f -name \"*.log\" -mtime +7 -delete` to remove log files older than 7 days. \n\nTo prevent resource contention, set the concurrency policy to `Forbid` so that new job executions are skipped if a previous execution is still running. \n\nFor job history management, configure the job to keep exactly `3` successful job completions and `1` failed job in its history.",
"concepts": ["cronjobs", "jobs", "scheduling", "concurrency-policy"],
"verification": [
{
"id": "1",
"description": "CronJob is created with correct name and namespace",
"verificationScriptFile": "q11_s1_validate_cronjob_exists.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "CronJob has correct schedule",
"verificationScriptFile": "q11_s2_validate_cronjob_schedule.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "CronJob has correct command",
"verificationScriptFile": "q11_s3_validate_cronjob_command.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "4",
"description": "CronJob has correct concurrency policy and history limits",
"verificationScriptFile": "q11_s4_validate_cronjob_policy.sh",
"expectedOutput": "0",
"weightage": 1
}
]
},
{
"id": "12",
"namespace": "workloads",
"machineHostname": "ckad9999",
"question": "To ensure application reliability, the team needs to implement health checking for a critical service. \n\nCreate a Pod named `health-pod` in namespace `workloads` using the `nginx` image with the following health monitoring configuration: \n\n1) A liveness probe that performs an HTTP GET request to the path `/healthz` on port `80` every 15 seconds to determine if the container is alive. If this check fails, Kubernetes will restart the container. \n\n2) A readiness probe that checks if the container is ready to serve traffic by testing if port `80` is open and accepting connections every 10 seconds. \n\nConfigure appropriate initial delay, timeout, and failure threshold values based on best practices.",
"concepts": ["pods", "probes", "liveness-probe", "readiness-probe", "health-checks"],
"verification": [
{
"id": "1",
"description": "Pod is created and running",
"verificationScriptFile": "q12_s1_validate_pod_running.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "2",
"description": "Pod has correct liveness probe",
"verificationScriptFile": "q12_s2_validate_liveness_probe.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "3",
"description": "Pod has correct readiness probe",
"verificationScriptFile": "q12_s3_validate_readiness_probe.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "13",
"namespace": "cluster-admin",
"machineHostname": "ckad9999",
"question": "To implement proper access control and security segmentation in the cluster, you need to configure RBAC resources. \n\nFirst, create a `ClusterRole` named `pod-reader` that defines a set of permissions for pod operations. This role should specifically allow three operations on pods: `get` (view individual pods), `watch` (receive notifications about pod changes), and `list` (view collections of pods). \n\nNext, create a `ClusterRoleBinding` named `read-pods` that associates this role with the user `jane` in the namespace `cluster-admin`. \n\nThis binding will grant user `Jane` read-only access to pod resources across all namespaces in the cluster, following the principle of least privilege while allowing her to perform her monitoring duties.",
"concepts": ["rbac", "cluster-role", "cluster-role-binding", "authorization"],
"verification": [
{
"id": "1",
"description": "ClusterRole is created with correct permissions",
"verificationScriptFile": "q13_s1_validate_clusterrole.sh",
"expectedOutput": "0",
"weightage": 3
},
{
"id": "2",
"description": "ClusterRoleBinding is created correctly",
"verificationScriptFile": "q13_s2_validate_clusterrolebinding.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "14",
"namespace": "cluster-admin",
"machineHostname": "ckad9999",
"question": "Deploy the Bitnami Nginx chart in the `web` namespace using Helm. \n\nFirst, add the Bitnami repository (`https://charts.bitnami.com/bitnami`) if not already present. \n\nThen, deploy the `Bitnami` `nginx` chart with exactly `2` replicas to ensure high availability. \n\nVerify the deployment is successful by checking that pods are running and the service is correctly configured.",
"concepts": ["helm", "package-management", "nginx", "services"],
"verification": [
{
"id": "1",
"description": "Bitnami repository is added",
"verificationScriptFile": "q14_s1_validate_helm_repo.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "Bitnami Nginx is deployed with correct configuration",
"verificationScriptFile": "q14_s2_validate_nginx_deployed.sh",
"expectedOutput": "0",
"weightage": 3
}
]
},
{
"id": "15",
"namespace": "cluster-admin",
"machineHostname": "ckad9999",
"question": "To support a custom backup solution, you need to extend the Kubernetes API. \n\nCreate a CustomResourceDefinition (CRD) named `backups.data.example.com` that defines a new resource type `Backup` in API group `data.example.com` with version `v1alpha1`. \n\nThis custom resource should include a schema that validates two required fields: `spec.source` (a string representing the source data location) and `spec.destination` (a string representing where backups should be stored). \n\nConfigure appropriate additional fields like shortNames, scope, and descriptions to make this CRD user-friendly.",
"concepts": ["custom-resource-definitions", "api-extensions", "crds", "api-groups"],
"verification": [
{
"id": "1",
"description": "CRD is created with correct API group and version",
"verificationScriptFile": "q15_s1_validate_crd_api.sh",
"expectedOutput": "0",
"weightage": 3
},
{
"id": "2",
"description": "CRD has required fields in schema",
"verificationScriptFile": "q15_s2_validate_crd_schema.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "16",
"namespace": "networking",
"machineHostname": "ckad9999",
"question": "To enhance security through network segmentation, implement a network policy for a web application. \n\nCreate a NetworkPolicy named `allow-traffic` in namespace `networking` that allows incoming traffic only from pods with the label `tier=frontend` to pods with the label `app=web` on TCP port `80`. \n\nAll other incoming traffic to these pods should be denied. \n\nThis implements the principle of least privilege at the network level, ensuring that the web application can only be accessed by authorized frontend components.",
"concepts": ["network-policies", "pod-security", "network-security", "labels"],
"verification": [
{
"id": "1",
"description": "NetworkPolicy is created",
"verificationScriptFile": "q16_s1_validate_policy_created.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "NetworkPolicy has correct pod selector",
"verificationScriptFile": "q16_s2_validate_pod_selector.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "3",
"description": "NetworkPolicy has correct ingress rules",
"verificationScriptFile": "q16_s3_validate_policy_rules.sh",
"expectedOutput": "0",
"weightage": 1
}
]
},
{
"id": "17",
"namespace": "networking",
"machineHostname": "ckad9999",
"question": "The microservices architecture requires internal service communication. \n\nCreate a ClusterIP service named `internal-app` in namespace `networking` to enable this communication pattern. \n\nConfigure the service to route traffic to pods with the label `app=backend`. The service should accept connections on port `80` and forward them to port `8080` on the backend pods. \n\nClusterIP is the appropriate service type because this communication is entirely internal to the cluster and doesn`t need external exposure. \n\nEnsure the selector exactly matches the pod labels and the port configuration correctly maps the service port to the target port on the pods.",
"concepts": ["services", "clusterip", "networking", "selectors"],
"verification": [
{
"id": "1",
"description": "Service is created with correct type",
"verificationScriptFile": "q17_s1_validate_service_type.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "2",
"description": "Service has correct selector",
"verificationScriptFile": "q17_s2_validate_service_selector.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "3",
"description": "Service has correct port configuration",
"verificationScriptFile": "q17_s3_validate_service_ports.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "18",
"namespace": "networking",
"machineHostname": "ckad9999",
"question": "A public-facing web application needs to be exposed to external users. \n\nCreate a NodePort service named `public-web` in namespace `networking` that will expose the `web-frontend` deployment to external users. \n\nConfigure the service to accept external traffic on port `80` and forward it to port `8080` on the deployment`s pods. Set the NodePort to `30080`. \n\nUsing a `NodePort` service will expose the application on a static port on each node in the cluster, making it accessible via any node`s IP address. \n\nEnsure the service selector correctly targets the `web-frontend` deployment pods and that the port configuration is appropriate for a web application. \n\nThis setup will enable external users to access the web application through `<node-ip>:30080`.",
"concepts": ["services", "nodeport", "networking", "exposing-apps"],
"verification": [
{
"id": "1",
"description": "Service is created with correct type",
"verificationScriptFile": "q18_s1_validate_service_type.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "Service has correct selector",
"verificationScriptFile": "q18_s2_validate_service_selector.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "3",
"description": "Service has correct port configuration",
"verificationScriptFile": "q18_s3_validate_service_ports.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "19",
"namespace": "networking",
"machineHostname": "ckad9999",
"question": "The API team needs to implement host-based routing for their services. \n\nCreate an Ingress resource named `api-ingress` in namespace `networking` that implements the following routing rule: \n\n- All HTTP traffic for the hostname `api.example.com` should be directed to the service `api-service` on port `80`. \n\nThis Ingress will utilize the cluster`s ingress controller to provide more sophisticated HTTP routing than is possible with Services alone. \n\nMake sure to properly configure the host field with the exact domain name and set up the correct backend service reference.",
"concepts": ["ingress", "networking", "host-based-routing", "http-routing"],
"verification": [
{
"id": "1",
"description": "Ingress is created",
"verificationScriptFile": "q19_s1_validate_ingress_created.sh",
"expectedOutput": "0",
"weightage": 1
},
{
"id": "2",
"description": "Ingress has correct host",
"verificationScriptFile": "q19_s2_validate_ingress_host.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "3",
"description": "Ingress has correct service backend",
"verificationScriptFile": "q19_s3_validate_ingress_backend.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "20",
"namespace": "networking",
"machineHostname": "ckad9999",
"question": "A one-time configuration backup operation needs to be performed. \n\nCreate a Kubernetes Job named `backup-job` in the `networking` namespace to handle this task. \n\nThe job should create a single pod using the `busybox` image with a command that copies all files from the directory `/etc/config` to the `/backup` directory. \n\nConfigure the job with the following specifications: \n\n- Set `restartPolicy: Never` to ensure that containers are not restarted after completion or failure \n- Set `backoffLimit: 0` so that the job will not be retried if it fails \n\nThis job represents a one-time, batch operation that should either complete successfully or fail without retries, allowing administrators to then investigate any issues manually. \n\nVerify that the job completes successfully and that the files are properly copied to the destination directory.",
"concepts": ["jobs", "batch-processing", "pods", "containers"],
"verification": [
{
"id": "1",
"description": "Job is created and completes successfully",
"verificationScriptFile": "q20_s1_validate_job_completed.sh",
"expectedOutput": "0",
"weightage": 3
},
{
"id": "2",
"description": "Job has correct configuration",
"verificationScriptFile": "q20_s2_validate_job_config.sh",
"expectedOutput": "0",
"weightage": 2
}
]
},
{
"id": "21",
"namespace": "docker-oci",
"machineHostname": "ckad9999",
"question": "For improved container image distribution and portability, you need to work with the Open Container Initiative (OCI) format. \n\nComplete the following steps: \n\n1. Pull the `nginx:latest` image from Docker Hub using the `docker pull` command \n\n2. Create a directory at `/root/oci-images` if it doesn`t already exist \n\n3. Using the appropriate docker commands, export the nginx image in OCI format and store it in this directory \n\nThis will help standardize the container image format and improve portability across different container runtimes.",
"concepts": ["containers", "images", "oci-format", "docker"],
"verification": [
{
"id": "1",
"description": "OCI directory exists with image content",
"verificationScriptFile": "q21_s1_validate_oci_dir_exists.sh",
"expectedOutput": "0",
"weightage": 2
},
{
"id": "2",
"description": "Nginx image is properly stored in OCI format",
"verificationScriptFile": "q21_s2_validate_nginx_image.sh",
"expectedOutput": "0",
"weightage": 3
}
]
}
]
}

View File

@@ -0,0 +1,10 @@
{
"lab": "ckad-001",
"workerNodes":1 ,
"answers": "assets/exams/ckad/001/answers.md",
"questions": "assessment.json",
"totalMarks": 100,
"lowScore": 40,
"mediumScore": 60,
"highScore": 90
}

View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Setup for Question 10: Create a Secret and use it in a Pod
# Create the workloads namespace if it doesn't exist already
if ! kubectl get namespace workloads &> /dev/null; then
kubectl create namespace workloads
fi
# Delete any existing Secret and Pod with the same names
kubectl delete secret db-credentials -n workloads --ignore-not-found=true
kubectl delete pod secure-pod -n workloads --ignore-not-found=true
echo "Setup complete for Question 10: Environment ready for creating Secret 'db-credentials' and Pod 'secure-pod'"
exit 0

View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Setup for Question 11: Create a CronJob for log cleaning
# Create the workloads namespace if it doesn't exist already
if ! kubectl get namespace workloads &> /dev/null; then
kubectl create namespace workloads
fi
# Delete any existing CronJob with the same name
kubectl delete cronjob log-cleaner -n workloads --ignore-not-found=true
# Create a directory with some sample log files for demonstration
mkdir -p /tmp/var/log
touch /tmp/var/log/test1.log
touch /tmp/var/log/test2.log
touch /tmp/var/log/app.log
touch /tmp/var/log/system.log
echo "Setup complete for Question 11: Environment ready for creating CronJob 'log-cleaner'"
echo "Note: In a real environment, log files would be on the host system. These sample files"
echo " are for demonstration only and won't actually be accessible from the CronJob."
exit 0

View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Setup for Question 12: Create a Pod with liveness and readiness probes
# Create the workloads namespace if it doesn't exist already
if ! kubectl get namespace workloads &> /dev/null; then
kubectl create namespace workloads
fi
# Delete any existing Pod with the same name
kubectl delete pod health-pod -n workloads --ignore-not-found=true
# Create an index.html and healthz endpoint for testing the probes
cat <<EOF > /tmp/index.html
<!DOCTYPE html>
<html>
<head>
<title>CKAD Exam</title>
</head>
<body>
<h1>Welcome to the CKAD Practice Exam!</h1>
</body>
</html>
EOF
cat <<EOF > /tmp/healthz
OK
EOF
echo "Setup complete for Question 12: Environment ready for creating Pod 'health-pod' with liveness and readiness probes"
echo "Note: In a real environment, you would need to set up files at /healthz in the container."
exit 0

View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Setup for Question 14: Install Helm and deploy Bitnami Nginx
# Create the web namespace if it doesn't exist already
if ! kubectl get namespace web &> /dev/null; then
kubectl create namespace web
fi
# Delete any existing helm installations of nginx
if command -v helm &> /dev/null; then
helm uninstall nginx -n web --ignore-not-found
fi
echo "Setup complete for Question 14: Environment ready for installing Helm and deploying Bitnami Nginx"
echo "Note: The candidate should add the Bitnami repo if not already present: helm repo add bitnami https://charts.bitnami.com/bitnami"
exit 0

View File

@@ -0,0 +1,67 @@
#!/bin/bash
# Setup for Question 16: Create a NetworkPolicy
# Create the networking namespace if it doesn't exist already
if ! kubectl get namespace networking &> /dev/null; then
kubectl create namespace networking
fi
# Delete any existing NetworkPolicy with the same name
kubectl delete networkpolicy allow-traffic -n networking --ignore-not-found=true
# Create pods with the required labels for testing the network policy
kubectl delete pod -l app=web -n networking --ignore-not-found=true
kubectl delete pod -l tier=frontend -n networking --ignore-not-found=true
# Create a web pod with label app=web
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: web-pod
namespace: networking
labels:
app: web
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
EOF
# Create a frontend pod with label tier=frontend
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: frontend-pod
namespace: networking
labels:
tier: frontend
spec:
containers:
- name: busybox
image: busybox
command: ["sleep", "3600"]
EOF
# Create a pod with no relevant labels for testing isolation
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: other-pod
namespace: networking
labels:
tier: other
spec:
containers:
- name: busybox
image: busybox
command: ["sleep", "3600"]
EOF
echo "Setup complete for Question 16: Created pods with necessary labels for NetworkPolicy testing"
exit 0

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Setup for Question 17: Create a ClusterIP service
# Create the networking namespace if it doesn't exist already
if ! kubectl get namespace networking &> /dev/null; then
kubectl create namespace networking
fi
# Delete any existing service with the same name
kubectl delete service internal-app -n networking --ignore-not-found=true
# Create backend pods with the required labels
kubectl delete deployment backend-app -n networking --ignore-not-found=true
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-app
namespace: networking
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: nginx
ports:
- containerPort: 8080
command: ["/bin/sh", "-c"]
args: ["nginx -g 'daemon off;' & echo 'Backend service running on port 8080' && sleep infinity"]
EOF
echo "Setup complete for Question 17: Created backend pods with label app=backend for the ClusterIP service"
exit 0

View File

@@ -0,0 +1,39 @@
#!/bin/bash
# Setup for Question 18: Create a LoadBalancer service
# Create the networking namespace if it doesn't exist already
if ! kubectl get namespace networking &> /dev/null; then
kubectl create namespace networking
fi
# Delete any existing service with the same name
kubectl delete service public-web -n networking --ignore-not-found=true
# Create a deployment to be exposed by the LoadBalancer service
kubectl delete deployment web-frontend -n networking --ignore-not-found=true
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-frontend
namespace: networking
spec:
replicas: 3
selector:
matchLabels:
app: web-frontend
template:
metadata:
labels:
app: web-frontend
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
EOF
echo "Setup complete for Question 18: Created deployment 'web-frontend' for the LoadBalancer service"
exit 0

View File

@@ -0,0 +1,57 @@
#!/bin/bash
# Setup for Question 19: Create an Ingress resource
# Create the networking namespace if it doesn't exist already
if ! kubectl get namespace networking &> /dev/null; then
kubectl create namespace networking
fi
# Delete any existing Ingress with the same name
kubectl delete ingress api-ingress -n networking --ignore-not-found=true
# Create a service to be used by the Ingress
kubectl delete service api-service -n networking --ignore-not-found=true
kubectl delete deployment api-backend -n networking --ignore-not-found=true
# Create a deployment for the API service
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-backend
namespace: networking
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: nginx
ports:
- containerPort: 80
EOF
# Create a service for the API deployment
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: networking
spec:
selector:
app: api
ports:
- port: 80
targetPort: 80
EOF
echo "Setup complete for Question 19: Created service 'api-service' for the Ingress resource"
exit 0

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Setup for Question 1: Create a deployment called nginx-deployment in namespace dev
# Create the namespace if it doesn't exist already
if kubectl get namespace dev &> /dev/null; then
kubectl delete namespace dev --ignore-not-found=true
fi
# Delete any existing deployment with the same name to ensure a clean state
kubectl delete deployment nginx-deployment -n dev --ignore-not-found=true
echo "Setup complete for Question 1: Environment ready for creating nginx deployment in namespace 'dev'"
exit 0

View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Setup for Question 20: Create a Job to backup configuration files
# Create the networking namespace if it doesn't exist already
if ! kubectl get namespace networking &> /dev/null; then
kubectl create namespace networking
fi
# Delete any existing job with the same name
kubectl delete job backup-job -n networking --ignore-not-found=true
# Create a ConfigMap with sample configuration files
kubectl delete configmap example-config -n networking --ignore-not-found=true
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: example-config
namespace: networking
data:
nginx.conf: |
server {
listen 80;
server_name example.com;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
app.conf: |
log_level=debug
port=8080
max_connections=100
EOF
echo "Setup complete for Question 20: Environment ready for creating backup Job"
echo "Note: The example ConfigMap is just for reference. In a real environment,"
echo " the student would need to create a Pod that mounts both /etc/config and /backup directories."
exit 0

View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Setup for Question 22: Pull and store nginx image in OCI format
# Create the directory for storing OCI images
mkdir -p /root/oci-images
# Remove any existing content to ensure clean state
rm -rf /root/oci-images/*
# Make sure required tools are installed
if ! command -v docker &> /dev/null; then
echo "Installing docker for image pulling..."
apt-get update
apt-get install -y docker.io
systemctl start docker
fi
echo "Setup complete for Question 22: Environment ready for pulling and storing the nginx image in OCI format"
echo "Task: Pull the nginx:latest image and store it in OCI format in the directory /root/oci-images"
exit 0

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Setup for Question 2: Create a PersistentVolume named 'pv-storage'
# Create the storage-test namespace if it doesn't exist already
if ! kubectl get namespace storage-test &> /dev/null; then
kubectl create namespace storage-test
fi
# Delete any existing PV with the same name to ensure a clean state
kubectl delete pv pv-storage --ignore-not-found=true
# Create the /mnt/data directory on the host if possible (this may require privileged access)
# In a real environment, this would need to be handled by the cluster admin
echo "Note: Ensure /mnt/data directory exists on the node for the hostPath volume"
echo "Setup complete for Question 2: Environment ready for creating PersistentVolume 'pv-storage'"
exit 0

View File

@@ -0,0 +1,9 @@
#!/bin/bash
# Setup for Question 3: Create a StorageClass named 'fast-storage'
# Delete any existing StorageClass with the same name to ensure a clean state
kubectl delete storageclass slow-storage --ignore-not-found=true
echo "Setup complete for Question 3: Environment ready for creating StorageClass 'fast-storage'"
exit 0

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Setup for Question 4: Create a PersistentVolumeClaim named 'pvc-app'
# Create the storage-test namespace if it doesn't exist already
if ! kubectl get namespace storage-test &> /dev/null; then
kubectl create namespace storage-test
fi
# Delete any existing PVC with the same name to ensure a clean state
kubectl delete pvc pvc-app -n storage-test --ignore-not-found=true
# Create the StorageClass if it doesn't exist (dependency for this question)
# if ! kubectl get storageclass fast-storage &> /dev/null; then
# cat <<EOF | kubectl apply -f -
# apiVersion: storage.k8s.io/v1
# kind: StorageClass
# metadata:
# name: fast-storage
# provisioner: kubernetes.io/no-provisioner
# volumeBindingMode: WaitForFirstConsumer
# EOF
# echo "Created dependency: StorageClass 'fast-storage'"
# fi
echo "Setup complete for Question 4: Environment ready for creating PersistentVolumeClaim 'pvc-app'"
exit 0

View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Setup for Question 5: Troubleshoot and fix a broken deployment
# Create the troubleshooting namespace if it doesn't exist already
if ! kubectl get namespace troubleshooting &> /dev/null; then
kubectl create namespace troubleshooting
fi
# Delete any existing deployment with the same name
kubectl delete deployment broken-app -n troubleshooting --ignore-not-found=true
# Create a broken deployment with an invalid image name
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: broken-app
namespace: troubleshooting
spec:
replicas: 2
selector:
matchLabels:
app: broken-app
template:
metadata:
labels:
app: broken-app
spec:
containers:
- name: app
image: nginx:nonexistentversion # This image tag doesn't exist
ports:
- containerPort: 80
EOF
echo "Setup complete for Question 5: Created broken deployment 'broken-app' in namespace 'troubleshooting'"
exit 0

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Setup for Question 6: Multi-container pod with shared volume
# Create the troubleshooting namespace if it doesn't exist already
if ! kubectl get namespace troubleshooting &> /dev/null; then
kubectl create namespace troubleshooting
fi
# Delete any existing pod with the same name
kubectl delete pod sidecar-pod -n troubleshooting --ignore-not-found=true
echo "Setup complete for Question 6: Environment ready for creating a multi-container pod with shared volume"
exit 0

View File

@@ -0,0 +1,54 @@
#!/bin/bash
# Setup for Question 7: Service with incorrect selector not routing traffic to pods
# Create the troubleshooting namespace if it doesn't exist already
if ! kubectl get namespace troubleshooting &> /dev/null; then
kubectl create namespace troubleshooting
fi
# Delete any existing resources with the same names
kubectl delete service web-service -n troubleshooting --ignore-not-found=true
kubectl delete deployment web-app -n troubleshooting --ignore-not-found=true
# Create a deployment with label app=web-app
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: troubleshooting
spec:
replicas: 2
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
EOF
# Create a service with incorrect selector (app=web instead of app=web-app)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: web-service
namespace: troubleshooting
spec:
selector:
app: web # Incorrect selector, should be app=web-app
ports:
- port: 80
targetPort: 80
EOF
echo "Setup complete for Question 7: Created service 'web-service' with incorrect selector"
exit 0

View File

@@ -0,0 +1,34 @@
#!/bin/bash
# Setup for Question 8: Pod with high CPU usage
# Create the troubleshooting namespace if it doesn't exist already
if ! kubectl get namespace troubleshooting &> /dev/null; then
kubectl create namespace troubleshooting
fi
# Delete any existing pod with the same name
kubectl delete pod logging-pod -n troubleshooting --ignore-not-found=true
# Create a pod with a container that has high CPU usage and no resource limits
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: logging-pod
namespace: troubleshooting
spec:
containers:
- name: cpu-consumer
image: busybox
command: ["/bin/sh", "-c"]
args:
- "while true; do echo 'Consuming CPU...'; done"
- name: normal-container
image: nginx
EOF
echo "Setup complete for Question 8: Created pod 'logging-pod' with high CPU usage container"
echo "Note: In a real environment, the 'cpu-consumer' container would actually consume high CPU."
echo " The student needs to identify this container and set appropriate CPU limits."
exit 0

View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Setup for Question 9: Create a ConfigMap and use it in a Pod
# Create the workloads namespace if it doesn't exist already
if ! kubectl get namespace workloads &> /dev/null; then
kubectl create namespace workloads
fi
# Delete any existing ConfigMap and Pod with the same names
kubectl delete configmap app-config -n workloads --ignore-not-found=true
kubectl delete pod config-pod -n workloads --ignore-not-found=true
echo "Setup complete for Question 9: Environment ready for creating ConfigMap 'app-config' and Pod 'config-pod'"
exit 0

View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Validate if the Secret 'db-credentials' exists in the 'workloads' namespace with correct data
USERNAME=$(kubectl get secret db-credentials -n workloads -o jsonpath='{.data.username}' 2>/dev/null | base64 --decode)
PASSWORD=$(kubectl get secret db-credentials -n workloads -o jsonpath='{.data.password}' 2>/dev/null | base64 --decode)
if [ "$USERNAME" = "admin" ] && [ "$PASSWORD" = "securepass" ]; then
echo "Success: Secret 'db-credentials' exists with correct data"
exit 0
else
echo "Error: Secret 'db-credentials' does not have the correct data."
echo "Expected: username=admin, password=securepass"
echo "Found: username=$USERNAME, password=$PASSWORD"
exit 1
fi

View File

@@ -0,0 +1,10 @@
#!/bin/bash
# Validate if the CronJob 'log-cleaner' exists in the 'workloads' namespace
if kubectl get cronjob log-cleaner -n workloads &> /dev/null; then
echo "Success: CronJob 'log-cleaner' exists in namespace 'workloads'"
exit 0
else
echo "Error: CronJob 'log-cleaner' does not exist in namespace 'workloads'"
exit 1
fi

View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Validate if the CronJob 'log-cleaner' has the correct schedule (every hour)
SCHEDULE=$(kubectl get cronjob log-cleaner -n workloads -o jsonpath='{.spec.schedule}' 2>/dev/null)
if [ "$SCHEDULE" = "0 * * * *" ] || [ "$SCHEDULE" = "@hourly" ]; then
echo "Success: CronJob 'log-cleaner' has the correct schedule (every hour): $SCHEDULE"
exit 0
else
echo "Error: CronJob 'log-cleaner' does not have the correct schedule"
echo "Expected: '0 * * * *' or '@hourly', Found: '$SCHEDULE'"
exit 1
fi

View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Validate that the 'health-pod' is running in namespace 'workloads'
NAMESPACE="workloads"
POD_NAME="health-pod"
# Check if the pod exists
POD_STATUS=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.status.phase}' 2>/dev/null)
if [ -z "$POD_STATUS" ]; then
echo "❌ Pod '$POD_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Check if the pod is running
if [ "$POD_STATUS" != "Running" ]; then
echo "❌ Pod '$POD_NAME' exists but is not running (current status: $POD_STATUS)"
# Get additional details about non-running pod
CONTAINER_STATUSES=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.status.containerStatuses[*].state}' 2>/dev/null)
if [ -n "$CONTAINER_STATUSES" ]; then
echo "Container statuses: $CONTAINER_STATUSES"
fi
exit 1
fi
# Check if all containers are ready
READY_COUNT=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.status.containerStatuses[*].ready}' | grep -o "true" | wc -l)
CONTAINER_COUNT=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[*].name}' | wc -w)
if [ "$READY_COUNT" -ne "$CONTAINER_COUNT" ]; then
echo "❌ Pod '$POD_NAME' is running, but not all containers are ready ($READY_COUNT of $CONTAINER_COUNT ready)"
exit 1
fi
# All checks passed
echo "✅ Pod '$POD_NAME' is running successfully in namespace '$NAMESPACE'"
exit 0

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Validate that the 'health-pod' in namespace 'workloads' has a correctly configured liveness probe
NAMESPACE="workloads"
POD_NAME="health-pod"
# Check if the pod exists
kubectl get pod $POD_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Pod '$POD_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Check if liveness probe is configured
LIVENESS_PROBE=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].livenessProbe}' 2>/dev/null)
if [ -z "$LIVENESS_PROBE" ]; then
echo "❌ Liveness probe not configured for pod '$POD_NAME'"
exit 1
fi
# Check if liveness probe is using HTTP GET
HTTP_GET=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].livenessProbe.httpGet}' 2>/dev/null)
if [ -z "$HTTP_GET" ]; then
echo "❌ Liveness probe is not configured to use HTTP GET method"
exit 1
fi
# Check the path
PROBE_PATH=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].livenessProbe.httpGet.path}' 2>/dev/null)
if [ "$PROBE_PATH" != "/healthz" ]; then
echo "❌ Liveness probe path is not configured correctly. Expected '/healthz', got '$PROBE_PATH'"
exit 1
fi
# Check the port
PROBE_PORT=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].livenessProbe.httpGet.port}' 2>/dev/null)
if [ "$PROBE_PORT" != "80" ] && [ "$PROBE_PORT" != "http" ]; then
echo "❌ Liveness probe port is not configured correctly. Expected '80' or 'http', got '$PROBE_PORT'"
exit 1
fi
# Check period seconds (should be 15s as specified in the question)
PERIOD_SECONDS=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].livenessProbe.periodSeconds}' 2>/dev/null)
if [ -z "$PERIOD_SECONDS" ]; then
PERIOD_SECONDS="10" # Default value if not specified
fi
if [ "$PERIOD_SECONDS" != "15" ]; then
echo "⚠️ Liveness probe periodSeconds is set to '$PERIOD_SECONDS', but '15' was specified in the requirements"
fi
# All checks passed
echo "✅ Liveness probe correctly configured to perform HTTP GET requests to '/healthz' on port '$PROBE_PORT'"
echo "✅ Probe configured with periodSeconds: $PERIOD_SECONDS"
# Additional probe details for informational purposes
INITIAL_DELAY=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].livenessProbe.initialDelaySeconds}' 2>/dev/null)
TIMEOUT=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].livenessProbe.timeoutSeconds}' 2>/dev/null)
FAILURE_THRESHOLD=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].livenessProbe.failureThreshold}' 2>/dev/null)
echo " Additional probe settings - initialDelaySeconds: ${INITIAL_DELAY:-N/A}, timeoutSeconds: ${TIMEOUT:-N/A}, failureThreshold: ${FAILURE_THRESHOLD:-N/A}"
exit 0

View File

@@ -0,0 +1,60 @@
#!/bin/bash
# Validate that the 'health-pod' in namespace 'workloads' has a correctly configured readiness probe
NAMESPACE="workloads"
POD_NAME="health-pod"
# Check if the pod exists
kubectl get pod $POD_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Pod '$POD_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Check if readiness probe is configured
READINESS_PROBE=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].readinessProbe}' 2>/dev/null)
if [ -z "$READINESS_PROBE" ]; then
echo "❌ Readiness probe not configured for pod '$POD_NAME'"
exit 1
fi
# The readiness probe should be a TCP socket check
TCP_SOCKET=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].readinessProbe.tcpSocket}' 2>/dev/null)
if [ -z "$TCP_SOCKET" ]; then
echo "❌ Readiness probe is not configured to use TCP socket check"
exit 1
fi
# Check the port
PROBE_PORT=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].readinessProbe.tcpSocket.port}' 2>/dev/null)
if [ "$PROBE_PORT" != "80" ] && [ "$PROBE_PORT" != "http" ]; then
echo "❌ Readiness probe port is not configured correctly. Expected '80' or 'http', got '$PROBE_PORT'"
exit 1
fi
# Check period seconds (should be 10s as specified in the question)
PERIOD_SECONDS=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].readinessProbe.periodSeconds}' 2>/dev/null)
if [ -z "$PERIOD_SECONDS" ]; then
PERIOD_SECONDS="10" # Default value if not specified
fi
if [ "$PERIOD_SECONDS" != "10" ]; then
echo "⚠️ Readiness probe periodSeconds is set to '$PERIOD_SECONDS', but '10' was specified in the requirements"
fi
# All checks passed
echo "✅ Readiness probe correctly configured to check TCP port $PROBE_PORT"
echo "✅ Probe configured with periodSeconds: $PERIOD_SECONDS"
# Additional probe details for informational purposes
INITIAL_DELAY=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].readinessProbe.initialDelaySeconds}' 2>/dev/null)
TIMEOUT=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].readinessProbe.timeoutSeconds}' 2>/dev/null)
FAILURE_THRESHOLD=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[0].readinessProbe.failureThreshold}' 2>/dev/null)
echo " Additional probe settings - initialDelaySeconds: ${INITIAL_DELAY:-N/A}, timeoutSeconds: ${TIMEOUT:-N/A}, failureThreshold: ${FAILURE_THRESHOLD:-N/A}"
exit 0

View File

@@ -0,0 +1,54 @@
#!/bin/bash
# Validate that the ClusterRole 'pod-reader' has the correct permissions for pod operations
CLUSTERROLE_NAME="pod-reader"
# Check if the ClusterRole exists
kubectl get clusterrole $CLUSTERROLE_NAME > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ ClusterRole '$CLUSTERROLE_NAME' not found"
exit 1
fi
# Check if the ClusterRole has rules for pods
RESOURCE_PODS=$(kubectl get clusterrole $CLUSTERROLE_NAME -o jsonpath='{.rules[*].resources}' | grep -o "pods" | wc -l)
if [ $RESOURCE_PODS -eq 0 ]; then
echo "❌ ClusterRole '$CLUSTERROLE_NAME' does not have rules for 'pods' resource"
exit 1
fi
# Check for the required verbs: get, watch, and list
VERBS=$(kubectl get clusterrole $CLUSTERROLE_NAME -o jsonpath='{.rules[?(@.resources[*]=="pods")].verbs[*]}')
echo "🔍 Verifying required verbs in ClusterRole '$CLUSTERROLE_NAME'..."
# Check for 'get' verb
if [[ ! $VERBS =~ "get" ]]; then
echo "❌ ClusterRole '$CLUSTERROLE_NAME' is missing 'get' verb for pods"
exit 1
fi
# Check for 'watch' verb
if [[ ! $VERBS =~ "watch" ]]; then
echo "❌ ClusterRole '$CLUSTERROLE_NAME' is missing 'watch' verb for pods"
exit 1
fi
# Check for 'list' verb
if [[ ! $VERBS =~ "list" ]]; then
echo "❌ ClusterRole '$CLUSTERROLE_NAME' is missing 'list' verb for pods"
exit 1
fi
# Ensure that the ClusterRole doesn't provide excessive permissions
RESOURCE_COUNT=$(kubectl get clusterrole $CLUSTERROLE_NAME -o jsonpath='{.rules[*].resources}' | wc -w)
VERB_COUNT=$(kubectl get clusterrole $CLUSTERROLE_NAME -o jsonpath='{.rules[*].verbs}' | wc -w)
if [ $RESOURCE_COUNT -gt 1 ] || [ $VERB_COUNT -gt 3 ]; then
echo "⚠️ ClusterRole '$CLUSTERROLE_NAME' may have more permissions than necessary for the least privilege principle"
fi
# Success
echo "✅ ClusterRole '$CLUSTERROLE_NAME' correctly allows get, watch, and list operations on pods"
exit 0

View File

@@ -0,0 +1,51 @@
#!/bin/bash
# Validate that the ClusterRoleBinding 'read-pods' correctly associates the 'pod-reader' role with user 'jane'
CLUSTERROLEBINDING_NAME="read-pods"
CLUSTERROLE_NAME="pod-reader"
USER_NAME="jane"
NAMESPACE="cluster-admin"
# Check if the ClusterRoleBinding exists
kubectl get clusterrolebinding $CLUSTERROLEBINDING_NAME > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ ClusterRoleBinding '$CLUSTERROLEBINDING_NAME' not found"
exit 1
fi
# Check if the ClusterRoleBinding references the correct ClusterRole
ROLE_REF=$(kubectl get clusterrolebinding $CLUSTERROLEBINDING_NAME -o jsonpath='{.roleRef.name}' 2>/dev/null)
if [ "$ROLE_REF" != "$CLUSTERROLE_NAME" ]; then
echo "❌ ClusterRoleBinding '$CLUSTERROLEBINDING_NAME' references role '$ROLE_REF' instead of '$CLUSTERROLE_NAME'"
exit 1
fi
# Check the roleRef kind - should be ClusterRole
ROLE_KIND=$(kubectl get clusterrolebinding $CLUSTERROLEBINDING_NAME -o jsonpath='{.roleRef.kind}' 2>/dev/null)
if [ "$ROLE_KIND" != "ClusterRole" ]; then
echo "❌ ClusterRoleBinding '$CLUSTERROLEBINDING_NAME' references a '$ROLE_KIND' instead of a 'ClusterRole'"
exit 1
fi
# Check if the ClusterRoleBinding has a subject for user 'jane'
SUBJECTS=$(kubectl get clusterrolebinding $CLUSTERROLEBINDING_NAME -o json | jq -r '.subjects[] | select(.name=="jane" and .kind=="User")' 2>/dev/null)
if [ -z "$SUBJECTS" ]; then
echo "❌ ClusterRoleBinding '$CLUSTERROLEBINDING_NAME' does not bind to user '$USER_NAME'"
exit 1
fi
# Check if the namespace of the subject is specified as 'cluster-admin'
SUBJECT_NS=$(kubectl get clusterrolebinding $CLUSTERROLEBINDING_NAME -o json | jq -r '.subjects[] | select(.name=="jane" and .kind=="User") | .namespace' 2>/dev/null)
if [ "$SUBJECT_NS" != "$NAMESPACE" ] && [ "$SUBJECT_NS" != "null" ]; then
echo "❌ ClusterRoleBinding subject namespace is '$SUBJECT_NS' instead of '$NAMESPACE'"
exit 1
fi
# Success
echo "✅ ClusterRoleBinding '$CLUSTERROLEBINDING_NAME' correctly associates ClusterRole '$CLUSTERROLE_NAME' with user '$USER_NAME'"
echo "✅ This binding grants Jane read-only access to pod resources across all namespaces"
exit 0

View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Validate that Helm is properly installed in the cluster
# Check if helm command is available
command -v helm > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Helm command not found. Please install Helm."
exit 1
fi
# Check helm version to ensure it's working
HELM_VERSION=$(helm version --short 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$HELM_VERSION" ]; then
echo "❌ Helm is installed but not working properly. Check Helm installation."
exit 1
fi
echo "✅ Helm is correctly installed (version: $HELM_VERSION)"
# Check if the Bitnami repository is added
BITNAMI_REPO=$(helm repo list 2>/dev/null | grep -i bitnami | wc -l)
if [ $BITNAMI_REPO -eq 0 ]; then
echo "⚠️ Bitnami repository is not added to Helm. Add it with: helm repo add bitnami https://charts.bitnami.com/bitnami"
else
echo "✅ Bitnami repository is properly configured"
fi
# Check if helm can access the Kubernetes cluster
HELM_LIST=$(helm list -A 2>/dev/null)
if [ $? -ne 0 ]; then
echo "❌ Helm cannot access the Kubernetes cluster. Check Kubernetes configuration."
exit 1
fi
echo "✅ Helm can successfully communicate with the Kubernetes cluster"
exit 0

View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Validate that the Bitnami repository is added to Helm
# Check if the Bitnami repository is added
BITNAMI_REPO=$(helm repo list 2>/dev/null | grep -i bitnami)
if [ $? -ne 0 ] || [ -z "$BITNAMI_REPO" ]; then
echo "❌ Bitnami repository is not added to Helm"
exit 1
fi
# Extract the URL from the repo list
REPO_URL=$(echo "$BITNAMI_REPO" | awk '{print $2}')
# Check if the URL is correct
if [[ ! "$REPO_URL" =~ "charts.bitnami.com/bitnami" ]]; then
echo "❌ Bitnami repository URL is incorrect: $REPO_URL"
exit 1
fi
echo "✅ Bitnami repository is properly configured with URL: $REPO_URL"
# Check if repo is up to date
LAST_UPDATE=$(helm repo list 2>/dev/null | grep -i bitnami | awk '{print $3}')
if [ -n "$LAST_UPDATE" ]; then
echo " Last repository update: $LAST_UPDATE"
fi
exit 0

View File

@@ -0,0 +1,87 @@
#!/bin/bash
# Validate that Bitnami Nginx is deployed with 2 replicas and LoadBalancer service type in namespace 'web'
NAMESPACE="web"
RELEASE_NAME="nginx" # Default release name, can also check for any Nginx release
# Check if the namespace exists
kubectl get namespace $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Namespace '$NAMESPACE' not found"
exit 1
fi
# Check if there's any nginx deployment from Helm in the namespace
HELM_RELEASE=$(helm list -n $NAMESPACE 2>/dev/null | grep -i nginx | wc -l)
if [ $HELM_RELEASE -eq 0 ]; then
echo "❌ No Nginx Helm release found in namespace '$NAMESPACE'"
exit 1
fi
echo "✅ Nginx Helm release found in namespace '$NAMESPACE'"
# Find the deployment name (may vary based on release name)
DEPLOYMENT_NAME=$(kubectl get deployments -n $NAMESPACE -l "app.kubernetes.io/name=nginx" -o name 2>/dev/null || kubectl get deployments -n $NAMESPACE -l "app=nginx" -o name 2>/dev/null)
if [ -z "$DEPLOYMENT_NAME" ]; then
echo "❌ Cannot find Nginx deployment in namespace '$NAMESPACE'"
exit 1
fi
DEPLOYMENT_NAME=$(echo $DEPLOYMENT_NAME | sed 's|deployment.apps/||')
echo "✅ Found Nginx deployment: $DEPLOYMENT_NAME"
# Check replicas count
REPLICAS=$(kubectl get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.spec.replicas}' 2>/dev/null)
if [ -z "$REPLICAS" ]; then
echo "❌ Cannot get replica count for deployment '$DEPLOYMENT_NAME'"
exit 1
fi
if [ "$REPLICAS" -ne 2 ]; then
echo "❌ Deployment '$DEPLOYMENT_NAME' has $REPLICAS replicas, but 2 were required"
exit 1
fi
echo "✅ Deployment has correct number of replicas: $REPLICAS"
# Check service type
SERVICE_NAME=$(kubectl get services -n $NAMESPACE -l "app.kubernetes.io/name=nginx" -o name 2>/dev/null || kubectl get services -n $NAMESPACE -l "app=nginx" -o name 2>/dev/null)
if [ -z "$SERVICE_NAME" ]; then
echo "❌ Cannot find Nginx service in namespace '$NAMESPACE'"
exit 1
fi
SERVICE_NAME=$(echo $SERVICE_NAME | sed 's|service/||')
SERVICE_TYPE=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.spec.type}' 2>/dev/null)
if [ -z "$SERVICE_TYPE" ]; then
echo "❌ Cannot get service type for service '$SERVICE_NAME'"
exit 1
fi
if [ "$SERVICE_TYPE" != "LoadBalancer" ]; then
echo "❌ Service '$SERVICE_NAME' is of type '$SERVICE_TYPE', but 'LoadBalancer' was required"
exit 1
fi
echo "✅ Service has correct type: $SERVICE_TYPE"
# Check if pods are running
RUNNING_PODS=$(kubectl get pods -n $NAMESPACE -l "app.kubernetes.io/name=nginx" -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}' 2>/dev/null | wc -w)
if [ -z "$RUNNING_PODS" ] || [ "$RUNNING_PODS" -eq 0 ]; then
# Try another common label
RUNNING_PODS=$(kubectl get pods -n $NAMESPACE -l "app=nginx" -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}' 2>/dev/null | wc -w)
fi
if [ -z "$RUNNING_PODS" ] || [ "$RUNNING_PODS" -lt "$REPLICAS" ]; then
echo "❌ Not all Nginx pods are running (running: $RUNNING_PODS, expected: $REPLICAS)"
exit 1
fi
echo "✅ All $RUNNING_PODS Nginx pods are running"
echo "✅ Bitnami Nginx has been successfully deployed with 2 replicas and LoadBalancer service"
exit 0

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Validate that the CustomResourceDefinition 'backups.data.example.com' has the correct API group and version
CRD_NAME="backups.data.example.com"
EXPECTED_GROUP="data.example.com"
EXPECTED_VERSION="v1alpha1"
# Check if the CRD exists
kubectl get crd $CRD_NAME > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ CustomResourceDefinition '$CRD_NAME' not found"
exit 1
fi
# Check API group
GROUP=$(kubectl get crd $CRD_NAME -o jsonpath='{.spec.group}' 2>/dev/null)
if [ "$GROUP" != "$EXPECTED_GROUP" ]; then
echo "❌ CRD has incorrect API group: '$GROUP', expected: '$EXPECTED_GROUP'"
exit 1
fi
echo "✅ CRD has correct API group: '$GROUP'"
# Check API version
# The structure of versions can be different based on the Kubernetes version
# First try the newer structure
VERSIONS=$(kubectl get crd $CRD_NAME -o jsonpath='{.spec.versions[*].name}' 2>/dev/null)
# If that doesn't work, try the older structure
if [ -z "$VERSIONS" ]; then
VERSIONS=$(kubectl get crd $CRD_NAME -o jsonpath='{.spec.version}' 2>/dev/null)
fi
if [[ ! $VERSIONS =~ $EXPECTED_VERSION ]]; then
echo "❌ CRD versions ($VERSIONS) do not include expected version: '$EXPECTED_VERSION'"
exit 1
fi
echo "✅ CRD includes correct API version: '$EXPECTED_VERSION'"
# Check the resource type name (singular/plural)
PLURAL=$(kubectl get crd $CRD_NAME -o jsonpath='{.spec.names.plural}' 2>/dev/null)
SINGULAR=$(kubectl get crd $CRD_NAME -o jsonpath='{.spec.names.singular}' 2>/dev/null)
KIND=$(kubectl get crd $CRD_NAME -o jsonpath='{.spec.names.kind}' 2>/dev/null)
if [ "$PLURAL" != "backups" ]; then
echo "❌ CRD has incorrect plural name: '$PLURAL', expected: 'backups'"
exit 1
fi
if [ "$KIND" != "Backup" ]; then
echo "❌ CRD has incorrect kind: '$KIND', expected: 'Backup'"
exit 1
fi
echo "✅ CRD has correct names - plural: '$PLURAL', kind: '$KIND'"
# Check that the v1alpha1 version is served and stored
SERVED=$(kubectl get crd $CRD_NAME -o jsonpath='{.spec.versions[?(@.name=="v1alpha1")].served}' 2>/dev/null)
STORAGE=$(kubectl get crd $CRD_NAME -o jsonpath='{.spec.versions[?(@.name=="v1alpha1")].storage}' 2>/dev/null)
if [ "$SERVED" != "true" ] && [ -n "$SERVED" ]; then
echo "⚠️ The version 'v1alpha1' might not be served (served: $SERVED)"
fi
echo "✅ CustomResourceDefinition '$CRD_NAME' has correct API group '$EXPECTED_GROUP' and version '$EXPECTED_VERSION'"
exit 0

View File

@@ -0,0 +1,88 @@
#!/bin/bash
# Validate that the CustomResourceDefinition 'backups.data.example.com' has required schema fields
CRD_NAME="backups.data.example.com"
REQUIRED_FIELDS=("spec.source" "spec.destination")
# Check if the CRD exists
kubectl get crd $CRD_NAME > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ CustomResourceDefinition '$CRD_NAME' not found"
exit 1
fi
# Get the full CRD definition for inspection
CRD_JSON=$(kubectl get crd $CRD_NAME -o json)
# Check for schema validation in different Kubernetes API versions
# First, try to get the schema for v1 API
SCHEMA=$(echo "$CRD_JSON" | jq -r '.spec.versions[] | select(.name=="v1alpha1") | .schema.openAPIV3Schema // empty' 2>/dev/null)
# If that's empty, try the beta API versions
if [ -z "$SCHEMA" ]; then
SCHEMA=$(echo "$CRD_JSON" | jq -r '.spec.validation.openAPIV3Schema // empty' 2>/dev/null)
fi
if [ -z "$SCHEMA" ]; then
echo "❌ No schema validation found in the CRD"
exit 1
fi
echo "✅ Schema validation is defined for the CRD"
# Check for the required properties in the schema
SCHEMA_PROPERTIES=$(echo "$CRD_JSON" | jq -r '.spec.versions[] | select(.name=="v1alpha1") | .schema.openAPIV3Schema.properties.spec.properties // empty' 2>/dev/null)
# If that's empty, try the beta API versions
if [ -z "$SCHEMA_PROPERTIES" ]; then
SCHEMA_PROPERTIES=$(echo "$CRD_JSON" | jq -r '.spec.validation.openAPIV3Schema.properties.spec.properties // empty' 2>/dev/null)
fi
if [ -z "$SCHEMA_PROPERTIES" ]; then
echo "❌ Could not find 'spec' properties in the schema"
exit 1
fi
# Check for each required field
for field in "${REQUIRED_FIELDS[@]}"; do
field_name=$(echo $field | cut -d'.' -f2)
# Check if the field is defined in properties
FIELD_DEF=$(echo "$SCHEMA_PROPERTIES" | jq -r ".$field_name // empty" 2>/dev/null)
if [ -z "$FIELD_DEF" ]; then
echo "❌ Required field '$field' is not defined in the schema"
exit 1
fi
echo "✅ Field '$field' is defined in the schema"
# Check if the field is a string type
FIELD_TYPE=$(echo "$SCHEMA_PROPERTIES" | jq -r ".$field_name.type" 2>/dev/null)
if [ "$FIELD_TYPE" != "string" ]; then
echo "⚠️ Field '$field' is of type '$FIELD_TYPE', not 'string' as expected"
fi
done
# Check if both fields are required
REQUIRED_LIST=$(echo "$CRD_JSON" | jq -r '.spec.versions[] | select(.name=="v1alpha1") | .schema.openAPIV3Schema.properties.spec.required // empty' 2>/dev/null)
# If that's empty, try the beta API versions
if [ -z "$REQUIRED_LIST" ]; then
REQUIRED_LIST=$(echo "$CRD_JSON" | jq -r '.spec.validation.openAPIV3Schema.properties.spec.required // empty' 2>/dev/null)
fi
if [ -z "$REQUIRED_LIST" ]; then
echo "⚠️ No required fields specified in the schema"
else
# Check if both source and destination are in the required list
if echo "$REQUIRED_LIST" | jq -e 'index("source")' > /dev/null && echo "$REQUIRED_LIST" | jq -e 'index("destination")' > /dev/null; then
echo "✅ Both 'source' and 'destination' are marked as required fields"
else
echo "⚠️ Not all required fields ('source', 'destination') are marked as required in the schema"
fi
fi
echo "✅ CRD schema includes the required fields 'spec.source' and 'spec.destination'"
exit 0

View File

@@ -0,0 +1,10 @@
#!/bin/bash
# Validate if the NetworkPolicy 'allow-traffic' exists in the 'networking' namespace
if kubectl get networkpolicy allow-traffic -n networking &> /dev/null; then
echo "Success: NetworkPolicy 'allow-traffic' exists in namespace 'networking'"
exit 0
else
echo "Error: NetworkPolicy 'allow-traffic' does not exist in namespace 'networking'"
exit 1
fi

View File

@@ -0,0 +1,12 @@
#!/bin/bash
# Validate if the Service 'internal-app' is of type ClusterIP in the 'networking' namespace
SERVICE_TYPE=$(kubectl get service internal-app -n networking -o jsonpath='{.spec.type}' 2>/dev/null)
if [ "$SERVICE_TYPE" = "ClusterIP" ]; then
echo "Success: Service 'internal-app' is of correct type (ClusterIP)"
exit 0
else
echo "Error: Service 'internal-app' is not of the correct type. Found: '$SERVICE_TYPE', Expected: 'ClusterIP'"
exit 1
fi

View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Validate that the service 'public-web' in namespace 'networking' is of type NodePort with correct nodePort
NAMESPACE="networking"
SERVICE_NAME="public-web"
EXPECTED_TYPE="NodePort"
EXPECTED_NODE_PORT=30080
# Check if the service exists
kubectl get service $SERVICE_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Service '$SERVICE_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Check service type
SERVICE_TYPE=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.spec.type}' 2>/dev/null)
if [ -z "$SERVICE_TYPE" ]; then
echo "❌ Cannot determine service type for '$SERVICE_NAME'"
exit 1
fi
if [ "$SERVICE_TYPE" != "$EXPECTED_TYPE" ]; then
echo "❌ Service '$SERVICE_NAME' is of type '$SERVICE_TYPE', not '$EXPECTED_TYPE' as required"
exit 1
fi
echo "✅ Service '$SERVICE_NAME' is correctly configured as type '$EXPECTED_TYPE'"
# Check nodePort value
NODE_PORT=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null)
if [ -z "$NODE_PORT" ]; then
echo "❌ NodePort is not configured for service '$SERVICE_NAME'"
exit 1
fi
if [ "$NODE_PORT" -ne "$EXPECTED_NODE_PORT" ]; then
echo "❌ Service '$SERVICE_NAME' uses nodePort $NODE_PORT, expected $EXPECTED_NODE_PORT"
exit 1
fi
echo "✅ Service '$SERVICE_NAME' is correctly configured with nodePort: $NODE_PORT"
exit 0

View File

@@ -0,0 +1,92 @@
#!/bin/bash
# Validate that the service 'public-web' in namespace 'networking' has the correct selector to target 'web-frontend' deployment pods
NAMESPACE="networking"
SERVICE_NAME="public-web"
DEPLOYMENT_NAME="web-frontend"
# Check if the service exists
kubectl get service $SERVICE_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Service '$SERVICE_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Check if the deployment exists
kubectl get deployment $DEPLOYMENT_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "⚠️ Deployment '$DEPLOYMENT_NAME' not found in namespace '$NAMESPACE', but will continue checking service selector"
fi
# Get service selector
SERVICE_SELECTOR=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o json | jq -r '.spec.selector' 2>/dev/null)
if [ -z "$SERVICE_SELECTOR" ] || [ "$SERVICE_SELECTOR" == "null" ]; then
echo "❌ Service '$SERVICE_NAME' does not have a selector"
exit 1
fi
echo "🔍 Service '$SERVICE_NAME' has selector: $SERVICE_SELECTOR"
# Check if deployment exists to compare selectors
if kubectl get deployment $DEPLOYMENT_NAME -n $NAMESPACE > /dev/null 2>&1; then
# Get deployment selector
DEPLOYMENT_SELECTOR=$(kubectl get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o json | jq -r '.spec.selector.matchLabels' 2>/dev/null)
if [ -z "$DEPLOYMENT_SELECTOR" ] || [ "$DEPLOYMENT_SELECTOR" == "null" ]; then
echo "⚠️ Deployment '$DEPLOYMENT_NAME' does not have matchLabels in its selector"
else
echo "🔍 Deployment '$DEPLOYMENT_NAME' has selector: $DEPLOYMENT_SELECTOR"
# Check if service selector is a subset of deployment selector
# This is a simplistic check and might not work for complex selectors
MATCHES=true
for key in $(echo "$SERVICE_SELECTOR" | jq -r 'keys[]'); do
service_value=$(echo "$SERVICE_SELECTOR" | jq -r --arg k "$key" '.[$k]')
deployment_value=$(echo "$DEPLOYMENT_SELECTOR" | jq -r --arg k "$key" '.[$k] // empty')
if [ "$service_value" != "$deployment_value" ]; then
MATCHES=false
echo "❌ Service selector key '$key' value '$service_value' does not match deployment value '$deployment_value'"
fi
done
if [ "$MATCHES" = true ]; then
echo "✅ Service selector matches deployment selector"
else
echo "❌ Service selector does not match deployment selector"
exit 1
fi
fi
fi
# Check if the service selects any pods
SELECTOR_STRING=""
for key in $(echo $SERVICE_SELECTOR | jq -r 'keys[]'); do
value=$(echo $SERVICE_SELECTOR | jq -r --arg key "$key" '.[$key]')
if [ -n "$SELECTOR_STRING" ]; then
SELECTOR_STRING="$SELECTOR_STRING,"
fi
SELECTOR_STRING="${SELECTOR_STRING}${key}=${value}"
done
SELECTED_PODS=$(kubectl get pods -n $NAMESPACE -l "$SELECTOR_STRING" -o name 2>/dev/null)
if [ -z "$SELECTED_PODS" ]; then
echo "⚠️ No pods are currently selected by the service selector"
else
POD_COUNT=$(echo "$SELECTED_PODS" | wc -l)
echo "✅ Service selector targets $POD_COUNT pods"
fi
# Check if the service has endpoints
ENDPOINTS=$(kubectl get endpoints $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.subsets[*].addresses}' 2>/dev/null)
if [ -z "$ENDPOINTS" ] || [ "$ENDPOINTS" == "[]" ]; then
echo "⚠️ Service '$SERVICE_NAME' has no endpoints, which may indicate a selector problem"
else
echo "✅ Service '$SERVICE_NAME' has endpoints, indicating its selector is working"
fi
echo "✅ Service '$SERVICE_NAME' has a selector that targets pods from the '$DEPLOYMENT_NAME' deployment"
exit 0

View File

@@ -0,0 +1,94 @@
#!/bin/bash
# Validate that the service 'public-web' in namespace 'networking' has correct port configurations
NAMESPACE="networking"
SERVICE_NAME="public-web"
EXPECTED_PORT=80
EXPECTED_TARGET_PORT=8080
EXPECTED_NODE_PORT=30080
# Check if the service exists
kubectl get service $SERVICE_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Service '$SERVICE_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Check service port configuration
SERVICE_PORT=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.spec.ports[0].port}' 2>/dev/null)
if [ -z "$SERVICE_PORT" ]; then
echo "❌ Cannot determine service port for '$SERVICE_NAME'"
exit 1
fi
if [ "$SERVICE_PORT" != "$EXPECTED_PORT" ]; then
echo "❌ Service '$SERVICE_NAME' exposes port $SERVICE_PORT, not port $EXPECTED_PORT as required"
exit 1
fi
echo "✅ Service '$SERVICE_NAME' correctly exposes port $SERVICE_PORT"
# Check target port configuration
TARGET_PORT=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.spec.ports[0].targetPort}' 2>/dev/null)
if [ -z "$TARGET_PORT" ]; then
echo "❌ Target port is not specified"
exit 1
fi
if [ "$TARGET_PORT" != "$EXPECTED_TARGET_PORT" ]; then
echo "❌ Service has incorrect target port: $TARGET_PORT, expected: $EXPECTED_TARGET_PORT"
exit 1
fi
echo "✅ Service has correct target port: $TARGET_PORT"
# Check node port configuration
NODE_PORT=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null)
if [ -z "$NODE_PORT" ]; then
echo "❌ NodePort is not specified"
exit 1
fi
if [ "$NODE_PORT" != "$EXPECTED_NODE_PORT" ]; then
echo "❌ Service has incorrect node port: $NODE_PORT, expected: $EXPECTED_NODE_PORT"
exit 1
fi
echo "✅ Service has correct node port: $NODE_PORT"
# Check if the target port exists in the pods
SERVICE_SELECTOR=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o json | jq -r '.spec.selector' 2>/dev/null)
if [ -n "$SERVICE_SELECTOR" ] && [ "$SERVICE_SELECTOR" != "null" ]; then
# Format selector for kubectl label selector
SELECTOR_STRING=""
for key in $(echo $SERVICE_SELECTOR | jq -r 'keys[]'); do
value=$(echo $SERVICE_SELECTOR | jq -r --arg key "$key" '.[$key]')
if [ -n "$SELECTOR_STRING" ]; then
SELECTOR_STRING="$SELECTOR_STRING,"
fi
SELECTOR_STRING="${SELECTOR_STRING}${key}=${value}"
done
# Get a pod that matches the selector
POD_NAME=$(kubectl get pods -n $NAMESPACE -l "$SELECTOR_STRING" -o name 2>/dev/null | head -n 1 | cut -d '/' -f 2)
if [ -n "$POD_NAME" ]; then
# Check if the target port matches any container port in the pod
CONTAINER_PORTS=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.spec.containers[*].ports[*].containerPort}' 2>/dev/null)
if [[ $CONTAINER_PORTS == *"$TARGET_PORT"* ]]; then
echo "✅ Target port $TARGET_PORT matches container port in selected pods"
else
echo "⚠️ Target port $TARGET_PORT does not match any container port in pod $POD_NAME (ports: $CONTAINER_PORTS)"
fi
else
echo "⚠️ No pods found matching service selector, cannot verify container ports"
fi
fi
echo "✅ Service '$SERVICE_NAME' has correct port configuration (port: $SERVICE_PORT, targetPort: $TARGET_PORT, nodePort: $NODE_PORT)"
exit 0

View File

@@ -0,0 +1,30 @@
#!/bin/bash
# Validate that the Ingress 'api-ingress' is created in namespace 'networking'
NAMESPACE="networking"
INGRESS_NAME="api-ingress"
# Check if the ingress exists
kubectl get ingress $INGRESS_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Ingress '$INGRESS_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Get the API version of the Ingress
API_VERSION=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o jsonpath='{.apiVersion}' 2>/dev/null)
echo " Ingress '$INGRESS_NAME' exists in namespace '$NAMESPACE' (API version: $API_VERSION)"
# Check for any ingressClassName or annotations for ingress controller class
INGRESS_CLASS=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o jsonpath='{.spec.ingressClassName}' 2>/dev/null)
INGRESS_CLASS_ANNOTATION=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o jsonpath='{.metadata.annotations.kubernetes\.io/ingress\.class}' 2>/dev/null)
if [ -n "$INGRESS_CLASS" ]; then
echo " Ingress is using ingressClassName: $INGRESS_CLASS"
elif [ -n "$INGRESS_CLASS_ANNOTATION" ]; then
echo " Ingress is using annotation for ingress class: $INGRESS_CLASS_ANNOTATION"
fi
echo "✅ Ingress '$INGRESS_NAME' is successfully created in namespace '$NAMESPACE'"
exit 0

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# Validate that the Ingress 'api-ingress' in namespace 'networking' has the correct host 'api.example.com'
NAMESPACE="networking"
INGRESS_NAME="api-ingress"
EXPECTED_HOST="api.example.com"
# Check if the ingress exists
kubectl get ingress $INGRESS_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Ingress '$INGRESS_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Try different structures for different Kubernetes API versions
# For networking.k8s.io/v1 API
HOSTS_V1=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o jsonpath='{.spec.rules[*].host}' 2>/dev/null)
if [ -z "$HOSTS_V1" ]; then
# For extensions/v1beta1 or networking.k8s.io/v1beta1 API
HOSTS_BETA=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o jsonpath='{.spec.rules[*].host}' 2>/dev/null)
if [ -n "$HOSTS_BETA" ]; then
HOSTS=$HOSTS_BETA
else
echo "❌ No hosts found in Ingress rules"
exit 1
fi
else
HOSTS=$HOSTS_V1
fi
# Check if expected host is in the list of hosts
FOUND=false
for HOST in $HOSTS; do
if [ "$HOST" == "$EXPECTED_HOST" ]; then
FOUND=true
break
fi
done
if [ "$FOUND" = true ]; then
echo "✅ Ingress '$INGRESS_NAME' has the correct host: '$EXPECTED_HOST'"
else
echo "❌ Ingress '$INGRESS_NAME' does not have the expected host '$EXPECTED_HOST'. Found hosts: $HOSTS"
exit 1
fi
# Check if there are TLS settings for the host
TLS_HOSTS=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o jsonpath='{.spec.tls[*].hosts[*]}' 2>/dev/null)
if [ -n "$TLS_HOSTS" ]; then
# Check if expected host is in TLS hosts
TLS_FOUND=false
for TLS_HOST in $TLS_HOSTS; do
if [ "$TLS_HOST" == "$EXPECTED_HOST" ]; then
TLS_FOUND=true
break
fi
done
if [ "$TLS_FOUND" = true ]; then
echo " TLS is configured for host '$EXPECTED_HOST'"
else
echo " TLS is configured but not for host '$EXPECTED_HOST'"
fi
fi
echo "✅ Ingress '$INGRESS_NAME' is correctly configured with host '$EXPECTED_HOST'"
exit 0

View File

@@ -0,0 +1,96 @@
#!/bin/bash
# Validate that the Ingress 'api-ingress' in namespace 'networking' routes traffic to service 'api-service' on port 80
NAMESPACE="networking"
INGRESS_NAME="api-ingress"
EXPECTED_HOST="api.example.com"
EXPECTED_SERVICE="api-service"
EXPECTED_PORT=80
# Check if the ingress exists
kubectl get ingress $INGRESS_NAME -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Ingress '$INGRESS_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Check if the service exists
kubectl get service $EXPECTED_SERVICE -n $NAMESPACE > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "⚠️ Service '$EXPECTED_SERVICE' not found in namespace '$NAMESPACE'"
# Continue with validation as the service might be created later
fi
# Get API version to handle different Ingress structures
API_VERSION=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o jsonpath='{.apiVersion}' 2>/dev/null)
echo " Ingress API version: $API_VERSION"
# Handle different API versions (v1 vs v1beta1)
if [[ "$API_VERSION" == "networking.k8s.io/v1" ]]; then
# For v1 API
RULE_INDEX=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --arg host "$EXPECTED_HOST" '.spec.rules | map(.host == $host) | index(true) // empty')
if [ -z "$RULE_INDEX" ]; then
echo "❌ No rule found for host '$EXPECTED_HOST'"
exit 1
fi
# Check path-based rules
PATHS=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --argjson idx "$RULE_INDEX" '.spec.rules[$idx].http.paths[].path // "/"')
for PATH in $PATHS; do
# For each path, check the backend service
BACKEND_SERVICE_NAME=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --argjson idx "$RULE_INDEX" --arg path "$PATH" '.spec.rules[$idx].http.paths[] | select(.path == $path or (.path == null and $path == "/")).backend.service.name // empty')
BACKEND_SERVICE_PORT=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --argjson idx "$RULE_INDEX" --arg path "$PATH" '.spec.rules[$idx].http.paths[] | select(.path == $path or (.path == null and $path == "/")).backend.service.port.number // empty')
# If port is not a number, try getting it as a name
if [ -z "$BACKEND_SERVICE_PORT" ]; then
BACKEND_SERVICE_PORT=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --argjson idx "$RULE_INDEX" --arg path "$PATH" '.spec.rules[$idx].http.paths[] | select(.path == $path or (.path == null and $path == "/")).backend.service.port.name // empty')
fi
echo "🔍 Path '$PATH' routes to service: $BACKEND_SERVICE_NAME, port: $BACKEND_SERVICE_PORT"
if [ "$BACKEND_SERVICE_NAME" = "$EXPECTED_SERVICE" ]; then
# Check port
if [ "$BACKEND_SERVICE_PORT" = "$EXPECTED_PORT" ] || [ "$BACKEND_SERVICE_PORT" = "http" ]; then
echo "✅ Found correct backend service and port for path '$PATH'"
exit 0
else
echo "⚠️ Service name matches but port is different: expected $EXPECTED_PORT, got $BACKEND_SERVICE_PORT"
fi
fi
done
else
# For v1beta1 or extensions/v1beta1 API
RULE_INDEX=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --arg host "$EXPECTED_HOST" '.spec.rules | map(.host == $host) | index(true) // empty')
if [ -z "$RULE_INDEX" ]; then
echo "❌ No rule found for host '$EXPECTED_HOST'"
exit 1
fi
# Check path-based rules
PATHS=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --argjson idx "$RULE_INDEX" '.spec.rules[$idx].http.paths[].path // "/"')
for PATH in $PATHS; do
# For each path, check the backend service
BACKEND_SERVICE_NAME=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --argjson idx "$RULE_INDEX" --arg path "$PATH" '.spec.rules[$idx].http.paths[] | select(.path == $path or (.path == null and $path == "/")).backend.serviceName // empty')
BACKEND_SERVICE_PORT=$(kubectl get ingress $INGRESS_NAME -n $NAMESPACE -o json | jq -r --argjson idx "$RULE_INDEX" --arg path "$PATH" '.spec.rules[$idx].http.paths[] | select(.path == $path or (.path == null and $path == "/")).backend.servicePort // empty')
echo "🔍 Path '$PATH' routes to service: $BACKEND_SERVICE_NAME, port: $BACKEND_SERVICE_PORT"
if [ "$BACKEND_SERVICE_NAME" = "$EXPECTED_SERVICE" ]; then
# Check port - can be number or name
if [ "$BACKEND_SERVICE_PORT" = "$EXPECTED_PORT" ] || [ "$BACKEND_SERVICE_PORT" = "http" ]; then
echo "✅ Found correct backend service and port for path '$PATH'"
exit 0
else
echo "⚠️ Service name matches but port is different: expected $EXPECTED_PORT, got $BACKEND_SERVICE_PORT"
fi
fi
done
fi
# If we get here, we didn't find the expected backend
echo "❌ Ingress does not route traffic to '$EXPECTED_SERVICE' on port $EXPECTED_PORT for host '$EXPECTED_HOST'"
exit 1

View File

@@ -0,0 +1,11 @@
#bin/bash
# Validate namespace if present then return 0 else return 1
kubectl get namespace dev
if [ $? -eq 0 ]; then
echo "Namespace dev is present"
exit 0
else
echo "Namespace dev is not present"
exit 1
fi

View File

@@ -0,0 +1,10 @@
#!/bin/bash
# Validate if the deployment 'nginx-deployment' exists in the 'dev' namespace
if kubectl get deployment nginx-deployment -n dev &> /dev/null; then
echo "Success: Deployment 'nginx-deployment' exists in namespace 'dev'"
exit 0
else
echo "Error: Deployment 'nginx-deployment' does not exist in namespace 'dev'"
exit 1
fi

View File

@@ -0,0 +1,12 @@
#!/bin/bash
# Validate if the deployment 'nginx-deployment' in namespace 'dev' is using the correct image (nginx:latest)
IMAGE=$(kubectl get deployment nginx-deployment -n dev -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null)
if [ "$IMAGE" = "nginx:latest" ]; then
echo "Success: Deployment 'nginx-deployment' is using the correct image 'nginx:latest'"
exit 0
else
echo "Error: Deployment 'nginx-deployment' is not using the correct image. Found: '$IMAGE', Expected: 'nginx:latest'"
exit 1
fi

Some files were not shown because too many files have changed in this diff Show More