finalized update feature
This commit is contained in:
@@ -1409,7 +1409,7 @@ async function startUpdate(data) {
|
||||
}
|
||||
|
||||
// Update the existing stack instead of creating a new one
|
||||
const stackResult = await updateStack(dockerComposeResult.content, instanceData.instance.portainer_stack_id, port);
|
||||
const stackResult = await updateStack(dockerComposeResult.content, instanceData.instance.portainer_stack_id, port, instanceData.instance);
|
||||
if (!stackResult.success) {
|
||||
throw new Error(`Failed to update stack: ${stackResult.error}`);
|
||||
}
|
||||
@@ -2446,70 +2446,95 @@ async function checkInstanceHealth(instanceUrl) {
|
||||
|
||||
const data = await statusResponse.json();
|
||||
|
||||
// Update the health check step
|
||||
const healthStepElement = document.querySelectorAll('.step-item')[10]; // Adjust index based on your steps
|
||||
healthStepElement.classList.remove('active');
|
||||
healthStepElement.classList.add('completed');
|
||||
const statusText = healthStepElement.querySelector('.step-status');
|
||||
// Update the health check step - check if we're in update mode or launch mode
|
||||
const isUpdate = window.isUpdate;
|
||||
const healthStepIndex = isUpdate ? 4 : 10; // Different indices for update vs launch
|
||||
const healthStepElement = document.querySelectorAll('.step-item')[healthStepIndex];
|
||||
|
||||
if (data.status === 'active') {
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
statusText.textContent = `Instance is healthy (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed)`;
|
||||
if (healthStepElement) {
|
||||
healthStepElement.classList.remove('active');
|
||||
healthStepElement.classList.add('completed');
|
||||
const statusText = healthStepElement.querySelector('.step-status');
|
||||
|
||||
// Update progress bar to 100%
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.textContent = '100%';
|
||||
progressBar.classList.remove('progress-bar-animated');
|
||||
progressBar.classList.add('bg-success');
|
||||
progressText.textContent = `Health check completed successfully in ${elapsedTime}s`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
attempts: currentAttempt,
|
||||
elapsedTime: elapsedTime
|
||||
};
|
||||
} else if (data.status === 'inactive') {
|
||||
console.log(`Stack ${stackName} is inactive, continuing to poll...`);
|
||||
lastKnownStatus = 'inactive';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
} else if (data.status === 'starting') {
|
||||
console.log(`Stack ${stackName} is starting up, continuing to poll...`);
|
||||
lastKnownStatus = 'starting';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is initializing (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
if (data.status === 'active') {
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
statusText.textContent = `Instance is healthy (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed)`;
|
||||
|
||||
// Update progress bar to 100% if it exists
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.textContent = '100%';
|
||||
progressBar.classList.remove('progress-bar-animated');
|
||||
progressBar.classList.add('bg-success');
|
||||
progressText.textContent = `Health check completed successfully in ${elapsedTime}s`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
attempts: currentAttempt,
|
||||
elapsedTime: elapsedTime
|
||||
};
|
||||
} else if (data.status === 'inactive') {
|
||||
console.log(`Stack ${stackName} is inactive, continuing to poll...`);
|
||||
lastKnownStatus = 'inactive';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
} else if (data.status === 'starting') {
|
||||
console.log(`Stack ${stackName} is starting up, continuing to poll...`);
|
||||
lastKnownStatus = 'starting';
|
||||
if (progressText) {
|
||||
progressText.textContent = `Stack is initializing (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Instance is not healthy');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Instance is not healthy');
|
||||
// If step element doesn't exist, just log the status
|
||||
console.log(`Health check - Instance status: ${data.status}`);
|
||||
if (data.status === 'active') {
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
attempts: currentAttempt,
|
||||
elapsedTime: elapsedTime
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Health check attempt ${currentAttempt} failed:`, error);
|
||||
|
||||
// Update status to show current attempt and elapsed time
|
||||
const healthStepElement = document.querySelectorAll('.step-item')[10];
|
||||
const statusText = healthStepElement.querySelector('.step-status');
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
statusText.textContent = `Health check failed (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed): ${error.message}`;
|
||||
// Update status to show current attempt and elapsed time - check if step element exists
|
||||
const isUpdate = window.isUpdate;
|
||||
const healthStepIndex = isUpdate ? 4 : 10;
|
||||
const healthStepElement = document.querySelectorAll('.step-item')[healthStepIndex];
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
const progressPercent = Math.min((currentAttempt / maxRetries) * 100, 100);
|
||||
progressBar.style.width = `${progressPercent}%`;
|
||||
progressBar.textContent = `${Math.round(progressPercent)}%`;
|
||||
progressText.textContent = `Attempt ${currentAttempt}/${maxRetries} (${elapsedTime}s elapsed)`;
|
||||
if (healthStepElement) {
|
||||
const statusText = healthStepElement.querySelector('.step-status');
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
statusText.textContent = `Health check failed (Attempt ${currentAttempt}/${maxRetries}, ${elapsedTime}s elapsed): ${error.message}`;
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
const progressPercent = Math.min((currentAttempt / maxRetries) * 100, 100);
|
||||
progressBar.style.width = `${progressPercent}%`;
|
||||
progressBar.textContent = `${Math.round(progressPercent)}%`;
|
||||
progressText.textContent = `Attempt ${currentAttempt}/${maxRetries} (${elapsedTime}s elapsed)`;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentAttempt === maxRetries || (Date.now() - startTime > maxTotalTime)) {
|
||||
// Update progress bar to show failure
|
||||
// Update progress bar to show failure if it exists
|
||||
const progressBar = document.getElementById('healthProgress');
|
||||
const progressText = document.getElementById('healthProgressText');
|
||||
if (progressBar && progressText) {
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
progressBar.classList.remove('progress-bar-animated');
|
||||
progressBar.classList.add('bg-danger');
|
||||
progressText.textContent = `Health check failed after ${currentAttempt} attempts (${elapsedTime}s)`;
|
||||
@@ -2517,7 +2542,7 @@ async function checkInstanceHealth(instanceUrl) {
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Health check failed after ${currentAttempt} attempts (${elapsedTime}s): ${error.message}`
|
||||
error: `Health check failed after ${currentAttempt} attempts (${Math.round((Date.now() - startTime) / 1000)}s): ${error.message}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2525,7 +2550,8 @@ async function checkInstanceHealth(instanceUrl) {
|
||||
await new Promise(resolve => setTimeout(resolve, baseDelay));
|
||||
currentAttempt++;
|
||||
|
||||
// Update progress bar in real-time
|
||||
// Update progress bar in real-time if it exists
|
||||
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
||||
updateHealthProgress(currentAttempt, maxRetries, elapsedTime);
|
||||
}
|
||||
}
|
||||
@@ -2543,127 +2569,6 @@ function updateHealthProgress(currentAttempt, maxRetries, elapsedTime) {
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateInstance(instanceUrl, instanceId) {
|
||||
try {
|
||||
// First check if instance is already authenticated
|
||||
const instancesResponse = await fetch('/instances');
|
||||
const text = await instancesResponse.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, 'text/html');
|
||||
|
||||
// Find the instance with matching URL
|
||||
const instanceRow = Array.from(doc.querySelectorAll('table tbody tr')).find(row => {
|
||||
const urlCell = row.querySelector('td:nth-child(7) a'); // URL is in the 7th column
|
||||
return urlCell && urlCell.textContent.trim() === instanceUrl;
|
||||
});
|
||||
|
||||
if (!instanceRow) {
|
||||
throw new Error('Instance not found in database');
|
||||
}
|
||||
|
||||
// Get the instance ID from the status badge's data attribute
|
||||
const statusBadge = instanceRow.querySelector('[data-instance-id]');
|
||||
if (!statusBadge) {
|
||||
throw new Error('Could not find instance ID');
|
||||
}
|
||||
|
||||
const dbInstanceId = statusBadge.dataset.instanceId;
|
||||
|
||||
// Check if already authenticated
|
||||
const authStatusResponse = await fetch(`/instances/${dbInstanceId}/auth-status`);
|
||||
if (!authStatusResponse.ok) {
|
||||
throw new Error('Failed to check authentication status');
|
||||
}
|
||||
|
||||
const authStatus = await authStatusResponse.json();
|
||||
if (authStatus.authenticated) {
|
||||
console.log('Instance is already authenticated');
|
||||
return {
|
||||
success: true,
|
||||
message: 'Instance is already authenticated',
|
||||
alreadyAuthenticated: true
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Attempting login to:', `${instanceUrl}/api/admin/login`);
|
||||
|
||||
// First login to get token
|
||||
const loginResponse = await fetch(`${instanceUrl}/api/admin/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'administrator@docupulse.com',
|
||||
password: 'changeme'
|
||||
})
|
||||
});
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
const errorText = await loginResponse.text();
|
||||
throw new Error(`Login failed: ${errorText}`);
|
||||
}
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
if (loginData.status !== 'success' || !loginData.token) {
|
||||
throw new Error('Login failed: Invalid response from server');
|
||||
}
|
||||
|
||||
const token = loginData.token;
|
||||
|
||||
// Then create management API key
|
||||
const keyResponse = await fetch(`${instanceUrl}/api/admin/management-api-key`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: `Connection from ${window.location.hostname}`
|
||||
})
|
||||
});
|
||||
|
||||
if (!keyResponse.ok) {
|
||||
const errorText = await keyResponse.text();
|
||||
throw new Error(`Failed to create API key: ${errorText}`);
|
||||
}
|
||||
|
||||
const keyData = await keyResponse.json();
|
||||
if (!keyData.api_key) {
|
||||
throw new Error('No API key received from server');
|
||||
}
|
||||
|
||||
// Save the token to our database
|
||||
const saveResponse = await fetch(`/instances/${dbInstanceId}/save-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({ token: keyData.api_key })
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
const errorText = await saveResponse.text();
|
||||
throw new Error(`Failed to save token: ${errorText}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Successfully authenticated instance',
|
||||
alreadyAuthenticated: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function applyCompanyInformation(instanceUrl, company) {
|
||||
try {
|
||||
console.log('Applying company information to:', instanceUrl);
|
||||
@@ -3303,12 +3208,37 @@ function generateStackName(port) {
|
||||
}
|
||||
|
||||
// Add new function to update existing stack
|
||||
async function updateStack(dockerComposeContent, stackId, port) {
|
||||
async function updateStack(dockerComposeContent, stackId, port, instanceData = null) {
|
||||
try {
|
||||
console.log('Updating existing stack:', stackId);
|
||||
console.log('Port:', port);
|
||||
console.log('Modified docker-compose content length:', dockerComposeContent.length);
|
||||
|
||||
// For updates, we only need to update version-related environment variables
|
||||
// All other environment variables (pricing tiers, quotas, etc.) should be preserved
|
||||
// We also preserve the existing docker-compose configuration
|
||||
const envVars = [
|
||||
{
|
||||
name: 'APP_VERSION',
|
||||
value: window.currentDeploymentVersion || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'GIT_COMMIT',
|
||||
value: window.currentDeploymentCommit || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'GIT_BRANCH',
|
||||
value: window.currentDeploymentBranch || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'DEPLOYED_AT',
|
||||
value: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
console.log('Updating stack with version environment variables:', envVars);
|
||||
console.log('Preserving existing docker-compose configuration');
|
||||
|
||||
const response = await fetch('/api/admin/update-stack', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -3317,33 +3247,8 @@ async function updateStack(dockerComposeContent, stackId, port) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
stack_id: stackId,
|
||||
StackFileContent: dockerComposeContent,
|
||||
Env: [
|
||||
{
|
||||
name: 'PORT',
|
||||
value: port.toString()
|
||||
},
|
||||
{
|
||||
name: 'ISMASTER',
|
||||
value: 'false'
|
||||
},
|
||||
{
|
||||
name: 'APP_VERSION',
|
||||
value: window.currentDeploymentVersion || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'GIT_COMMIT',
|
||||
value: window.currentDeploymentCommit || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'GIT_BRANCH',
|
||||
value: window.currentDeploymentBranch || 'unknown'
|
||||
},
|
||||
{
|
||||
name: 'DEPLOYED_AT',
|
||||
value: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
// Don't send StackFileContent during updates - preserve existing configuration
|
||||
Env: envVars
|
||||
})
|
||||
});
|
||||
|
||||
@@ -3454,6 +3359,127 @@ async function updateStack(dockerComposeContent, stackId, port) {
|
||||
return pollResult;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateInstance(instanceUrl, instanceId) {
|
||||
try {
|
||||
// First check if instance is already authenticated
|
||||
const instancesResponse = await fetch('/instances');
|
||||
const text = await instancesResponse.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, 'text/html');
|
||||
|
||||
// Find the instance with matching URL
|
||||
const instanceRow = Array.from(doc.querySelectorAll('table tbody tr')).find(row => {
|
||||
const urlCell = row.querySelector('td:nth-child(7) a'); // URL is in the 7th column
|
||||
return urlCell && urlCell.textContent.trim() === instanceUrl;
|
||||
});
|
||||
|
||||
if (!instanceRow) {
|
||||
throw new Error('Instance not found in database');
|
||||
}
|
||||
|
||||
// Get the instance ID from the status badge's data attribute
|
||||
const statusBadge = instanceRow.querySelector('[data-instance-id]');
|
||||
if (!statusBadge) {
|
||||
throw new Error('Could not find instance ID');
|
||||
}
|
||||
|
||||
const dbInstanceId = statusBadge.dataset.instanceId;
|
||||
|
||||
// Check if already authenticated
|
||||
const authStatusResponse = await fetch(`/instances/${dbInstanceId}/auth-status`);
|
||||
if (!authStatusResponse.ok) {
|
||||
throw new Error('Failed to check authentication status');
|
||||
}
|
||||
|
||||
const authStatus = await authStatusResponse.json();
|
||||
if (authStatus.authenticated) {
|
||||
console.log('Instance is already authenticated');
|
||||
return {
|
||||
success: true,
|
||||
message: 'Instance is already authenticated',
|
||||
alreadyAuthenticated: true
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Attempting login to:', `${instanceUrl}/api/admin/login`);
|
||||
|
||||
// First login to get token
|
||||
const loginResponse = await fetch(`${instanceUrl}/api/admin/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'administrator@docupulse.com',
|
||||
password: 'changeme'
|
||||
})
|
||||
});
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
const errorText = await loginResponse.text();
|
||||
throw new Error(`Login failed: ${errorText}`);
|
||||
}
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
if (loginData.status !== 'success' || !loginData.token) {
|
||||
throw new Error('Login failed: Invalid response from server');
|
||||
}
|
||||
|
||||
const token = loginData.token;
|
||||
|
||||
// Then create management API key
|
||||
const keyResponse = await fetch(`${instanceUrl}/api/admin/management-api-key`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: `Connection from ${window.location.hostname}`
|
||||
})
|
||||
});
|
||||
|
||||
if (!keyResponse.ok) {
|
||||
const errorText = await keyResponse.text();
|
||||
throw new Error(`Failed to create API key: ${errorText}`);
|
||||
}
|
||||
|
||||
const keyData = await keyResponse.json();
|
||||
if (!keyData.api_key) {
|
||||
throw new Error('No API key received from server');
|
||||
}
|
||||
|
||||
// Save the token to our database
|
||||
const saveResponse = await fetch(`/instances/${dbInstanceId}/save-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({ token: keyData.api_key })
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
const errorText = await saveResponse.text();
|
||||
throw new Error(`Failed to save token: ${errorText}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Successfully authenticated instance',
|
||||
alreadyAuthenticated: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
|
||||
Reference in New Issue
Block a user