diff --git a/core/frontend/src/components/cloud/CloudTrayMenu.vue b/core/frontend/src/components/cloud/CloudTrayMenu.vue index 97928fbf0b..ee443455ee 100644 --- a/core/frontend/src/components/cloud/CloudTrayMenu.vue +++ b/core/frontend/src/components/cloud/CloudTrayMenu.vue @@ -197,13 +197,15 @@ import { FilebrowserFile } from '@/types/filebrowser' import { InstalledExtensionData, RunningContainer } from '@/types/kraken' import back_axios from '@/utils/api' import { prettifySize } from '@/utils/helper_functions' +import { + installOrUpdateMajorTom, + MAJOR_TOM_IDENTIFIER, +} from '@/utils/major_tom' import PullTracker from '@/utils/pull_tracker' const KRAKEN_API_URL = '/kraken/v1.0' -const MAJOR_TOM_CLOUD_URL = 'https://blueos.cloud/major_tom/install' - -const MAJOR_TOM_EXTENSION_IDENTIFIER = 'blueos.major_tom' +const MAJOR_TOM_EXTENSION_IDENTIFIER = MAJOR_TOM_IDENTIFIER const BLUEOS_CLOUD_URL = 'https://app.blueos.cloud' @@ -711,11 +713,6 @@ export default Vue.extend({ async cleanMajorTomFileToken(): Promise { await filebrowser.deleteFile(MAJOR_TOM_CLOUD_TOKEN_FILE) }, - async fetchMajorTomData(): Promise { - const data = await axios.get(MAJOR_TOM_CLOUD_URL) - - return data.data as InstalledExtensionData - }, async fetchMajorTomFileToken(): Promise { try { const response = await fetch(await filebrowser.singleFileRelativeURL(MAJOR_TOM_CLOUD_TOKEN_FILE)) @@ -808,17 +805,10 @@ export default Vue.extend({ ) try { - const majorTomData = await this.fetchMajorTomData() - this.pull_show_modal = true - await back_axios({ - method: 'POST', - url: `${KRAKEN_API_URL}/extension/install`, - data: majorTomData, - onDownloadProgress: (progressEvent) => { - this.pull_tracker?.digestNewData(progressEvent.event) - }, + await installOrUpdateMajorTom((progressEvent) => { + this.pull_tracker?.digestNewData(progressEvent.event) }) } catch (error) { this.setOperationError('Failed to install Major Tom.', error) diff --git a/core/frontend/src/components/utils/PullProgress.vue b/core/frontend/src/components/utils/PullProgress.vue index 001a8d4df7..72ca4f126b 100755 --- a/core/frontend/src/components/utils/PullProgress.vue +++ b/core/frontend/src/components/utils/PullProgress.vue @@ -12,6 +12,12 @@ +

+ {{ step }} +

+ { + this.major_tom_status_text = 'Updating Major Tom...' + return new Promise((resolve, reject) => { + const tracker = new PullTracker( + () => { resolve() }, + (error) => { reject(new Error(error)) }, + ) + + installOrUpdateMajorTom((progressEvent) => { + if (!progressEvent.event?.currentTarget?.response) { + return + } + tracker.digestNewData(progressEvent.event) + this.major_tom_pull_output = tracker.pull_output + this.major_tom_download_percentage = tracker.download_percentage + this.major_tom_extraction_percentage = tracker.extraction_percentage + this.major_tom_status_text = tracker.overall_status || 'Updating Major Tom...' + }) + .then(() => { resolve() }) + .catch(reject) + }) + }, + async updateMajorTomIfNeeded(): Promise { + let step = '' + try { + const action = await resolveMajorTomAction(this.has_internet) + if (action === 'skip') { + return + } + step = action === 'install' ? 'Installing Major Tom' : 'Updating Major Tom' + } catch (error) { + console.warn('Failed to check for Major Tom updates:', error) + return + } + + this.major_tom_step = step + this.major_tom_pull_output = '' + this.major_tom_download_percentage = 0 + this.major_tom_extraction_percentage = 0 + this.show_major_tom_progress = true + try { + await this.runMajorTomInstallOrUpdate() + } catch (error) { + notifier.pushError( + 'MAJOR_TOM_UPDATE_FAIL', + `Failed to update Major Tom: ${error}`, + true, + ) + } finally { + this.show_major_tom_progress = false + } + }, + async applyVersion(fullname: string): Promise { + const [repository, tag] = fullname.split(':') + await back_axios({ + method: 'post', + url: '/version-chooser/v1.0/version/current', + data: { + repository, + tag, + }, + }).finally(() => { this.waitForBackendToRestart(true) }) + }, backendIsOnline() { return new Promise((resolve) => { back_axios({ @@ -533,13 +616,15 @@ export default Vue.extend({ }, async pullAndSetVersion(args: string | string[]) { const fullname: string = Array.isArray(args) ? args[0] : args + await this.updateMajorTomIfNeeded() // This streams the output of docker pull + this.pull_step = 'Updating BlueOS' this.pull_output = 'Fetching remote image...' this.show_pull_output = true await this.pullVersion(fullname) .then(() => { this.show_pull_output = false - this.setVersion(fullname) + this.applyVersion(fullname) }) .catch((error) => { this.show_pull_output = false @@ -596,6 +681,7 @@ export default Vue.extend({ async updateBootstrap(image: string) { const [_, tag] = image.split(':') this.updating_bootstrap = true + this.pull_step = 'Updating Bootstrap' await this.pullVersion(image) .then(() => { this.show_pull_output = false @@ -638,15 +724,8 @@ export default Vue.extend({ }, async setVersion(args: string | string[]) { const fullname: string = Array.isArray(args) ? args[0] : args - const [repository, tag] = fullname.split(':') - await back_axios({ - method: 'post', - url: '/version-chooser/v1.0/version/current', - data: { - repository, - tag, - }, - }).finally(() => { this.waitForBackendToRestart(true) }) + await this.updateMajorTomIfNeeded() + await this.applyVersion(fullname) }, async deleteVersion(args: string | string[]) { const fullname: string = Array.isArray(args) ? args[0] : args diff --git a/core/frontend/src/utils/major_tom.ts b/core/frontend/src/utils/major_tom.ts new file mode 100644 index 0000000000..e5f3349006 --- /dev/null +++ b/core/frontend/src/utils/major_tom.ts @@ -0,0 +1,65 @@ +import axios from 'axios' + +import { fetchInstalledExtensions } from '@/components/kraken/KrakenManager' +import { InstalledExtensionData } from '@/types/kraken' +import back_axios from '@/utils/api' + +export const MAJOR_TOM_IDENTIFIER = 'blueos.major_tom' +export const MAJOR_TOM_INSTALL_URL = 'https://blueos.cloud/major_tom/install/' + +const KRAKEN_API_V2_URL = '/kraken/v2.0' + +export type MajorTomAction = 'skip' | 'install' | 'update' + +export async function fetchMajorTomInstallData(): Promise { + const response = await axios.get(MAJOR_TOM_INSTALL_URL) + return response.data as InstalledExtensionData +} + +export async function getInstalledMajorTom(): Promise { + const extensions = await fetchInstalledExtensions() + return extensions.find((extension) => extension.identifier === MAJOR_TOM_IDENTIFIER) +} + +export async function resolveMajorTomAction(hasInternet: boolean): Promise { + if (!hasInternet) { + return 'skip' + } + + const [installed, latest] = await Promise.all([ + getInstalledMajorTom(), + fetchMajorTomInstallData(), + ]) + + if (!installed) { + return 'install' + } + + if (installed.tag !== latest.tag) { + return 'update' + } + + return 'skip' +} + +export async function installOrUpdateMajorTom( + progressHandler: (event: { event: { currentTarget: { response: string } } }) => void, +): Promise { + const majorTomData = await fetchMajorTomInstallData() + + await back_axios({ + method: 'POST', + url: `${KRAKEN_API_V2_URL}/extension/`, + data: { + identifier: majorTomData.identifier, + name: majorTomData.name, + docker: majorTomData.docker, + tag: majorTomData.tag, + enabled: true, + permissions: majorTomData?.permissions ?? '', + user_permissions: majorTomData?.user_permissions ?? '', + }, + timeout: 600000, + onDownloadProgress: progressHandler, + }) +} diff --git a/core/services/kraken/config.py b/core/services/kraken/config.py index 442e389e89..42df6bb3dc 100644 --- a/core/services/kraken/config.py +++ b/core/services/kraken/config.py @@ -1,5 +1,7 @@ # This file is used to define general configurations for the app +from typing import Dict, List + SERVICE_NAME = "kraken" DEFAULT_MANIFESTS = [ @@ -10,12 +12,7 @@ }, ] -DEFAULT_EXTENSIONS = [ - { - "identifier": "blueos.major_tom", - "url": "https://blueos.cloud/major_tom/install", - }, -] +DEFAULT_EXTENSIONS: List[Dict[str, str]] = [] DEFAULT_INJECTED_ENV_VARIABLES = [ "MAV_SYSTEM_ID",