Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions core/frontend/src/components/kraken/KrakenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export async function setManifestSourceOrder(identifier: string, order: number):
export async function installExtension(
extension: InstalledExtensionData,
progressHandler: (event: any) => void,
signal?: AbortSignal,
): Promise<void> {
await back_axios({
url: `${KRAKEN_API_V2_URL}/extension/install`,
Expand All @@ -209,6 +210,7 @@ export async function installExtension(
},
timeout: 600000,
onDownloadProgress: progressHandler,
signal,
})
}

Expand Down Expand Up @@ -248,6 +250,18 @@ export async function uninstallExtension(identifier: string): Promise<void> {
})
}

/**
* Uninstall a specific version of an extension by its identifier and tag, uses API v2
* @param {string} identifier The identifier of the extension
* @param {string} tag The tag of the version to uninstall
*/
export async function uninstallExtensionVersion(identifier: string, tag: string): Promise<void> {
await back_axios({
method: 'DELETE',
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/${tag}`,
})
}

/**
* Restart an extension by its identifier, uses API v2
* @param {string} identifier The identifier of the extension
Expand All @@ -270,12 +284,18 @@ export async function updateExtensionToVersion(
identifier: string,
version: string,
progressHandler: (event: any) => void,
signal?: AbortSignal,
): Promise<void> {
await back_axios({
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/${version}`,
params: {
purge: false,
should_enable: false,
},
method: 'PUT',
timeout: 120000,
onDownloadProgress: progressHandler,
signal,
})
}

Expand Down Expand Up @@ -387,6 +407,7 @@ export async function finalizeExtension(
extension: InstalledExtensionData,
tempTag: string,
progressHandler: (event: any) => void,
signal?: AbortSignal,
): Promise<void> {
await back_axios({
method: 'POST',
Expand All @@ -402,6 +423,7 @@ export async function finalizeExtension(
},
timeout: 120000,
onDownloadProgress: progressHandler,
signal,
})
}

Expand All @@ -423,6 +445,7 @@ export default {
enableExtension,
disableExtension,
uninstallExtension,
uninstallExtensionVersion,
restartExtension,
listContainers,
getContainersStats,
Expand Down
9 changes: 9 additions & 0 deletions core/frontend/src/components/utils/PullProgress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
<v-card-actions v-if="cancelable" class="justify-end">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<v-card-actions v-if="cancelable" class="justify-end">
<v-card-actions v-if="cancelable" class="justify-center pa-2">

Maybe using a justify-start or using the same pattern from other popups that is the suggestion above followed by the <v-spacer /> on the suggestion bellow so that the button is at the left.
This is not a requirement, we just keep most of cancel buttons at the left, but they are usually together with an apply button, so if you think that will be more pleasant keep it at the right is ok.

<v-btn text color="primary" @click="$emit('cancel')">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<v-btn text color="primary" @click="$emit('cancel')">
<v-btn color="primary" @click="$emit('cancel')">

I would try to keep the style similar to other popups if possible

Cancel
</v-btn>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
</v-btn>
</v-btn>
<v-spacer />

This is a complement to the <v-card-actions v-if="cancelable" class="justify-center pa-2"> so that the button will be at the left side of the container

</v-card-actions>
</v-card>
</v-dialog>
</template>
Expand Down Expand Up @@ -80,6 +85,10 @@ export default Vue.extend({
type: String,
required: true,
},
cancelable: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand Down
111 changes: 84 additions & 27 deletions core/frontend/src/views/ExtensionManagerView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
:download="download_percentage"
:extraction="extraction_percentage"
:statustext="status_text"
:cancelable="!!active_abort_controller"
@cancel="cancelInstallOperation"
/>
<v-dialog
v-model="show_dialog"
Expand Down Expand Up @@ -538,6 +540,7 @@ export default Vue.extend({
install_from_file_last_level: -1,
active_operation_identifier: localStorage.getItem(ACTIVE_OPERATION_KEY) as null | string,
active_operation_type: (localStorage.getItem(ACTIVE_OPERATION_TYPE_KEY) ?? null) as null | 'install' | 'update',
active_abort_controller: null as null | AbortController,
}
},
computed: {
Expand Down Expand Up @@ -666,11 +669,32 @@ export default Vue.extend({
destroyed() {
clearInterval(this.metrics_interval)
this.stopUploadKeepAlive()
this.active_abort_controller?.abort()
this.active_abort_controller = null
},
methods: {
clearEditedExtension() {
this.edited_extension = null
},
beginInstallOperation(): AbortController {
this.active_abort_controller?.abort()
const controller = new AbortController()
this.active_abort_controller = controller
return controller
},
cancelInstallOperation(): void {
this.active_abort_controller?.abort()
},
showAlertError(error: unknown): void {
this.alerter = true
this.alerter_error = String(error)
},
finishInstallOperation(): void {
this.active_abort_controller = null
this.clearInstallingState()
this.resetPullOutput()
this.fetchInstalledExtensions()
},
setInstallFromFilePhase(phase: TarInstallPhase) {
this.install_from_file_phase = phase
if (phase !== 'error') {
Expand Down Expand Up @@ -824,27 +848,44 @@ export default Vue.extend({
this.file_uploading = false
}
},
swapExtensionVersion(identifier: string, enableTag: string, removeTag: string): Promise<void> {
return kraken.enableExtension(identifier, enableTag)
.then(() => kraken.uninstallExtensionVersion(identifier, removeTag))
.catch((error) => notifier.pushBackError('EXTENSION_VERSION_SWAP_FAIL', error))
},
async update(extension: InstalledExtensionData, version: string) {
const installedExt = this.installed_extensions[extension.identifier]
const previousTag = installedExt?.tag ?? extension.tag
this.setInstallingState(extension.identifier, 'update')
this.show_pull_output = true
const tracker = this.getTracker()
const controller = this.beginInstallOperation()
const tracker = this.getTracker(controller.signal)
kraken.updateExtensionToVersion(
extension.identifier,
version,
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
controller.signal,
)
.then(() => {
this.fetchInstalledExtensions()
.then(async () => {
notifier.pushSuccess('EXTENSION_UPDATE_SUCCESS', `${extension.name} updated successfully.`, true)
if (previousTag !== version) {
await this.swapExtensionVersion(extension.identifier, version, previousTag)
}
})
.catch((error) => {
this.alerter = true
this.alerter_error = String(error)
.catch(async (error) => {
if (axios.isCancel(error)) {
if (controller !== this.active_abort_controller) return
if (previousTag !== version) {
await this.swapExtensionVersion(extension.identifier, previousTag, version)
}
notifier.pushInfo('EXTENSION_UPDATE_CANCELLED', 'Extension update was cancelled.', true)
return
}
this.showAlertError(error)
notifier.pushBackError('EXTENSION_UPDATE_FAIL', error)
})
.finally(() => {
this.clearInstallingState()
this.resetPullOutput()
if (controller === this.active_abort_controller) this.finishInstallOperation()
})
},
metricsFor(extension: InstalledExtensionData): { cpu: number, memory: number} | Record<string, never> {
Expand Down Expand Up @@ -993,24 +1034,30 @@ export default Vue.extend({
this.setInstallingState(extension.identifier, 'install')
this.show_dialog = false
this.show_pull_output = true
const tracker = this.getTracker()
const controller = this.beginInstallOperation()
const tracker = this.getTracker(controller.signal)

kraken.installExtension(
extension,
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
controller.signal,
)
.then(() => {
this.fetchInstalledExtensions()
notifier.pushSuccess('EXTENSION_INSTALL_SUCCESS', `${extension.name} installed successfully.`, true)
})
.catch((error) => {
this.alerter = true
this.alerter_error = String(error)
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
.catch(async (error) => {
if (axios.isCancel(error)) {
if (controller !== this.active_abort_controller) return
await kraken.uninstallExtension(extension.identifier)
.catch((err) => notifier.pushBackError('EXTENSION_UNINSTALL_FAIL', err))
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Extension install was cancelled.', true)
return
}
this.showAlertError(error)
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
})
.finally(() => {
this.clearInstallingState()
this.resetPullOutput()
if (controller === this.active_abort_controller) this.finishInstallOperation()
})
},
async performActionFromModal(
Expand Down Expand Up @@ -1119,17 +1166,17 @@ export default Vue.extend({
temp[extension.identifier].loading = loading
this.installed_extensions = temp
},
getTracker(): PullTracker {
getTracker(signal: AbortSignal): PullTracker {
return new PullTracker(
() => {
setTimeout(() => {
this.show_pull_output = false
}, 1000)
},
(error) => {
this.alerter = true
this.alerter_error = String(error)
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
if (signal.aborted) return
this.showAlertError(error)
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
this.show_pull_output = false
},
)
Expand Down Expand Up @@ -1206,7 +1253,8 @@ export default Vue.extend({
}

this.show_pull_output = true
const tracker = this.getTracker()
const controller = this.beginInstallOperation()
const tracker = this.getTracker(controller.signal)
this.setInstallFromFilePhase('installing')
this.install_from_file_install_progress = 0
this.install_from_file_status_text = 'Starting installation...'
Expand All @@ -1216,20 +1264,29 @@ export default Vue.extend({
extension,
this.upload_temp_tag,
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
controller.signal,
)
this.setInstallFromFilePhase('success')
this.install_from_file_status_text = 'Extension installed successfully'
this.stopUploadKeepAlive()
this.upload_temp_tag = null
this.upload_metadata = null
this.fetchInstalledExtensions()
} catch (error) {
this.applyInstallFromFileError(String(error))
this.alerter = true
this.alerter_error = String(error)
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
if (axios.isCancel(error)) {
if (controller === this.active_abort_controller) {
this.setInstallFromFilePhase('ready')
this.install_from_file_status_text = ''
await kraken.uninstallExtension(extension.identifier)
.catch((err) => notifier.pushBackError('EXTENSION_UNINSTALL_FAIL', err))
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Installation from file was cancelled.', true)
}
} else {
this.applyInstallFromFileError(String(error))
this.showAlertError(error)
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
}
} finally {
this.resetPullOutput()
if (controller === this.active_abort_controller) this.finishInstallOperation()
}
},
setInstallingState(identifier: string, action: 'install' | 'update'): void {
Expand Down
4 changes: 2 additions & 2 deletions core/services/kraken/api/v2/routers/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,13 @@ async def update_to_latest(identifier: str, purge: bool = True, stable: bool = T

@extension_router_v2.put("/{identifier}/{tag}", status_code=status.HTTP_200_OK)
@extension_to_http_exception
async def update_to_tag(identifier: str, tag: str, purge: bool = True) -> Response:
async def update_to_tag(identifier: str, tag: str, purge: bool = True, should_enable: bool = True) -> Response:
"""
Update a given extension by its identifier and tag to latest version on the higher priority manifest and by default
purge all other tags, if purge is set to false it will keep all other versions disabled only.
"""
extension = cast(Extension, await Extension.from_manifest(identifier, tag))
return StreamingResponse(streamer(extension.update(purge)))
return StreamingResponse(streamer(extension.update(purge, should_enable)))


@extension_router_v2.delete("/{identifier}", status_code=status.HTTP_202_ACCEPTED)
Expand Down
14 changes: 8 additions & 6 deletions core/services/kraken/extension/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,15 @@ async def _disable_running_extension(self) -> Optional["Extension"]:
except ExtensionNotRunning:
return None

def _create_extension_settings(self) -> ExtensionSettings:
def _create_extension_settings(self, should_enable: bool = True) -> ExtensionSettings:
"""Create and save extension settings."""
new_extension = ExtensionSettings(
identifier=self.identifier,
name=self.source.name,
docker=self.source.docker,
tag=self.tag,
permissions=self.source.permissions,
enabled=True,
enabled=should_enable,
user_permissions=self.source.user_permissions,
)
# Save in settings first, if the image fails to install it will try to fetch after in main kraken check loop
Expand Down Expand Up @@ -223,13 +223,15 @@ async def _clear_remaining_tags(self) -> None:
to_clear = [version for version in to_clear if version.source.tag != self.tag]
await asyncio.gather(*(version.uninstall() for version in to_clear))

async def install(self, clear_remaining_tags: bool = True, atomic: bool = False) -> AsyncGenerator[bytes, None]:
async def install(
self, clear_remaining_tags: bool = True, atomic: bool = False, should_enable: bool = True
) -> AsyncGenerator[bytes, None]:
logger.info(f"Installing extension {self.identifier}:{self.tag}")

# First we should make sure no other tag is running
running_ext = await self._disable_running_extension()

self._create_extension_settings()
self._create_extension_settings(should_enable)
try:
self.lock(self.unique_entry)

Expand Down Expand Up @@ -262,8 +264,8 @@ async def install(self, clear_remaining_tags: bool = True, atomic: bool = False)
if clear_remaining_tags:
await self._clear_remaining_tags()

async def update(self, clear_remaining_tags: bool) -> AsyncGenerator[bytes, None]:
async for data in self.install(clear_remaining_tags):
async def update(self, clear_remaining_tags: bool, should_enable: bool = True) -> AsyncGenerator[bytes, None]:
async for data in self.install(clear_remaining_tags, should_enable=should_enable):
yield data

async def uninstall(self) -> None:
Expand Down
Loading