From 1f4a5770772b524f89d17799c8d6aa12f5ccd537 Mon Sep 17 00:00:00 2001 From: Master3395 Date: Fri, 12 Sep 2025 21:10:06 +0200 Subject: [PATCH] Enhance environment variable management with advanced mode and import/export features - Implement advanced environment variable mode for bulk editing and easier management. - Add functionality to import environment variables from existing Docker containers. - Introduce export options for environment variables to .env files. - Update UI to toggle between simple and advanced modes, with corresponding input fields. - Enhance Docker Compose integration with environment variable handling and user guidance. --- dockerManager/container.py | 40 +- .../static/dockerManager/dockerManager.js | 888 +++++++++++++++++- .../templates/dockerManager/runContainer.html | 287 +++++- .../dockerManager/viewContainer.html | 356 ++++++- dockerManager/urls.py | 2 + dockerManager/views.py | 106 +++ 6 files changed, 1626 insertions(+), 53 deletions(-) diff --git a/dockerManager/container.py b/dockerManager/container.py index d08166a4d..331e7fd33 100644 --- a/dockerManager/container.py +++ b/dockerManager/container.py @@ -302,11 +302,23 @@ class ContainerManager(multi.Thread): inspectImage = dockerAPI.inspect_image(image + ":" + tag) portConfig = {} - # Formatting envList for usage + # Formatting envList for usage - handle both simple and advanced modes envDict = {} - for key, value in envList.items(): - if (value['name'] != '') or (value['value'] != ''): - envDict[value['name']] = value['value'] + + # Check if advanced mode is being used + advanced_mode = data.get('advancedEnvMode', False) + + if advanced_mode: + # Advanced mode: envList is already a dictionary of key-value pairs + envDict = envList + else: + # Simple mode: envList is an array of objects with name/value properties + for key, value in envList.items(): + if isinstance(value, dict) and (value.get('name', '') != '' or value.get('value', '') != ''): + envDict[value['name']] = value['value'] + elif isinstance(value, str) and value != '': + # Handle case where value might be a string (fallback) + envDict[key] = value if 'ExposedPorts' in inspectImage['Config']: for item in inspectImage['Config']['ExposedPorts']: @@ -975,11 +987,23 @@ class ContainerManager(multi.Thread): con.startOnReboot = startOnReboot if 'envConfirmation' in data and data['envConfirmation']: - # Formatting envList for usage + # Formatting envList for usage - handle both simple and advanced modes envDict = {} - for key, value in envList.items(): - if (value['name'] != '') or (value['value'] != ''): - envDict[value['name']] = value['value'] + + # Check if advanced mode is being used + advanced_mode = data.get('advancedEnvMode', False) + + if advanced_mode: + # Advanced mode: envList is already a dictionary of key-value pairs + envDict = envList + else: + # Simple mode: envList is an array of objects with name/value properties + for key, value in envList.items(): + if isinstance(value, dict) and (value.get('name', '') != '' or value.get('value', '') != ''): + envDict[value['name']] = value['value'] + elif isinstance(value, str) and value != '': + # Handle case where value might be a string (fallback) + envDict[key] = value volumes = {} for index, volume in volList.items(): diff --git a/dockerManager/static/dockerManager/dockerManager.js b/dockerManager/static/dockerManager/dockerManager.js index 3f7131450..5a89b644d 100644 --- a/dockerManager/static/dockerManager/dockerManager.js +++ b/dockerManager/static/dockerManager/dockerManager.js @@ -124,6 +124,12 @@ app.controller('runContainer', function ($scope, $http) { $scope.iport = {}; $scope.portType = {}; $scope.envList = {}; + + // Advanced Environment Variable Mode + $scope.advancedEnvMode = false; + $scope.advancedEnvText = ''; + $scope.advancedEnvCount = 0; + $scope.parsedEnvVars = {}; $scope.addVolField = function () { $scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''}; $scope.volListNumber = $scope.volListNumber + 1; @@ -139,6 +145,358 @@ app.controller('runContainer', function ($scope, $http) { $scope.envList[countEnv + 1] = {'name': '', 'value': ''}; }; + // Advanced Environment Variable Functions + $scope.toggleEnvMode = function() { + if ($scope.advancedEnvMode) { + // Switching to advanced mode - convert existing envList to text format + $scope.convertToAdvancedFormat(); + } else { + // Switching to simple mode - convert advanced text to envList + $scope.convertToSimpleFormat(); + } + }; + + $scope.convertToAdvancedFormat = function() { + var envLines = []; + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envLines.push($scope.envList[key].name + '=' + $scope.envList[key].value); + } + } + $scope.advancedEnvText = envLines.join('\n'); + $scope.parseAdvancedEnv(); + }; + + $scope.convertToSimpleFormat = function() { + $scope.parseAdvancedEnv(); + var newEnvList = {}; + var index = 0; + for (var key in $scope.parsedEnvVars) { + newEnvList[index] = {'name': key, 'value': $scope.parsedEnvVars[key]}; + index++; + } + $scope.envList = newEnvList; + }; + + $scope.parseAdvancedEnv = function() { + $scope.parsedEnvVars = {}; + $scope.advancedEnvCount = 0; + + if (!$scope.advancedEnvText) { + return; + } + + var lines = $scope.advancedEnvText.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + + // Skip empty lines and comments + if (!line || line.startsWith('#')) { + continue; + } + + // Parse KEY=VALUE format + var equalIndex = line.indexOf('='); + if (equalIndex > 0) { + var key = line.substring(0, equalIndex).trim(); + var value = line.substring(equalIndex + 1).trim(); + + // Remove quotes if present + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + + if (key && key.match(/^[A-Za-z_][A-Za-z0-9_]*$/)) { + $scope.parsedEnvVars[key] = value; + $scope.advancedEnvCount++; + } + } + } + }; + + $scope.loadEnvTemplate = function() { + var templates = { + 'web-app': 'NODE_ENV=production\nPORT=3000\nDATABASE_URL=postgresql://user:pass@localhost/db\nREDIS_URL=redis://localhost:6379\nJWT_SECRET=your-jwt-secret\nAPI_KEY=your-api-key', + 'database': 'POSTGRES_DB=myapp\nPOSTGRES_USER=user\nPOSTGRES_PASSWORD=password\nPOSTGRES_HOST=localhost\nPOSTGRES_PORT=5432', + 'api': 'API_HOST=0.0.0.0\nAPI_PORT=8080\nLOG_LEVEL=info\nCORS_ORIGIN=*\nRATE_LIMIT=1000\nAPI_KEY=your-secret-key', + 'monitoring': 'PROMETHEUS_PORT=9090\nGRAFANA_PORT=3000\nALERTMANAGER_PORT=9093\nRETENTION_TIME=15d\nSCRAPE_INTERVAL=15s' + }; + + var templateNames = Object.keys(templates); + var templateChoice = prompt('Choose a template:\n' + templateNames.map((name, i) => (i + 1) + '. ' + name).join('\n') + '\n\nEnter number or template name:'); + + if (templateChoice) { + var templateIndex = parseInt(templateChoice) - 1; + var selectedTemplate = null; + + if (templateIndex >= 0 && templateIndex < templateNames.length) { + selectedTemplate = templates[templateNames[templateIndex]]; + } else { + // Try to find by name + var templateName = templateChoice.toLowerCase().replace(/\s+/g, '-'); + if (templates[templateName]) { + selectedTemplate = templates[templateName]; + } + } + + if (selectedTemplate) { + if ($scope.advancedEnvMode) { + $scope.advancedEnvText = selectedTemplate; + $scope.parseAdvancedEnv(); + } else { + // Convert template to simple format + var lines = selectedTemplate.split('\n'); + $scope.envList = {}; + var index = 0; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line && !line.startsWith('#')) { + var equalIndex = line.indexOf('='); + if (equalIndex > 0) { + $scope.envList[index] = { + 'name': line.substring(0, equalIndex).trim(), + 'value': line.substring(equalIndex + 1).trim() + }; + index++; + } + } + } + } + + new PNotify({ + title: 'Template Loaded', + text: 'Environment variable template has been loaded successfully', + type: 'success' + }); + } + } + }; + + // Docker Compose Functions for runContainer + $scope.generateDockerCompose = function() { + // Get container information from form + var containerInfo = { + name: $scope.name || 'my-container', + image: $scope.image || 'nginx:latest', + ports: $scope.eport || {}, + volumes: $scope.volList || {}, + environment: {} + }; + + // Collect environment variables + if ($scope.advancedEnvMode && $scope.parsedEnvVars) { + containerInfo.environment = $scope.parsedEnvVars; + } else { + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + containerInfo.environment[$scope.envList[key].name] = $scope.envList[key].value; + } + } + } + + // Generate docker-compose.yml content + var composeContent = generateDockerComposeYml(containerInfo); + + // Create and download file + var blob = new Blob([composeContent], { type: 'text/yaml' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'docker-compose.yml'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: 'Docker Compose Generated', + text: 'docker-compose.yml file has been generated and downloaded', + type: 'success' + }); + }; + + $scope.generateEnvFile = function() { + var envText = ''; + + if ($scope.advancedEnvMode && $scope.advancedEnvText) { + envText = $scope.advancedEnvText; + } else { + // Convert simple mode to .env format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envText += $scope.envList[key].name + '=' + $scope.envList[key].value + '\n'; + } + } + } + + if (!envText.trim()) { + new PNotify({ + title: 'Nothing to Generate', + text: 'No environment variables to generate .env file', + type: 'warning' + }); + return; + } + + // Create and download file + var blob = new Blob([envText], { type: 'text/plain' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = '.env'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: '.env File Generated', + text: '.env file has been generated and downloaded', + type: 'success' + }); + }; + + $scope.showComposeHelp = function() { + var helpContent = ` +
+

How to use Docker Compose with Environment Variables

+
+
Step 1: Download Files
+

Click "Generate docker-compose.yml" and "Generate .env file" to download both files.

+ +
Step 2: Place Files
+

Place both files in the same directory on your server.

+ +
Step 3: Run Docker Compose
+

Run the following commands in your terminal:

+
docker compose up -d
+ +
Step 4: Update Environment Variables
+

To update environment variables:

+
    +
  1. Edit the .env file
  2. +
  3. Run: docker compose up -d
  4. +
  5. Only the environment variables will be reloaded (no container rebuild needed!)
  6. +
+ +
Benefits:
+
    +
  • No need to recreate containers
  • +
  • Faster environment variable updates
  • +
  • Version control friendly
  • +
  • Easy to share configurations
  • +
+
+
+ `; + + // Create modal for help + var modal = document.createElement('div'); + modal.className = 'modal fade'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + $(modal).modal('show'); + + // Remove modal when closed + $(modal).on('hidden.bs.modal', function() { + document.body.removeChild(modal); + }); + }; + + $scope.loadEnvFromFile = function() { + var input = document.createElement('input'); + input.type = 'file'; + input.accept = '.env,text/plain'; + input.onchange = function(event) { + var file = event.target.files[0]; + if (file) { + var reader = new FileReader(); + reader.onload = function(e) { + $scope.advancedEnvText = e.target.result; + $scope.parseAdvancedEnv(); + $scope.$apply(); + + new PNotify({ + title: 'File Loaded', + text: 'Environment variables loaded from file successfully', + type: 'success' + }); + }; + reader.readAsText(file); + } + }; + input.click(); + }; + + $scope.copyEnvToClipboard = function() { + var textToCopy = ''; + + if ($scope.advancedEnvMode) { + textToCopy = $scope.advancedEnvText; + } else { + // Convert simple format to text + var envLines = []; + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envLines.push($scope.envList[key].name + '=' + $scope.envList[key].value); + } + } + textToCopy = envLines.join('\n'); + } + + if (textToCopy) { + navigator.clipboard.writeText(textToCopy).then(function() { + new PNotify({ + title: 'Copied to Clipboard', + text: 'Environment variables copied to clipboard', + type: 'success' + }); + }).catch(function(err) { + // Fallback for older browsers + var textArea = document.createElement('textarea'); + textArea.value = textToCopy; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + new PNotify({ + title: 'Copied to Clipboard', + text: 'Environment variables copied to clipboard', + type: 'success' + }); + }); + } + }; + + $scope.clearAdvancedEnv = function() { + $scope.advancedEnvText = ''; + $scope.parsedEnvVars = {}; + $scope.advancedEnvCount = 0; + }; + var statusFile; // Watch for changes to validate ports @@ -193,14 +551,29 @@ app.controller('runContainer', function ($scope, $http) { var image = $scope.image; var numberOfEnv = Object.keys($scope.envList).length; + // Prepare environment variables based on mode + var finalEnvList = {}; + if ($scope.advancedEnvMode && $scope.parsedEnvVars) { + // Use parsed environment variables from advanced mode + finalEnvList = $scope.parsedEnvVars; + } else { + // Convert simple envList to proper format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + finalEnvList[$scope.envList[key].name] = $scope.envList[key].value; + } + } + } + var data = { name: name, tag: tag, memory: memory, dockerOwner: dockerOwner, image: image, - envList: $scope.envList, - volList: $scope.volList + envList: finalEnvList, + volList: $scope.volList, + advancedEnvMode: $scope.advancedEnvMode }; @@ -580,6 +953,12 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) { $scope.statusInterval = null; $scope.statsInterval = null; + // Advanced Environment Variable Functions for viewContainer + $scope.advancedEnvMode = false; + $scope.advancedEnvText = ''; + $scope.advancedEnvCount = 0; + $scope.parsedEnvVars = {}; + // Auto-refresh status every 5 seconds $scope.startStatusMonitoring = function() { $scope.statusInterval = $interval(function() { @@ -665,6 +1044,492 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) { $scope.envList[countEnv + 1] = {'name': '', 'value': ''}; }; + // Advanced Environment Variable Functions for viewContainer + $scope.toggleEnvMode = function() { + if ($scope.advancedEnvMode) { + // Switching to advanced mode - convert existing envList to text format + $scope.convertToAdvancedFormat(); + } else { + // Switching to simple mode - convert advanced text to envList + $scope.convertToSimpleFormat(); + } + }; + + $scope.convertToAdvancedFormat = function() { + var envLines = []; + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envLines.push($scope.envList[key].name + '=' + $scope.envList[key].value); + } + } + $scope.advancedEnvText = envLines.join('\n'); + $scope.parseAdvancedEnv(); + }; + + $scope.convertToSimpleFormat = function() { + $scope.parseAdvancedEnv(); + var newEnvList = {}; + var index = 0; + for (var key in $scope.parsedEnvVars) { + newEnvList[index] = {'name': key, 'value': $scope.parsedEnvVars[key]}; + index++; + } + $scope.envList = newEnvList; + }; + + $scope.parseAdvancedEnv = function() { + $scope.parsedEnvVars = {}; + $scope.advancedEnvCount = 0; + + if (!$scope.advancedEnvText) { + return; + } + + var lines = $scope.advancedEnvText.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + + // Skip empty lines and comments + if (!line || line.startsWith('#')) { + continue; + } + + // Parse KEY=VALUE format + var equalIndex = line.indexOf('='); + if (equalIndex > 0) { + var key = line.substring(0, equalIndex).trim(); + var value = line.substring(equalIndex + 1).trim(); + + // Remove quotes if present + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + + if (key && key.match(/^[A-Za-z_][A-Za-z0-9_]*$/)) { + $scope.parsedEnvVars[key] = value; + $scope.advancedEnvCount++; + } + } + } + }; + + $scope.copyEnvToClipboard = function() { + var textToCopy = ''; + + if ($scope.advancedEnvMode) { + textToCopy = $scope.advancedEnvText; + } else { + // Convert simple format to text + var envLines = []; + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envLines.push($scope.envList[key].name + '=' + $scope.envList[key].value); + } + } + textToCopy = envLines.join('\n'); + } + + if (textToCopy) { + navigator.clipboard.writeText(textToCopy).then(function() { + new PNotify({ + title: 'Copied to Clipboard', + text: 'Environment variables copied to clipboard', + type: 'success' + }); + }).catch(function(err) { + // Fallback for older browsers + var textArea = document.createElement('textarea'); + textArea.value = textToCopy; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + new PNotify({ + title: 'Copied to Clipboard', + text: 'Environment variables copied to clipboard', + type: 'success' + }); + }); + } + }; + + // Import/Export Functions + $scope.importEnvFromContainer = function() { + // Show modal to select container to import from + $scope.showContainerImportModal = true; + $scope.loadContainersForImport(); + }; + + $scope.loadContainersForImport = function() { + $scope.importLoading = true; + $scope.importContainers = []; + + $http.get('/dockerManager/loadContainersForImport/', { + params: { + currentContainer: $scope.cName + } + }).then(function(response) { + $scope.importContainers = response.data.containers || []; + $scope.importLoading = false; + }).catch(function(error) { + new PNotify({ + title: 'Import Failed', + text: 'Failed to load containers for import', + type: 'error' + }); + $scope.importLoading = false; + }); + }; + + $scope.selectContainerForImport = function(container) { + $scope.selectedImportContainer = container; + $scope.loadEnvFromContainer(container.name); + }; + + $scope.loadEnvFromContainer = function(containerName) { + $scope.importEnvLoading = true; + + $http.get('/dockerManager/getContainerEnv/', { + params: { + containerName: containerName + } + }).then(function(response) { + if (response.data.success) { + var envVars = response.data.envVars || {}; + + if ($scope.advancedEnvMode) { + // Convert to .env format + var envText = ''; + for (var key in envVars) { + envText += key + '=' + envVars[key] + '\n'; + } + $scope.advancedEnvText = envText; + $scope.parseAdvancedEnv(); + } else { + // Convert to simple mode + $scope.envList = {}; + var index = 0; + for (var key in envVars) { + $scope.envList[index] = {'name': key, 'value': envVars[key]}; + index++; + } + } + + $scope.showContainerImportModal = false; + new PNotify({ + title: 'Import Successful', + text: 'Environment variables imported from ' + containerName, + type: 'success' + }); + } else { + new PNotify({ + title: 'Import Failed', + text: response.data.message || 'Failed to import environment variables', + type: 'error' + }); + } + $scope.importEnvLoading = false; + }).catch(function(error) { + new PNotify({ + title: 'Import Failed', + text: 'Failed to import environment variables', + type: 'error' + }); + $scope.importEnvLoading = false; + }); + }; + + $scope.exportEnvToFile = function() { + var envText = ''; + + if ($scope.advancedEnvMode && $scope.advancedEnvText) { + envText = $scope.advancedEnvText; + } else { + // Convert simple mode to .env format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envText += $scope.envList[key].name + '=' + $scope.envList[key].value + '\n'; + } + } + } + + if (!envText.trim()) { + new PNotify({ + title: 'Nothing to Export', + text: 'No environment variables to export', + type: 'warning' + }); + return; + } + + // Create and download file + var blob = new Blob([envText], { type: 'text/plain' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = $scope.cName + '_environment.env'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: 'Export Successful', + text: 'Environment variables exported to file', + type: 'success' + }); + }; + + // Docker Compose Functions + $scope.generateDockerCompose = function() { + // Get container information + var containerInfo = { + name: $scope.cName, + image: $scope.image || 'nginx:latest', + ports: $scope.ports || {}, + volumes: $scope.volList || {}, + environment: {} + }; + + // Collect environment variables + if ($scope.advancedEnvMode && $scope.parsedEnvVars) { + containerInfo.environment = $scope.parsedEnvVars; + } else { + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + containerInfo.environment[$scope.envList[key].name] = $scope.envList[key].value; + } + } + } + + // Generate docker-compose.yml content + var composeContent = generateDockerComposeYml(containerInfo); + + // Create and download file + var blob = new Blob([composeContent], { type: 'text/yaml' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'docker-compose.yml'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: 'Docker Compose Generated', + text: 'docker-compose.yml file has been generated and downloaded', + type: 'success' + }); + }; + + $scope.generateEnvFile = function() { + var envText = ''; + + if ($scope.advancedEnvMode && $scope.advancedEnvText) { + envText = $scope.advancedEnvText; + } else { + // Convert simple mode to .env format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envText += $scope.envList[key].name + '=' + $scope.envList[key].value + '\n'; + } + } + } + + if (!envText.trim()) { + new PNotify({ + title: 'Nothing to Generate', + text: 'No environment variables to generate .env file', + type: 'warning' + }); + return; + } + + // Create and download file + var blob = new Blob([envText], { type: 'text/plain' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = '.env'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: '.env File Generated', + text: '.env file has been generated and downloaded', + type: 'success' + }); + }; + + $scope.showComposeHelp = function() { + var helpContent = ` +
+

How to use Docker Compose with Environment Variables

+
+
Step 1: Download Files
+

Click "Generate docker-compose.yml" and "Generate .env file" to download both files.

+ +
Step 2: Place Files
+

Place both files in the same directory on your server.

+ +
Step 3: Run Docker Compose
+

Run the following commands in your terminal:

+
docker compose up -d
+ +
Step 4: Update Environment Variables
+

To update environment variables:

+
    +
  1. Edit the .env file
  2. +
  3. Run: docker compose up -d
  4. +
  5. Only the environment variables will be reloaded (no container rebuild needed!)
  6. +
+ +
Benefits:
+
    +
  • No need to recreate containers
  • +
  • Faster environment variable updates
  • +
  • Version control friendly
  • +
  • Easy to share configurations
  • +
+
+
+ `; + + // Create modal for help + var modal = document.createElement('div'); + modal.className = 'modal fade'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + $(modal).modal('show'); + + // Remove modal when closed + $(modal).on('hidden.bs.modal', function() { + document.body.removeChild(modal); + }); + }; + + // Helper function to generate Docker Compose YAML + function generateDockerComposeYml(containerInfo) { + var yml = 'version: \'3.8\'\n\n'; + yml += 'services:\n'; + yml += ' ' + containerInfo.name + ':\n'; + yml += ' image: ' + containerInfo.image + '\n'; + yml += ' container_name: ' + containerInfo.name + '\n'; + + // Add ports + var ports = Object.keys(containerInfo.ports); + if (ports.length > 0) { + yml += ' ports:\n'; + for (var i = 0; i < ports.length; i++) { + var port = ports[i]; + if (containerInfo.ports[port]) { + yml += ' - "' + containerInfo.ports[port] + ':' + port + '"\n'; + } + } + } + + // Add volumes + var volumes = Object.keys(containerInfo.volumes); + if (volumes.length > 0) { + yml += ' volumes:\n'; + for (var i = 0; i < volumes.length; i++) { + var volume = volumes[i]; + if (containerInfo.volumes[volume]) { + yml += ' - ' + containerInfo.volumes[volume] + ':' + volume + '\n'; + } + } + } + + // Add environment variables + var envVars = Object.keys(containerInfo.environment); + if (envVars.length > 0) { + yml += ' environment:\n'; + for (var i = 0; i < envVars.length; i++) { + var envVar = envVars[i]; + yml += ' - ' + envVar + '=' + containerInfo.environment[envVar] + '\n'; + } + } + + // Add restart policy + yml += ' restart: unless-stopped\n'; + + return yml; + } + + // Helper function to generate Docker Compose YAML (for runContainer) + function generateDockerComposeYml(containerInfo) { + var yml = 'version: \'3.8\'\n\n'; + yml += 'services:\n'; + yml += ' ' + containerInfo.name + ':\n'; + yml += ' image: ' + containerInfo.image + '\n'; + yml += ' container_name: ' + containerInfo.name + '\n'; + + // Add ports + var ports = Object.keys(containerInfo.ports); + if (ports.length > 0) { + yml += ' ports:\n'; + for (var i = 0; i < ports.length; i++) { + var port = ports[i]; + if (containerInfo.ports[port]) { + yml += ' - "' + containerInfo.ports[port] + ':' + port + '"\n'; + } + } + } + + // Add volumes + var volumes = Object.keys(containerInfo.volumes); + if (volumes.length > 0) { + yml += ' volumes:\n'; + for (var i = 0; i < volumes.length; i++) { + var volume = volumes[i]; + if (containerInfo.volumes[volume]) { + yml += ' - ' + containerInfo.volumes[volume] + ':' + volume + '\n'; + } + } + } + + // Add environment variables + var envVars = Object.keys(containerInfo.environment); + if (envVars.length > 0) { + yml += ' environment:\n'; + for (var i = 0; i < envVars.length; i++) { + var envVar = envVars[i]; + yml += ' - ' + envVar + '=' + containerInfo.environment[envVar] + '\n'; + } + } + + // Add restart policy + yml += ' restart: unless-stopped\n'; + + return yml; + } + $scope.showTop = function () { $scope.topHead = []; $scope.topProcesses = []; @@ -832,13 +1697,28 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) { url = "/docker/saveContainerSettings"; $scope.savingSettings = true; + // Prepare environment variables based on mode + var finalEnvList = {}; + if ($scope.advancedEnvMode && $scope.parsedEnvVars) { + // Use parsed environment variables from advanced mode + finalEnvList = $scope.parsedEnvVars; + } else { + // Convert simple envList to proper format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + finalEnvList[$scope.envList[key].name] = $scope.envList[key].value; + } + } + } + var data = { name: $scope.cName, memory: $scope.memory, startOnReboot: $scope.startOnReboot, envConfirmation: $scope.envConfirmation, - envList: $scope.envList, - volList: $scope.volList + envList: finalEnvList, + volList: $scope.volList, + advancedEnvMode: $scope.advancedEnvMode }; diff --git a/dockerManager/templates/dockerManager/runContainer.html b/dockerManager/templates/dockerManager/runContainer.html index feeef0c51..c56bbf6c9 100644 --- a/dockerManager/templates/dockerManager/runContainer.html +++ b/dockerManager/templates/dockerManager/runContainer.html @@ -510,6 +510,108 @@ to { opacity: 1; } } + /* Toggle Switch Styles */ + .switch input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; + } + + .slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + } + + input:checked + .slider { + background-color: var(--accent-color, #5b5fcf); + } + + input:checked + .slider:before { + transform: translateX(26px); + } + + /* Docker Compose Information Card Styles */ + .compose-info-card { + background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%); + border: 1px solid #e8e9ff; + border-radius: 16px; + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 4px 16px rgba(0,0,0,0.05); + } + + .compose-benefits h4 { + color: #1e293b; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .compose-benefits h4 i { + color: #007bff; + font-size: 1.2rem; + } + + .compose-benefits ul { + margin: 1rem 0; + padding-left: 1.5rem; + } + + .compose-benefits li { + margin-bottom: 0.75rem; + color: #495057; + line-height: 1.6; + } + + .compose-actions { + margin-top: 2rem; + display: flex; + gap: 1rem; + flex-wrap: wrap; + } + + .compose-actions .btn { + border-radius: 8px; + font-weight: 500; + padding: 0.75rem 1.5rem; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + } + + .compose-actions .btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0,0,0,0.15); + } + + .compose-actions .btn i { + margin-right: 0.5rem; + } + + .slider:hover { + box-shadow: 0 0 8px rgba(91, 95, 207, 0.3); + } + @media (max-width: 768px) { .form-row { grid-template-columns: 1fr; @@ -689,6 +791,41 @@ {% endif %} + +
+
+
+ +
+
+

{% trans "Docker Compose Benefits" %}

+

{% trans "Use Docker Compose for easier environment variable management" %}

+
+
+ +
+
+

{% trans "With Docker Compose, you can:" %}

+
    +
  • {% trans "Keep your environment variables in a separate .env file" %}
  • +
  • {% trans "When you change the .env, you don't need to rebuild the entire container image" %}
  • +
  • {% trans "You can simply run docker compose up -d again, and only the parts that changed (like the environment variables) will be reloaded" %}
  • +
+
+ + + +
+
+
+
+
@@ -701,34 +838,138 @@
-
-
- {% trans "Environment Variables" %} - + +
+
+
+ +

+ {% trans "Choose between simple line-by-line input or advanced bulk editing mode" %} +

+
+
+ +
+ + {% trans "Simple Mode" %} + {% trans "Advanced Mode" %} + +
+
+
- - {% for env, value in envList.items %} - - {% endfor %} + +
+
+
+ {% trans "Environment Variables" %} +
+ + +
+
-
- - - + + {% for env, value in envList.items %} + + {% endfor %} + +
+ + + +
+ +
+ + {% trans "No environment variables configured. Click 'Add Variable' to add one." %} +
+
-
- - {% trans "No environment variables configured. Click 'Add Variable' to add one." %} + +
+
+
+
+

+ {% trans "Advanced Environment Variables" %} +

+

+ {% trans "Switch to advanced mode to copy & paste multiple variables" %} +

+
+
+ + +
+
+ +
+
+ +

+ {% trans "Enter environment variables in KEY=VALUE format, one per line. Example:" %} +

+
+ DATABASE_URL=postgresql://user:pass@localhost/db
+ API_KEY=your-secret-key
+ DEBUG=true
+ PORT=3000 +
+
+ + + +
+
+ + {% trans "Parsed" %} {{ advancedEnvCount }} {% trans "environment variables" %} + {% trans "No environment variables detected" %} +
+
+ + +
+
+
diff --git a/dockerManager/templates/dockerManager/viewContainer.html b/dockerManager/templates/dockerManager/viewContainer.html index 2a80be27b..6cbf09add 100644 --- a/dockerManager/templates/dockerManager/viewContainer.html +++ b/dockerManager/templates/dockerManager/viewContainer.html @@ -836,35 +836,141 @@
+ +
+ +
+
+
+ +

+ {% trans "Enable advanced mode for bulk editing environment variables" %} +

+
+
+ +
+
+
+
+ + +
+ +
+
+
+
+ + {% trans "Docker Compose Environment Variables" %} +
+
+

{% trans "With Docker Compose, you can:" %}

+
    +
  • {% trans "Keep your environment variables in a separate .env file" %}
  • +
  • {% trans "When you change the .env, you don't need to rebuild the entire container image" %}
  • +
  • {% trans "You can simply run docker compose up -d again, and only the parts that changed (like the environment variables) will be reloaded" %}
  • +
+
+ + + +
+
+
+
+
+ {% for env, value in envList.items %} {% endfor %} -
-
- - -
- +
+
+ + +
+ +
+
+ +
-
- +
+ +
+
+
-
-
- + +
+
+ +
+
+
+
+
+ {% trans "Advanced Environment Variables" %} +
+

+ {% trans "Edit environment variables in bulk using KEY=VALUE format" %} +

+
+
+ + + +
+
+ +
+ + +
+ + {% trans "Parsed" %} {{ advancedEnvCount }} {% trans "environment variables" %} + {% trans "No environment variables detected" %} +
+
+
+
@@ -1052,6 +1158,220 @@
+ + + + + {% endblock %} {% block footer_scripts %} diff --git a/dockerManager/urls.py b/dockerManager/urls.py index ff9197bb8..cb6b5f771 100644 --- a/dockerManager/urls.py +++ b/dockerManager/urls.py @@ -20,6 +20,8 @@ urlpatterns = [ re_path(r'^saveContainerSettings$', views.saveContainerSettings, name='saveContainerSettings'), re_path(r'^getContainerTop$', views.getContainerTop, name='getContainerTop'), re_path(r'^assignContainer$', views.assignContainer, name='assignContainer'), + re_path(r'^loadContainersForImport$', views.loadContainersForImport, name='loadContainersForImport'), + re_path(r'^getContainerEnv$', views.getContainerEnv, name='getContainerEnv'), re_path(r'^searchImage$', views.searchImage, name='searchImage'), re_path(r'^manageImages$', views.manageImages, name='manageImages'), re_path(r'^getImageHistory$', views.getImageHistory, name='getImageHistory'), diff --git a/dockerManager/views.py b/dockerManager/views.py index 3866d6537..d8522e3aa 100644 --- a/dockerManager/views.py +++ b/dockerManager/views.py @@ -556,5 +556,111 @@ def executeContainerCommand(request): coreResult = cm.executeContainerCommand(userID, json.loads(request.body)) return coreResult + except KeyError: + return redirect(loadLoginPage) + + +def loadContainersForImport(request): + """ + Load all containers for import selection, excluding the current container + """ + try: + userID = request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson() + + currentContainer = request.GET.get('currentContainer', '') + + # Get all containers using Docker API + import docker + dockerClient = docker.from_env() + containers = dockerClient.containers.list(all=True) + + containerList = [] + for container in containers: + # Skip the current container + if container.name == currentContainer: + continue + + # Get container info + containerInfo = { + 'name': container.name, + 'image': container.image.tags[0] if container.image.tags else container.image.id, + 'status': container.status, + 'id': container.short_id + } + + # Count environment variables + try: + envVars = container.attrs.get('Config', {}).get('Env', []) + containerInfo['envCount'] = len(envVars) + except: + containerInfo['envCount'] = 0 + + containerList.append(containerInfo) + + return HttpResponse(json.dumps({ + 'success': 1, + 'containers': containerList + }), content_type='application/json') + + except Exception as e: + return HttpResponse(json.dumps({ + 'success': 0, + 'message': str(e) + }), content_type='application/json') + except KeyError: + return redirect(loadLoginPage) + + +def getContainerEnv(request): + """ + Get environment variables from a specific container + """ + try: + userID = request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson() + + containerName = request.GET.get('containerName', '') + + if not containerName: + return HttpResponse(json.dumps({ + 'success': 0, + 'message': 'Container name is required' + }), content_type='application/json') + + # Get container using Docker API + import docker + dockerClient = docker.from_env() + container = dockerClient.containers.get(containerName) + + # Extract environment variables + envVars = {} + envList = container.attrs.get('Config', {}).get('Env', []) + + for envVar in envList: + if '=' in envVar: + key, value = envVar.split('=', 1) + envVars[key] = value + + return HttpResponse(json.dumps({ + 'success': 1, + 'envVars': envVars + }), content_type='application/json') + + except Exception as e: + return HttpResponse(json.dumps({ + 'success': 0, + 'message': str(e) + }), content_type='application/json') except KeyError: return redirect(loadLoginPage) \ No newline at end of file