Project Scaffolding Instructions for Firebase Studio
Objective: Scaffold a complete, self-contained project for the "Vault Tactical Edge Demo." The project will use Docker Compose to run a local environment consisting of a HashiCorp Vault server, several Vault Agent containers (simulating Stryker vehicles), and a Node.js Express server that provides a control API and serves a static web GUI.
Order of Operations:
Create the complete directory and file structure as specified below.
Populate each file with the provided source code.
The final project should be a fully self-contained Git repository, ready to be initialized and run.
Step 1: Create the Project Directory Structure
Please generate the following directory and file structure:
vault-tactical-edge-demo/ ├── .gitignore ├── ddil-vault-compose.yaml ├── run-demo.sh ├── demo-controller/ │ ├── Dockerfile │ ├── package.json │ ├── server.js │ └── public/ │ └── index.html └── policies/ └── mission-app-policy.hcl
Step 2: Populate Each File with Source Code
File 1: /run-demo.sh
Purpose: This is the main user-facing script. It checks for dependencies, cleans up old environments, and uses Docker Compose to build and launch the entire demo.
Source Code: (Note: This is a modified version of your gist script, updated to use the local compose file instead of downloading it, making the project fully self-contained.)
#!/bin/bash
set -e
set -u
set -o pipefail
GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color COMPOSE_FILE="ddil-vault-compose.yaml"
print_message() {
echo -e "${GREEN}▶
check_dependency() { if ! command -v "$1" &> /dev/null; then echo -e "${YELLOW}Error: '$1' command not found. Please install it and try again.${NC}" exit 1 fi }
print_message "Phase 1: Running Pre-flight Checks..." check_dependency "docker" check_dependency "docker-compose"
print_message "Phase 2: Cleaning up any previous demo environments..." if [ -f "$COMPOSE_FILE" ]; then docker-compose -f "$COMPOSE_FILE" down --remove-orphans fi
print_message "Phase 3: Building and launching the demo environment..." docker-compose -f "$COMPOSE_FILE" up --build -d
VAULT_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' vault-tactical-edge-demo_vault-server_1)
print_message "Phase 4: Demo Environment is Ready!" echo "------------------------------------------------------------------" echo -e "${YELLOW}The Tactical Demo GUI is now running at:${NC} http://localhost:8080" echo -e "${YELLOW}The Vault Server UI is accessible at:${NC} http://${VAULT_IP}:8200" echo "" echo "Use the web GUI to run interactive scenarios against the live Vault instance." echo "The initial Unseal Key and Root Token will be printed in the Docker Compose logs." echo "You can view logs with: 'docker-compose -f ${COMPOSE_FILE} logs -f'" echo "------------------------------------------------------------------"
File 2: /ddil-vault-compose.yaml
Purpose: The Docker Compose file that defines all the services (Vault server, API controller, Stryker agents) and their configurations.
Source Code: (Content fetched from your gist URL)
version: '3.8'
services: vault-server: image: vault:latest ports: - "8200:8200" environment: - VAULT_ADDR=http://0.0.0.0:8200 - VAULT_API_ADDR=http://0.0.0.0:8200 - VAULT_DEV_ROOT_TOKEN_ID=root - VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 cap_add: - IPC_LOCK command: server
demo-controller: build: context: ./demo-controller ports: - "8080:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock
stryker-1: image: vault:latest depends_on: - vault-server command: agent -config=/vault/config/agent-config.hcl volumes: - ./agent-config:/vault/config
stryker-2: image: vault:latest depends_on: - vault-server command: agent -config=/vault/config/agent-config.hcl volumes: - ./agent-config:/vault/config
stryker-3: image: vault:latest depends_on: - vault-server command: agent -config=/vault/config/agent-config.hcl volumes: - ./agent-config:/vault/config
File 3: /demo-controller/Dockerfile
Purpose: Instructs Docker how to build the container image for the demo-controller service.
Source Code:
FROM node:18-alpine WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 8080 CMD [ "node", "server.js" ]
File 4: /demo-controller/package.json
Purpose: Defines the Node.js project and its dependencies (Express, CORS).
Source Code:
{ "name": "demo-controller", "version": "1.0.0", "description": "API and GUI for the Vault Tactical Demo", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "cors": "^2.8.5", "express": "^4.18.2", "child_process": "^1.0.2" } }
File 5: /demo-controller/server.js
Purpose: The Node.js Express server that provides the control API and serves the static GUI.
Source Code:
const express = require('express'); const cors = require('cors'); const path = require('path'); const { exec } = require('child_process');
const app = express(); const port = 8080;
app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public')));
const runCommand = (command) => {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(Exec error for command "${command}": ${stderr});
return reject({ message: stderr || error.message });
}
resolve({ message: stdout.trim() });
});
});
};
app.get('/api/state', async (req, res) => { try { const vaultStatusCmd = "docker exec vault-tactical-edge-demo_vault-server_1 vault status -format=json"; const result = await runCommand(vaultStatusCmd); const vaultState = JSON.parse(result.message); res.json({ vault_server: { status: vaultState.sealed ? 'sealed' : 'unsealed' } }); } catch (error) { if (error.message && error.message.includes("sealed")) { res.json({ vault_server: { status: 'sealed' } }); } else { res.status(500).json({ message: "Failed to get Vault status.", details: error.message || 'Unknown error' }); } } });
app.post('/api/actions/:action', async (req, res) => { const action = req.params.action; try { let command, successMessage; switch (action) { case 'seal-vault': command = "docker exec vault-tactical-edge-demo_vault-server_1 vault operator seal"; successMessage = "Vault server is now SEALED."; break; // Add more scenarios here in the future default: return res.status(400).json({ message: "Invalid action specified." }); } await runCommand(command); res.json({ message: successMessage }); } catch (error) { res.status(500).json(error); } });
app.listen(port, () => {
console.log(Demo controller listening at http://localhost:${port});
});
File 6: /demo-controller/public/index.html
Purpose: The single-page static web GUI for the demo.
Source Code: (This is the live GUI version that interacts with the API)
<title>Vault Tactical Edge Demo (Live)</title> <script src="https://cdn.tailwindcss.com"></script> <script src="https://unpkg.com/lucide@latest"></script> <style> body { font-family: 'Inter', sans-serif; } .map-container { background-image: url('https://images.unsplash.com/photo-1568623229237-072211de2cb2?q=80&w=2070&auto=format&fit=crop'); background-size: cover; background-position: center; position: relative; } .vehicle-icon { transition: all 0.3s ease-in-out; } @keyframes blink { 50% { opacity: 0.3; } } .status-sealed { animation: blink 1s linear infinite; } @keyframes pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); } } .status-unsealed { animation: pulse 2s infinite; } </style><script>
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
const eventLog = document.getElementById('event-log');
const sealBtn = document.getElementById('seal-btn');
const vaultServerIcon = document.getElementById('vault-server-icon');
const logEvent = (message, type = 'info') => {
const colors = { info: 'text-cyan-400', success: 'text-green-400', warn: 'text-yellow-400', danger: 'text-red-400' };
const icons = { info: 'info', success: 'check-circle', warn: 'alert-triangle', danger: 'shield-alert' };
const logEntry = document.createElement('div');
logEntry.className = `flex items-start space-x-2 ${colors[type]}`;
logEntry.innerHTML = `<i data-lucide="${icons[type]}" class="w-4 h-4 flex-shrink-0 mt-0.5"></i><span><span class="font-bold">${new Date().toLocaleTimeString()}:</span> ${message}</span>`;
eventLog.prepend(logEntry);
lucide.createIcons();
};
const callApi = async (endpoint, method = 'POST') => {
try {
const response = await fetch(`http://localhost:8080/api/actions/${endpoint}`, { method });
const result = await response.json();
if (!response.ok) throw new Error(result.message || 'API call failed');
logEvent(result.message, 'success');
} catch (error) {
logEvent(`Error: ${error.message}`, 'danger');
}
};
const updateUI = (state) => {
const isSealed = state.vault_server.status === 'sealed';
vaultServerIcon.classList.toggle('bg-blue-600', !isSealed);
vaultServerIcon.classList.toggle('border-blue-300', !isSealed);
vaultServerIcon.classList.toggle('status-unsealed', !isSealed);
vaultServerIcon.classList.toggle('bg-red-600', isSealed);
vaultServerIcon.classList.toggle('border-red-400', isSealed);
vaultServerIcon.classList.toggle('status-sealed', isSealed);
};
const checkState = async () => {
try {
const response = await fetch('http://localhost:8080/api/state');
const state = await response.json();
updateUI(state);
} catch (error) {
logEvent('Cannot connect to demo controller. Is the environment running?', 'danger');
}
};
sealBtn.addEventListener('click', () => {
logEvent('Executing Scenario: Seal Vault Server', 'warn');
callApi('seal-vault');
});
logEvent('Live Demo GUI Initialized. Connecting to local backend...', 'info');
setInterval(checkState, 2000); // Poll for state every 2 seconds
});
</script>
File 7: /.gitignore
Purpose: To prevent committing sensitive or unnecessary files to the Git repository.
Source Code:
vault-keys.json *.log
demo-controller/node_modules/
.docker/
File 8: /policies/mission-app-policy.hcl
Purpose: A simple Vault policy file that will be used in a more advanced version of the demo. For now, it's a placeholder.
Source Code:
path "secret/data/mission-app/*" { capabilities = ["read"] }
Once Firebase Studio has generated this structure and populated the files, you will have a complete, ready-to-run local project.