mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
feat: force-reinstall option for apps
This commit is contained in:
parent
04e169fe7b
commit
9bb4ff5afc
|
|
@ -50,6 +50,16 @@ export default class SystemController {
|
|||
return await this.systemService.checkLatestVersion();
|
||||
}
|
||||
|
||||
async forceReinstallService({ request, response }: HttpContext) {
|
||||
const payload = await request.validateUsing(installServiceValidator);
|
||||
const result = await this.dockerService.forceReinstall(payload.service_name);
|
||||
if (!result) {
|
||||
response.internalServerError({ error: 'Failed to force reinstall service' });
|
||||
return;
|
||||
}
|
||||
response.send({ success: result.success, message: result.message });
|
||||
}
|
||||
|
||||
async requestSystemUpdate({ response }: HttpContext) {
|
||||
if (!this.systemUpdateService.isSidecarAvailable()) {
|
||||
response.status(503).send({
|
||||
|
|
|
|||
|
|
@ -194,6 +194,140 @@ export class DockerService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force reinstall a service by stopping, removing, and recreating its container.
|
||||
* This method will also clear any associated volumes/data.
|
||||
* Handles edge cases gracefully (e.g., container not running, container not found).
|
||||
*/
|
||||
async forceReinstall(serviceName: string): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const service = await Service.query().where('service_name', serviceName).first()
|
||||
if (!service) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Service ${serviceName} not found`,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if installation is already in progress
|
||||
if (this.activeInstallations.has(serviceName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Service ${serviceName} installation is already in progress`,
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as installing to prevent concurrent operations
|
||||
this.activeInstallations.add(serviceName)
|
||||
service.installation_status = 'installing'
|
||||
await service.save()
|
||||
|
||||
this._broadcast(
|
||||
serviceName,
|
||||
'reinstall-starting',
|
||||
`Starting force reinstall for ${serviceName}...`
|
||||
)
|
||||
|
||||
// Step 1: Try to stop and remove the container if it exists
|
||||
try {
|
||||
const containers = await this.docker.listContainers({ all: true })
|
||||
const container = containers.find((c) => c.Names.includes(`/${serviceName}`))
|
||||
|
||||
if (container) {
|
||||
const dockerContainer = this.docker.getContainer(container.Id)
|
||||
|
||||
// Only try to stop if it's running
|
||||
if (container.State === 'running') {
|
||||
this._broadcast(serviceName, 'stopping', `Stopping container...`)
|
||||
await dockerContainer.stop({ t: 10 }).catch((error) => {
|
||||
// If already stopped, continue
|
||||
if (!error.message.includes('already stopped')) {
|
||||
logger.warn(`Error stopping container: ${error.message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Step 2: Remove the container
|
||||
this._broadcast(serviceName, 'removing', `Removing container...`)
|
||||
await dockerContainer.remove({ force: true }).catch((error) => {
|
||||
logger.warn(`Error removing container: ${error.message}`)
|
||||
})
|
||||
} else {
|
||||
this._broadcast(
|
||||
serviceName,
|
||||
'no-container',
|
||||
`No existing container found, proceeding with installation...`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Error during container cleanup: ${error.message}`)
|
||||
this._broadcast(
|
||||
serviceName,
|
||||
'cleanup-warning',
|
||||
`Warning during cleanup: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
// Step 3: Clear volumes/data if needed
|
||||
try {
|
||||
this._broadcast(serviceName, 'clearing-volumes', `Checking for volumes to clear...`)
|
||||
const volumes = await this.docker.listVolumes()
|
||||
const serviceVolumes =
|
||||
volumes.Volumes?.filter(
|
||||
(v) => v.Name.includes(serviceName) || v.Labels?.service === serviceName
|
||||
) || []
|
||||
|
||||
for (const vol of serviceVolumes) {
|
||||
try {
|
||||
const volume = this.docker.getVolume(vol.Name)
|
||||
await volume.remove({ force: true })
|
||||
this._broadcast(serviceName, 'volume-removed', `Removed volume: ${vol.Name}`)
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to remove volume ${vol.Name}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceVolumes.length === 0) {
|
||||
this._broadcast(serviceName, 'no-volumes', `No volumes found to clear`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Error during volume cleanup: ${error.message}`)
|
||||
this._broadcast(
|
||||
serviceName,
|
||||
'volume-cleanup-warning',
|
||||
`Warning during volume cleanup: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
// Step 4: Mark service as uninstalled
|
||||
service.installed = false
|
||||
service.installation_status = 'installing'
|
||||
await service.save()
|
||||
|
||||
// Step 5: Recreate the container
|
||||
this._broadcast(serviceName, 'recreating', `Recreating container...`)
|
||||
const containerConfig = this._parseContainerConfig(service.container_config)
|
||||
|
||||
// Execute installation asynchronously and handle cleanup
|
||||
this._createContainer(service, containerConfig).catch(async (error) => {
|
||||
logger.error(`Reinstallation failed for ${serviceName}: ${error.message}`)
|
||||
await this._cleanupFailedInstallation(serviceName)
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Service ${serviceName} force reinstall initiated successfully. You can receive updates via server-sent events.`,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Force reinstall failed for ${serviceName}: ${error.message}`)
|
||||
await this._cleanupFailedInstallation(serviceName)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to force reinstall service ${serviceName}: ${error.message}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the long-running process of creating a Docker container for a service.
|
||||
* NOTE: This method should not be called directly. Instead, use `createContainerPreflight` to check prerequisites first
|
||||
|
|
|
|||
|
|
@ -121,6 +121,16 @@ class API {
|
|||
})()
|
||||
}
|
||||
|
||||
async forceReinstallService(service_name: string) {
|
||||
return catchInternal(async () => {
|
||||
const response = await this.client.post<{ success: boolean; message: string }>(
|
||||
`/system/services/force-reinstall`,
|
||||
{ service_name }
|
||||
)
|
||||
return response.data
|
||||
})()
|
||||
}
|
||||
|
||||
async getInternetStatus() {
|
||||
return catchInternal(async () => {
|
||||
const response = await this.client.get<boolean>('/system/internet-status')
|
||||
|
|
|
|||
|
|
@ -106,7 +106,60 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
|
|||
}
|
||||
}
|
||||
|
||||
async function handleForceReinstall(record: ServiceSlim) {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await api.forceReinstallService(record.service_name)
|
||||
if (!response) {
|
||||
throw new Error('An internal error occurred while trying to force reinstall the service.')
|
||||
}
|
||||
if (!response.success) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
||||
closeAllModals()
|
||||
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
window.location.reload() // Reload the page to reflect changes
|
||||
}, 3000) // Add small delay to allow for the action to complete
|
||||
} catch (error) {
|
||||
console.error(`Error force reinstalling service ${record.service_name}:`, error)
|
||||
showError(`Failed to force reinstall service: ${error.message || 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
const AppActions = ({ record }: { record: ServiceSlim }) => {
|
||||
const ForceReinstallButton = () => (
|
||||
<StyledButton
|
||||
icon="ExclamationTriangleIcon"
|
||||
variant="action"
|
||||
onClick={() => {
|
||||
openModal(
|
||||
<StyledModal
|
||||
title={'Force Reinstall?'}
|
||||
onConfirm={() => handleForceReinstall(record)}
|
||||
onCancel={closeAllModals}
|
||||
open={true}
|
||||
confirmText={'Force Reinstall'}
|
||||
cancelText="Cancel"
|
||||
>
|
||||
<p className="text-gray-700">
|
||||
Are you sure you want to force reinstall {record.service_name}? This will{' '}
|
||||
<strong>WIPE ALL DATA</strong> for this service and cannot be undone. You should
|
||||
only do this if the service is malfunctioning and other troubleshooting steps have
|
||||
failed.
|
||||
</p>
|
||||
</StyledModal>,
|
||||
`${record.service_name}-force-reinstall-modal`
|
||||
)
|
||||
}}
|
||||
disabled={isInstalling}
|
||||
>
|
||||
Force Reinstall
|
||||
</StyledButton>
|
||||
)
|
||||
|
||||
if (!record) return null
|
||||
if (!record.installed) {
|
||||
return (
|
||||
|
|
@ -120,6 +173,7 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
|
|||
>
|
||||
Install
|
||||
</StyledButton>
|
||||
<ForceReinstallButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -189,6 +243,7 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
|
|||
Restart
|
||||
</StyledButton>
|
||||
)}
|
||||
<ForceReinstallButton />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ router
|
|||
router.get('/services', [SystemController, 'getServices'])
|
||||
router.post('/services/affect', [SystemController, 'affectService'])
|
||||
router.post('/services/install', [SystemController, 'installService'])
|
||||
router.post('/services/force-reinstall', [SystemController, 'forceReinstallService'])
|
||||
router.get('/latest-version', [SystemController, 'checkLatestVersion'])
|
||||
router.post('/update', [SystemController, 'requestSystemUpdate'])
|
||||
router.get('/update/status', [SystemController, 'getSystemUpdateStatus'])
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user