From c44204a49b2baa731dd675625d97b6b4a8d05777 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Thu, 10 Sep 2020 10:19:53 +0100 Subject: [PATCH] Add support for Helm Upgrade and history (#458) * Improve presentation and fix issue with development versions * Add support for Helm Upgrade and Helm history * Remove console logging * Add comment * Minor tweaks following self-review * Remove debug logging * Fix whitespace * Minor tidy ups * Fix compile issue * Fix front-end unit tests * Always show upgrade button * Minor entity store type updates * Address PR feedback * Revert * Remove description when checking for similar charts * Only show button when the helm chart is available Co-authored-by: Richard Cox --- .../packages/core/sass/mat-desktop.scss | 4 + .../components/list/list.component.html | 2 +- .../components/list/list.component.types.ts | 15 +- .../components/stepper/step/step.component.ts | 2 +- .../stepper/steppers/steppers.component.ts | 2 +- .../suse-extensions/sass/_all-theme.scss | 2 + .../src/custom/helm/helm-entity-catalog.ts | 7 +- .../src/custom/helm/helm-entity-factory.ts | 7 + .../src/custom/helm/helm-entity-generator.ts | 22 ++- .../custom/helm/store/helm.action-builders.ts | 12 +- .../src/custom/helm/store/helm.actions.ts | 26 ++++ .../src/custom/helm/store/helm.effects.ts | 28 ++++ .../src/custom/helm/store/helm.types.ts | 44 +++--- .../helm-release-tab-base.component.ts | 1 + .../tabs/helm-release-helper.service.ts | 130 ++++++++++++++++- .../helm-release-history-tab.component.html | 1 + .../helm-release-history-tab.component.scss | 0 ...helm-release-history-tab.component.spec.ts | 31 ++++ .../helm-release-history-tab.component.ts | 92 ++++++++++++ .../helm-release-summary-tab.component.html | 63 ++++---- .../helm-release-summary-tab.component.scss | 7 + ...helm-release-summary-tab.component.spec.ts | 5 +- ...m-release-summary-tab.component.theme.scss | 15 ++ .../helm-release-summary-tab.component.ts | 11 +- .../store/workload-action-builders.ts | 50 +++++-- .../store/workloads-entity-factory.ts | 9 ++ .../store/workloads-entity-generator.ts | 27 +++- .../workloads/store/workloads.actions.ts | 58 +++++++- .../workloads/store/workloads.effects.ts | 87 ++++++++++- .../release-version-data-source.ts | 61 ++++++++ .../release-version-list-config.ts | 131 +++++++++++++++++ .../upgrade-release.component.html | 37 +++++ .../upgrade-release.component.scss | 59 ++++++++ .../upgrade-release.component.spec.ts | 37 +++++ .../upgrade-release.component.ts | 114 +++++++++++++++ .../kubernetes/workloads/workload.types.ts | 17 +++ .../workloads/workloads-entity-catalog.ts | 14 +- .../kubernetes/workloads/workloads.module.ts | 10 +- .../kubernetes/workloads/workloads.routing.ts | 12 +- .../workloads/workloads.testing.module.ts | 47 ++++++ .../plugins/kubernetes/install_release.go | 135 ++++++++++++++---- src/jetstream/plugins/kubernetes/main.go | 2 + 42 files changed, 1318 insertions(+), 118 deletions(-) create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.html create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.scss create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.spec.ts create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.ts create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.theme.scss create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/release-version-data-source.ts create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/release-version-list-config.ts create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.html create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.scss create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.spec.ts create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.ts create mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.testing.module.ts diff --git a/src/frontend/packages/core/sass/mat-desktop.scss b/src/frontend/packages/core/sass/mat-desktop.scss index cf474347e0..7e11af970e 100644 --- a/src/frontend/packages/core/sass/mat-desktop.scss +++ b/src/frontend/packages/core/sass/mat-desktop.scss @@ -63,6 +63,10 @@ $desktop-toggle-button-item-height: $desktop-menu-item-height - 2px; font-size: $desktop-font-size; } + .mat-slide-toggle-label { + font-size: $desktop-font-size; + } + // Allow a slightly-wider snackbar on desktop .mat-snack-bar-container { max-width: 40vw; diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.html b/src/frontend/packages/core/src/shared/components/list/list.component.html index f8b7839b14..db67aaa1e0 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list.component.html @@ -50,7 +50,7 @@ - {{ multiFilterManager.allLabel }} + {{ multiFilterManager.allLabel }} {{selectItem.label}} diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.types.ts b/src/frontend/packages/core/src/shared/components/list/list.component.types.ts index de841ba208..6416a73dc3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.types.ts +++ b/src/frontend/packages/core/src/shared/components/list/list.component.types.ts @@ -1,7 +1,7 @@ import { Injectable, Type } from '@angular/core'; import * as moment from 'moment'; import { BehaviorSubject, combineLatest, Observable, of as observableOf } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; +import { first, map, startWith } from 'rxjs/operators'; import { ListView } from '../../../../../store/src/actions/list.actions'; import { ActionState } from '../../../../../store/src/reducers/api-request-reducer/types'; @@ -130,9 +130,11 @@ export interface IListMultiFilterConfig { key: string; label: string; allLabel?: string; + hideAllOption?: boolean; list$: Observable; loading$: Observable; select: BehaviorSubject; + autoSelectFirst?: boolean; } export interface IListFilter { @@ -208,6 +210,7 @@ export class MultiFilterManager { public filterKey: string; public allLabel: string; + public hideAllOption = false; constructor( public multiFilterConfig: IListMultiFilterConfig, @@ -215,10 +218,20 @@ export class MultiFilterManager { ) { this.filterKey = this.multiFilterConfig.key; this.allLabel = multiFilterConfig.allLabel || 'All'; + this.hideAllOption = multiFilterConfig.hideAllOption || false; this.filterItems$ = this.getItemObservable(multiFilterConfig); this.hasOneOrLessItems$ = this.filterItems$.pipe(map(items => items.length <= 1)); this.hasItems$ = this.filterItems$.pipe(map(items => !!items.length)); this.filterIsReady$ = this.getReadyObservable(multiFilterConfig, dataSource, this.hasItems$); + + // Also select the first option if configured + if (multiFilterConfig.autoSelectFirst) { + this.filterItems$.pipe(first()).subscribe(options => { + if (options && options.length > 0) { + this.selectItem(options[0].value); + } + }) + } } private getReadyObservable( diff --git a/src/frontend/packages/core/src/shared/components/stepper/step/step.component.ts b/src/frontend/packages/core/src/shared/components/stepper/step/step.component.ts index 73f164301a..9ab65bcd51 100644 --- a/src/frontend/packages/core/src/shared/components/stepper/step/step.component.ts +++ b/src/frontend/packages/core/src/shared/components/stepper/step/step.component.ts @@ -21,7 +21,7 @@ export interface StepOnNextResult { data?: any; } -export type StepOnNextFunction = () => Observable; +export type StepOnNextFunction = (index: number, step: StepComponent) => Observable; @Component({ selector: 'app-step', diff --git a/src/frontend/packages/core/src/shared/components/stepper/steppers/steppers.component.ts b/src/frontend/packages/core/src/shared/components/stepper/steppers/steppers.component.ts index 1e0776289c..a79b0fcc7a 100644 --- a/src/frontend/packages/core/src/shared/components/stepper/steppers/steppers.component.ts +++ b/src/frontend/packages/core/src/shared/components/stepper/steppers/steppers.component.ts @@ -117,7 +117,7 @@ export class SteppersComponent implements OnInit, AfterContentInit, OnDestroy { if (this.currentIndex < this.steps.length) { const step = this.steps[this.currentIndex]; step.busy = true; - const obs$ = step.onNext(); + const obs$ = step.onNext(this.currentIndex, step); if (!(obs$ instanceof Observable)) { return; } diff --git a/src/frontend/packages/suse-extensions/sass/_all-theme.scss b/src/frontend/packages/suse-extensions/sass/_all-theme.scss index 5afbb47729..116316ed35 100644 --- a/src/frontend/packages/suse-extensions/sass/_all-theme.scss +++ b/src/frontend/packages/suse-extensions/sass/_all-theme.scss @@ -4,6 +4,7 @@ @import '../src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme'; @import '../src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.theme'; @import '../src/custom/helm/list-types/monocular-chart-card/monocular-chart-card.component.theme'; +@import '../src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.theme'; @import '../src/custom/kubernetes/list-types/kubernetes-nodes/kubernetes-node-link/kubernetes-node-link.component.theme'; @mixin apply-theme-suse-extensions($stratos-theme) { @@ -15,6 +16,7 @@ @include kube-analysis-report-theme($theme, $app-theme); @include kube-analysis-card-theme($theme, $app-theme); @include monocular-chart-card($theme, $app-theme); + @include helm-release-summary-tab-theme($theme, $app-theme); @include kube-node-link-theme($theme, $app-theme); } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-catalog.ts b/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-catalog.ts index 288b527982..83dda2cd49 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-catalog.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-catalog.ts @@ -1,4 +1,4 @@ -import { StratosCatalogEndpointEntity, StratosCatalogEntity } from '../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { IFavoriteMetadata } from '../../../../store/src/types/user-favorites.types'; import { MonocularChart, HelmVersion } from './store/helm.types'; import { HelmChartActionBuilders, HelmVersionActionBuilders } from './store/helm.action-builders'; +import { StratosCatalogEndpointEntity, StratosCatalogEntity } from '../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { IFavoriteMetadata } from '../../../../store/src/types/user-favorites.types'; import { MonocularChart, HelmVersion, MonocularVersion } from './store/helm.types'; import { HelmChartActionBuilders, HelmVersionActionBuilders, HelmChartVersionsActionBuilders } from './store/helm.action-builders'; /** * A strongly typed collection of Helm Catalog Entities. @@ -6,8 +6,9 @@ import { StratosCatalogEndpointEntity, StratosCatalogEntity } from '../../../../ */ export class HelmEntityCatalog { endpoint: StratosCatalogEndpointEntity; - chart: StratosCatalogEntity - version: StratosCatalogEntity + chart: StratosCatalogEntity; + version: StratosCatalogEntity; + chartVersions: StratosCatalogEntity; } /** diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-factory.ts b/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-factory.ts index 5bb57a9a65..302dd8a060 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-factory.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-factory.ts @@ -5,6 +5,7 @@ import { HelmVersion, MonocularChart } from './store/helm.types'; export const helmVersionsEntityType = 'helmVersions'; export const monocularChartsEntityType = 'monocularCharts'; +export const monocularChartVersionsEntityType = 'monocularChartVersions'; export const getMonocularChartId = (entity: MonocularChart) => entity.id; export const getHelmVersionId = (entity: HelmVersion) => entity.endpointId; @@ -45,6 +46,12 @@ entityCache[helmVersionsEntityType] = new HelmEntitySchema( { idAttribute: getHelmVersionId } ); +entityCache[monocularChartVersionsEntityType] = new HelmEntitySchema( + monocularChartVersionsEntityType, + {}, + { idAttribute: getHelmVersionId } +); + export function helmEntityFactory(key: string): EntitySchema { const entity = entityCache[key]; if (!entity) { diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-generator.ts b/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-generator.ts index 380b225a40..c9ef3d8afa 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-generator.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm-entity-generator.ts @@ -11,14 +11,17 @@ import { helmEntityFactory, helmVersionsEntityType, monocularChartsEntityType, + monocularChartVersionsEntityType, } from './helm-entity-factory'; import { HelmChartActionBuilders, helmChartActionBuilders, + HelmChartVersionsActionBuilders, + helmChartVersionsActionBuilders, HelmVersionActionBuilders, helmVersionActionBuilders, } from './store/helm.action-builders'; -import { HelmVersion, MonocularChart } from './store/helm.types'; +import { HelmVersion, MonocularChart, MonocularVersion } from './store/helm.types'; export function generateHelmEntities(): StratosBaseCatalogEntity[] { @@ -35,10 +38,12 @@ export function generateHelmEntities(): StratosBaseCatalogEntity[] { authTypes: [], renderPriority: 10, }; + return [ generateEndpointEntity(endpointDefinition), generateChartEntity(endpointDefinition), generateVersionEntity(endpointDefinition), + generateChartVersionsEntity(endpointDefinition), ]; } @@ -80,4 +85,19 @@ function generateVersionEntity(endpointDefinition: StratosEndpointExtensionDefin return helmEntityCatalog.version; } +function generateChartVersionsEntity(endpointDefinition: StratosEndpointExtensionDefinition) { + const definition = { + type: monocularChartVersionsEntityType, + schema: helmEntityFactory(monocularChartVersionsEntityType), + endpoint: endpointDefinition + }; + helmEntityCatalog.chartVersions = new StratosCatalogEntity( + definition, + { + actionBuilders: helmChartVersionsActionBuilders + } + ); + return helmEntityCatalog.chartVersions; +} + diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.action-builders.ts b/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.action-builders.ts index 53c20f4de9..df443d3ea6 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.action-builders.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.action-builders.ts @@ -1,5 +1,5 @@ import { OrchestratedActionBuilders } from '../../../../../store/src/entity-catalog/action-orchestrator/action-orchestrator'; -import { GetHelmVersions, GetMonocularCharts, HelmInstall } from './helm.actions'; +import { GetHelmChartVersions, GetHelmVersions, GetMonocularCharts, HelmInstall } from './helm.actions'; import { HelmInstallValues } from './helm.types'; export interface HelmChartActionBuilders extends OrchestratedActionBuilders { @@ -20,4 +20,12 @@ export interface HelmVersionActionBuilders extends OrchestratedActionBuilders { export const helmVersionActionBuilders: HelmVersionActionBuilders = { getMultiple: () => new GetHelmVersions() -} \ No newline at end of file +} + +export interface HelmChartVersionsActionBuilders extends OrchestratedActionBuilders { + getMultiple: (repoName: string, chartName: string) => GetHelmChartVersions +} + +export const helmChartVersionsActionBuilders: HelmChartVersionsActionBuilders = { + getMultiple: (repoName: string, chartName: string) => new GetHelmChartVersions(repoName, chartName) +} diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.actions.ts b/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.actions.ts index c47e8e47f3..df552269dd 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.actions.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.actions.ts @@ -5,6 +5,7 @@ import { helmEntityFactory, helmVersionsEntityType, monocularChartsEntityType, + monocularChartVersionsEntityType, } from '../helm-entity-factory'; import { HelmInstallValues } from './helm.types'; @@ -12,6 +13,10 @@ export const GET_MONOCULAR_CHARTS = '[Monocular] Get Charts'; export const GET_MONOCULAR_CHARTS_SUCCESS = '[Monocular] Get Charts Success'; export const GET_MONOCULAR_CHARTS_FAILURE = '[Monocular] Get Charts Failure'; +export const GET_MONOCULAR_CHART_VERSIONS = '[Monocular] Get Chart Versions'; +export const GET_MONOCULAR_CHART_VERSIONS_SUCCESS = '[Monocular] Get Chart Versions Success'; +export const GET_MONOCULAR_CHART_VERSIONS_FAILURE = '[Monocular] Get Chart Versions Failure'; + export const GET_HELM_VERSIONS = '[Helm] Get Versions'; export const GET_HELM_VERSIONS_SUCCESS = '[Helm] Get Versions Success'; export const GET_HELM_VERSIONS_FAILURE = '[Helm] Get Versions Failure'; @@ -73,3 +78,24 @@ export class GetHelmVersions implements MonocularPaginationAction { }; flattenPagination = true; } + +export class GetHelmChartVersions implements MonocularPaginationAction { + constructor(public repoName: string, public chartName: string) { + this.paginationKey = `'monocular-chart-versions-${repoName}-${chartName}`; + } + type = GET_MONOCULAR_CHART_VERSIONS; + endpointType = HELM_ENDPOINT_TYPE; + entityType = monocularChartVersionsEntityType; + entity = [helmEntityFactory(monocularChartVersionsEntityType)]; + actions = [ + GET_MONOCULAR_CHART_VERSIONS, + GET_MONOCULAR_CHART_VERSIONS_SUCCESS, + GET_MONOCULAR_CHART_VERSIONS_FAILURE + ]; + paginationKey: string; + initialParams = { + 'order-direction': 'asc', + 'order-direction-field': 'version', + }; + flattenPagination = true; +} diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.effects.ts b/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.effects.ts index 65a3d1b9b8..31508d886f 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.effects.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.effects.ts @@ -23,7 +23,9 @@ import { helmEntityCatalog } from '../helm-entity-catalog'; import { getHelmVersionId, getMonocularChartId, HELM_ENDPOINT_TYPE } from '../helm-entity-factory'; import { GET_HELM_VERSIONS, + GET_MONOCULAR_CHART_VERSIONS, GET_MONOCULAR_CHARTS, + GetHelmChartVersions, GetHelmVersions, GetMonocularCharts, HELM_INSTALL, @@ -128,6 +130,32 @@ export class HelmEffects { }) ); + @Effect() + fetchChartVersions$ = this.actions$.pipe( + ofType(GET_MONOCULAR_CHART_VERSIONS), + flatMap(action => { + const entityKey = entityCatalog.getEntityKey(action); + return this.makeRequest(action, `/pp/${this.proxyAPIVersion}/chartsvc/v1/charts/${action.repoName}/${action.chartName}/versions`, + (response) => { + const base = { + entities: { [entityKey]: {} }, + result: [] + } as NormalizedResponse; + + const items = response.data as Array; + const processedData = items.reduce((res, data) => { + const id = getMonocularChartId(data); + res.entities[entityKey][id] = data; + // Promote the name to the top-level object for simplicity + data.name = data.attributes.name; + res.result.push(id); + return res; + }, base); + return processedData; + }, []); + }) + ); + @Effect() helmInstall$ = this.actions$.pipe( ofType(HELM_INSTALL), diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.types.ts b/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.types.ts index 54b21b657c..4ddf46021c 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.types.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/store/helm.types.ts @@ -1,3 +1,6 @@ +import { Chart } from './../monocular/shared/models/chart'; +import { ChartVersion } from './../monocular/shared/models/chart-version'; + export interface MonocularRepository { name: string; url: string; @@ -7,26 +10,18 @@ export interface MonocularRepository { status: string; } -export interface MonocularChart { - id: string; +// Reuse types from the Monocular codebase +export interface MonocularChart extends Chart { name: string; - attributes: { - description: string; - home: string; - icon: string; - keywords: string[]; - repo: { - name: string; - url: string; - }; - }; - relationships: { - latestChartVersion: { - data: { - version: string - } - } - }; +} + +export type MonocularVersion = ChartVersion; + +// Basic Chart Metadata +export interface ChartMetadata { + name: string; + description: string; + sources: string[]; } export interface HelmVersion { @@ -50,8 +45,6 @@ export enum HelmStatus { Pending_Rollback = 8 } - - export interface HelmInstallValues { endpoint: string; releaseName: string; @@ -59,3 +52,12 @@ export interface HelmInstallValues { values: string; chart: string; } + +export interface HelmUpgradeValues { + chart: { + name: string; + repo: string; + version: string; + }; + restartPods?: boolean; +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts index 8bad3751c8..d121deed5d 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts @@ -58,6 +58,7 @@ export class HelmReleaseTabBaseComponent implements OnDestroy { { link: 'summary', label: 'Summary', icon: 'helm', iconFont: 'stratos-icons' }, { link: 'notes', label: 'Notes', icon: 'subject' }, { link: 'values', label: 'Values', icon: 'list' }, + { link: 'history', label: 'History', icon: 'schedule' }, { link: 'analysis', label: 'Analysis', icon: 'assignment', hidden$: this.analysisService.hideAnalysis$ }, { link: '-', label: 'Resources' }, { link: 'graph', label: 'Overview', icon: 'share', hidden$: sessionService.isTechPreview().pipe(map(tp => !tp)) }, diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-helper.service.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-helper.service.ts index fae00ae737..749d08e560 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-helper.service.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-helper.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { combineLatest, Observable } from 'rxjs'; import { filter, map } from 'rxjs/operators'; +import { helmEntityCatalog } from '../../../../helm/helm-entity-catalog'; +import { ChartMetadata } from '../../../../helm/store/helm.types'; import { kubeEntityCatalog } from '../../../kubernetes-entity-catalog'; import { ContainerStateCollection, KubernetesPod } from '../../../store/kube.types'; import { getHelmReleaseDetailsFromGuid } from '../../store/workloads-entity-factory'; @@ -14,6 +16,49 @@ import { } from '../../workload.types'; import { workloadsEntityCatalog } from '../../workloads-entity-catalog'; +// Simple class to represent MAJOR.MINOR.REVISION version +class Version { + + public major: number; + public minor: number; + public revision: number; + + public valid: boolean; + + constructor(v: string) { + this.valid = false; + if (typeof v === 'string') { + const parts = v.split('.'); + if (parts.length === 3) { + this.major = parseInt(parts[0], 10); + this.minor = parseInt(parts[1], 10); + this.revision = parseInt(parts[2], 10); + this.valid = true; + } + } + } + + // Is this version newer than the supplied other version? + public isNewer(other: Version): boolean { + if (!this.valid || !other.valid) { + return false; + } + + if (this.major > other.major) { + return true; + } + + if (this.major === other.major) { + if (this.minor > other.minor) { + return true; + } + if (this.minor === other.minor) { + return this.revision > other.revision; + } + } + return false; + } +} @Injectable() export class HelmReleaseHelperService { @@ -71,10 +116,8 @@ export class HelmReleaseHelperService { public fetchReleaseResources(): Observable { // Get helm release - const action = workloadsEntityCatalog.resource.actions.get(this.releaseTitle, this.endpointGuid); - return workloadsEntityCatalog.resource.store.getEntityMonitor( - action.guid - ).entity$.pipe( + const guid = workloadsEntityCatalog.resource.actions.get(this.releaseTitle, this.endpointGuid).guid; + return workloadsEntityCatalog.resource.store.getEntityMonitor(guid).entity$.pipe( filter(resources => !!resources) ); } @@ -89,6 +132,24 @@ export class HelmReleaseHelperService { ); } + // Check to see if a workload has updates available + public getCharts() { + return helmEntityCatalog.chart.store.getPaginationService().entities$.pipe( + filter(charts => !!charts) + ); + } + + public fetchReleaseHistory(): Observable { + // Get the history for a Helm release + return workloadsEntityCatalog.history.store.getEntityService( + this.releaseTitle, + this.endpointGuid, + { namespace: this.namespace } + ).waitForEntity$.pipe( + map(historyEntity => historyEntity.entity.revisions) + ); + } + private mapPods(pods: KubernetesPod[]): HelmReleaseChartData { const podPhases: { [phase: string]: number, } = {}; const containers = { @@ -144,4 +205,63 @@ export class HelmReleaseHelperService { } return false; } + + public hasUpgrade(returnLatest = false): Observable { + const updates = combineLatest(this.getCharts(), this.release$); + return updates.pipe( + map(([charts, release]) => { + for (const c of charts) { + if (this.isProbablySameChart(c.attributes, release.chart.metadata)) { + if (c.relationships && c.relationships.latestChartVersion && c.relationships.latestChartVersion.data) { + const latest = new Version(c.relationships.latestChartVersion.data.version); + const current = new Version(release.chart.metadata.version); + if (latest.isNewer(current)) { + return { + release, + upgrade: c.attributes, + version: c.relationships.latestChartVersion.data.version + }; + } + } + } + } + // No newer release, so return the release itself if that is what was requested and we can find the chart + // NOTE: If the helm repository is removed that we installed from, we won't be able to find the chart + if (returnLatest) { + const releaseChart = charts.find(c => c.relationships.latestChartVersion.data.version === release.chart.metadata.version); + if (releaseChart) { + return { + release, + upgrade: releaseChart.attributes, + version: releaseChart.relationships.latestChartVersion.data.version + } + } + } + return null; + }) + ); + } + + // We might have a chart with the same name in multiple repositories - we only have chart metadata + // We don't know which Helm repository it came from, so use the name and sources to match + private isProbablySameChart(a: ChartMetadata, b: ChartMetadata): boolean { + // Basic properties must be the same + if ((a.name !== b.name) || (a.sources.length !== b.sources.length)) { + return false; + } + + // Sources must match + let count = 0; + a.sources.forEach(source => { + count += b.sources.findIndex((s) => s === source) === -1 ? 0 : 1; + }); + + if (count !== a.sources.length) { + return false; + } + + return true; + } + } + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.html new file mode 100644 index 0000000000..7e055c154c --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.html @@ -0,0 +1 @@ + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.spec.ts new file mode 100644 index 0000000000..f2b19dceeb --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.spec.ts @@ -0,0 +1,31 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HelmReleaseGuidMock } from '../../../../../helm/helm-testing.module'; +import { HelmReleaseHelperService } from '../helm-release-helper.service'; +import { HelmReleaseHistoryTabComponent } from './helm-release-history-tab.component'; + +describe('HelmReleaseHistoryTabComponent', () => { + let component: HelmReleaseHistoryTabComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HelmReleaseHistoryTabComponent ], + providers: [ + HelmReleaseHelperService, + HelmReleaseGuidMock + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HelmReleaseHistoryTabComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.ts new file mode 100644 index 0000000000..df49f034bc --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-history-tab/helm-release-history-tab.component.ts @@ -0,0 +1,92 @@ +import { Component } from '@angular/core'; +import * as moment from 'moment'; +import { of } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; + +import { + ITableListDataSource, +} from '../../../../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source-types'; +import { ITableColumn } from '../../../../../../../../core/src/shared/components/list/list-table/table.types'; +import { HelmReleaseHelperService } from './../helm-release-helper.service'; + +@Component({ + selector: 'app-helm-release-history-tab', + templateUrl: './helm-release-history-tab.component.html', + styleUrls: ['./helm-release-history-tab.component.scss'] +}) +export class HelmReleaseHistoryTabComponent { + + public columns: ITableColumn[] = []; + + public dataSource: ITableListDataSource; + + constructor(public helmReleaseHelper: HelmReleaseHelperService) { + + // Use the ame column layout as the Helm CLI + this.columns = [ + { + columnId: 'revision', + headerCell: () => 'Revision', + cellFlex: '1', + cellDefinition: { + valuePath: 'revision' + } + }, + { + columnId: 'updated', + headerCell: () => 'Updated', + cellFlex: '3', + cellDefinition: { + getValue: row => moment(row.last_deployed).format('LLL') + } + }, + { + columnId: 'status', + headerCell: () => 'Status', + cellFlex: '2', + cellDefinition: { + valuePath: 'status' + } + }, + { + columnId: 'chart', + headerCell: () => 'Chart', + cellFlex: '2', + cellDefinition: { + getValue: row => `${row.chart.name}-${row.chart.version}` + } + }, + { + columnId: 'app_version', + headerCell: () => 'App Version', + cellFlex: '1', + cellDefinition: { + valuePath: 'chart.appVersion' + } + }, + { + columnId: 'description', + headerCell: () => 'Description', + cellFlex: '2', + cellDefinition: { + valuePath: 'description' + } + }, + ]; + + const data$ = this.helmReleaseHelper.fetchReleaseHistory(); + this.dataSource = { + connect: () => data$, + disconnect: () => { }, + trackBy: (index, item) => item.revision, + isTableLoading$: data$.pipe( + map(revisions => !revisions), + startWith(true), + ), + getRowState: (row) => { + return of({}); + } + }; + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.html index ad0b133c87..ade8a8bf0a 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.html +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.html @@ -4,6 +4,11 @@ Delete + + @@ -16,35 +21,41 @@ - +
+ Upgrade available: {{ upgrade }} +
+ -
-
- {{ release.chart.metadata.version }} - - - {{ release.chart.metadata.appVersion || '-' }} - - - {{ getClusterName() | async }} - - - {{ release.namespace }} - - {{ release.version }} - - {{ release.status | titlecase }} - - - {{ release.info.first_deployed | date:'medium' }} - - - {{ release.info.last_deployed | date:'medium' }} - +
+
+ {{ release.chart.metadata.version }} + + + {{ release.chart.metadata.appVersion || '-' }} + + + {{ getClusterName() | async }} + + + {{ release.namespace }} + + {{ release.version }} + + {{ release.status | titlecase }} + + + {{ release.info.first_deployed | date:'medium' }} + + + {{ release.info.last_deployed | date:'medium' }} + +
-
- + +
+
diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.scss index d57c1bb42f..b2f30b0f4a 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.scss +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.scss @@ -43,6 +43,13 @@ opacity: .6; } +.chart-upgrade { + border-radius: 8px; + float: right; + font-size: 12px; + margin-right: 10px; + padding: 2px 8px; +} .grid { $bottom-space: 20px; diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts index 4582dd922d..9eb1592e81 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts @@ -2,9 +2,10 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TabNavService } from 'frontend/packages/core/tab-nav.service'; import { SidePanelService } from '../../../../../../../../core/src/shared/services/side-panel.service'; -import { HelmReleaseProviders, KubeBaseGuidMock, KubernetesBaseTestModules } from '../../../../kubernetes.testing.module'; +import { HelmReleaseProviders, KubeBaseGuidMock } from '../../../../kubernetes.testing.module'; import { KubernetesEndpointService } from '../../../../services/kubernetes-endpoint.service'; import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service'; +import { WorkloadsBaseTestingModule } from '../../../workloads.testing.module'; import { AnalysisReportSelectorComponent, } from './../../../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component'; @@ -17,7 +18,7 @@ describe('HelmReleaseSummaryTabComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - ...KubernetesBaseTestModules + ...WorkloadsBaseTestingModule ], declarations: [HelmReleaseSummaryTabComponent, AnalysisReportSelectorComponent], providers: [ diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.theme.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.theme.scss new file mode 100644 index 0000000000..30c0894af2 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.theme.scss @@ -0,0 +1,15 @@ +@mixin helm-release-summary-tab-theme($theme, $app-theme) { + + $status-colors: map-get($app-theme, status); + $status-success: map-get($status-colors, success); + $status-warning: map-get($status-colors, warning); + $status-danger: map-get($status-colors, danger); + $status-tentative: map-get($status-colors, tentative); + $status-info: map-get($status-colors, info); + + .chart-upgrade { + border: 1px solid $status-success; + color: $status-success; + } + +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts index 3b3d8ce7c8..0a8fbac769 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts @@ -47,6 +47,11 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { public path: string; + public hasUpgrade$: Observable; + + // Can we upgrade? Yes as long as the Helm Chart can be found + public canUpgrade$: Observable; + public podChartColors = [ { name: 'Running', @@ -115,6 +120,10 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { ) ); + this.hasUpgrade$ = this.helmReleaseHelper.hasUpgrade().pipe(map(v => v ? v.version : null)); + + this.canUpgrade$ = this.helmReleaseHelper.hasUpgrade(true).pipe(map(v => !!v)); + this.resources$ = combineLatest( this.helmReleaseHelper.fetchReleaseGraph(), this.analysisReportUpdated$ @@ -217,7 +226,7 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { const action = workloadsEntityCatalog.release.actions.getMultiple(); this.store.dispatch(new ClearPaginationOfType(action)); this.completeDelete(); - this.store.dispatch(new RouterNav({ path: ['workloads'] })); + this.store.dispatch(new RouterNav({ path: ['./workloads'] })); } }); }); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workload-action-builders.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workload-action-builders.ts index eb7591f9c5..f85d09243e 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workload-action-builders.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workload-action-builders.ts @@ -1,42 +1,74 @@ import { OrchestratedActionBuilders, } from '../../../../../../store/src/entity-catalog/action-orchestrator/action-orchestrator'; -import { GetHelmRelease, GetHelmReleaseGraph, GetHelmReleaseResource, GetHelmReleases } from './workloads.actions'; +import { HelmUpgradeValues } from '../../../helm/store/helm.types'; +import { + GetHelmRelease, + GetHelmReleaseGraph, + GetHelmReleaseHistory, + GetHelmReleaseResource, + GetHelmReleases, + UpgradeHelmRelease, +} from './workloads.actions'; export interface WorkloadReleaseBuilders extends OrchestratedActionBuilders { get: ( title: string, endpointGuid: string, - extraArgs: { namespace: string } + extraArgs: { namespace: string, } ) => GetHelmRelease; getMultiple: () => GetHelmReleases; + upgrade: ( + title: string, + endpointGuid: string, + namespace: string, + values: HelmUpgradeValues) => UpgradeHelmRelease; } export const workloadReleaseBuilders: WorkloadReleaseBuilders = { - get: (title: string, endpointGuid: string, { namespace }: { namespace: string }) => { + get: (title: string, endpointGuid: string, { namespace }: { namespace: string, }) => { return new GetHelmRelease(endpointGuid, namespace, title); }, - getMultiple: () => new GetHelmReleases() -} + getMultiple: () => new GetHelmReleases(), + upgrade: ( + title: string, + endpointGuid: string, + namespace: string, + values: HelmUpgradeValues) => new UpgradeHelmRelease(title, endpointGuid, namespace, values) +}; export interface WorkloadGraphBuilders extends OrchestratedActionBuilders { get: ( releaseTitle: string, endpointGuid: string - ) => GetHelmReleaseGraph + ) => GetHelmReleaseGraph; } export const workloadGraphBuilders: WorkloadGraphBuilders = { get: (releaseTitle: string, endpointGuid: string) => new GetHelmReleaseGraph(endpointGuid, releaseTitle) -} +}; export interface WorkloadResourceBuilders extends OrchestratedActionBuilders { get: ( releaseTitle: string, endpointGuid: string, - ) => GetHelmReleaseResource + ) => GetHelmReleaseResource; } export const workloadResourceBuilders: WorkloadResourceBuilders = { get: (releaseTitle: string, endpointGuid: string) => new GetHelmReleaseResource(endpointGuid, releaseTitle) -} \ No newline at end of file +}; + +export interface WorkloadResourceHistoryBuilders extends OrchestratedActionBuilders { + get: ( + releaseTitle: string, + endpointGuid: string, + extraArgs: { namespace: string, } + ) => GetHelmReleaseHistory; +} + +export const workloadResourceHistoryBuilders: WorkloadResourceHistoryBuilders = { + get: (releaseTitle: string, endpointGuid: string, { namespace }: { namespace: string, }) => + new GetHelmReleaseHistory(endpointGuid, namespace, releaseTitle) +}; + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-factory.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-factory.ts index c1f3dd65bc..25ada89e41 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-factory.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-factory.ts @@ -7,6 +7,7 @@ export const helmReleasePodEntityType = 'helmReleasePod'; export const helmReleaseServiceEntityType = 'helmReleaseService'; export const helmReleaseGraphEntityType = 'helmReleaseGraph'; export const helmReleaseResourceEntityType = 'helmReleaseResource'; +export const helmReleaseHistoryEntityType = 'helmReleaseHistory'; const separator = ':'; export const getHelmReleaseDetailsFromGuid = (guid: string) => { @@ -48,8 +49,16 @@ entityCache[helmReleaseResourceEntityType] = new KubernetesEntitySchema( { idAttribute: getHelmReleaseResourceIdByObj } ); +entityCache[helmReleaseHistoryEntityType] = new KubernetesEntitySchema( + helmReleaseHistoryEntityType, + {}, + { idAttribute: getHelmReleaseResourceIdByObj } +); + Object.entries(entityCache).forEach(([key, workloadSchema]) => addKubernetesEntitySchema(key, workloadSchema)); + export const createHelmReleaseEntities = (): { [cacheName: string]: EntitySchema; } => { return entityCache; }; + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-generator.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-generator.ts index 3a8f8a0181..d1695d2322 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-generator.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-generator.ts @@ -7,6 +7,7 @@ import { IFavoriteMetadata } from '../../../../../../store/src/types/user-favori import { kubernetesEntityFactory } from '../../kubernetes-entity-factory'; import { HelmRelease, HelmReleaseGraph, HelmReleaseResources } from '../workload.types'; import { workloadsEntityCatalog } from '../workloads-entity-catalog'; +import { HelmReleaseHistory } from './../workload.types'; import { WorkloadGraphBuilders, workloadGraphBuilders, @@ -14,15 +15,24 @@ import { workloadReleaseBuilders, WorkloadResourceBuilders, workloadResourceBuilders, + WorkloadResourceHistoryBuilders, + workloadResourceHistoryBuilders, } from './workload-action-builders'; -import { helmReleaseEntityKey, helmReleaseGraphEntityType, helmReleaseResourceEntityType } from './workloads-entity-factory'; +import { + helmReleaseEntityKey, + helmReleaseGraphEntityType, + helmReleaseHistoryEntityType, + helmReleaseResourceEntityType, +} from './workloads-entity-factory'; export function generateWorkloadsEntities(endpointDefinition: StratosEndpointExtensionDefinition): StratosBaseCatalogEntity[] { + return [ generateReleaseEntity(endpointDefinition), generateReleaseGraphEntity(endpointDefinition), generateReleaseResourceEntity(endpointDefinition), + generateReleaseHistoryEntity(endpointDefinition) ]; } @@ -71,3 +81,18 @@ function generateReleaseResourceEntity(endpointDefinition: StratosEndpointExtens return workloadsEntityCatalog.resource; } +function generateReleaseHistoryEntity(endpointDefinition: StratosEndpointExtensionDefinition) { + const definition = { + type: helmReleaseHistoryEntityType, + schema: kubernetesEntityFactory(helmReleaseHistoryEntityType), + endpoint: endpointDefinition + }; + workloadsEntityCatalog.history = new StratosCatalogEntity( + definition, + { + actionBuilders: workloadResourceHistoryBuilders + } + ); + return workloadsEntityCatalog.history; +} + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads.actions.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads.actions.ts index b2448b591f..d80ba31beb 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads.actions.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads.actions.ts @@ -1,7 +1,7 @@ import { EntityRequestAction } from 'frontend/packages/store/src/types/request.types'; -import { PaginatedAction } from '../../../../../../store/src/types/pagination.types'; import { MonocularPaginationAction } from '../../../helm/store/helm.actions'; +import { HelmUpgradeValues } from '../../../helm/store/helm.types'; import { KUBERNETES_ENDPOINT_TYPE, kubernetesEntityFactory, @@ -15,6 +15,7 @@ import { getHelmReleaseResourceId, helmReleaseEntityKey, helmReleaseGraphEntityType, + helmReleaseHistoryEntityType, helmReleaseResourceEntityType, } from './workloads-entity-factory'; @@ -38,12 +39,16 @@ export const UPDATE_HELM_RELEASE = '[Helm] Update Release'; export const UPDATE_HELM_RELEASE_SUCCESS = '[Helm] Update Release Success'; export const UPDATE_HELM_RELEASE_FAILURE = '[Helm] Update Release Failure'; -interface HelmReleaseSingleEntity extends EntityRequestAction { - guid: string; -} +export const GET_HELM_RELEASE_HISTORY = '[Helm] Get Release History'; +export const GET_HELM_RELEASE_HISTORY_SUCCESS = '[Helm] Get Release History Success'; +export const GET_HELM_RELEASE_HISTORY_FAILURE = '[Helm] Get Release History Failure'; -interface HelmReleasePaginated extends PaginatedAction, EntityRequestAction { +export const UPGRADE_HELM_RELEASE = '[Helm] Upgrade Release'; +export const UPGRADE_HELM_RELEASE_SUCCESS = '[Helm] Upgrade Release Success'; +export const UPGRADE_HELM_RELEASE_FAILURE = '[Helm] Upgrade Release Failure'; +interface HelmReleaseSingleEntity extends EntityRequestAction { + guid: string; } export class GetHelmReleases implements MonocularPaginationAction { @@ -170,3 +175,46 @@ export class GetHelmReleaseServices implements KubePaginationAction { }; flattenPagination = true; } + +export class GetHelmReleaseHistory implements HelmReleaseSingleEntity { + constructor( + public endpointGuid: string, + public namespace: string, + public releaseTitle: string + ) { + this.guid = getHelmReleaseId(this.endpointGuid, this.namespace, this.releaseTitle); + } + type = GET_HELM_RELEASE_HISTORY; + endpointType = KUBERNETES_ENDPOINT_TYPE; + entity = kubernetesEntityFactory(helmReleaseHistoryEntityType); + entityType = helmReleaseHistoryEntityType; + actions = [ + GET_HELM_RELEASE_HISTORY, + GET_HELM_RELEASE_HISTORY_SUCCESS, + GET_HELM_RELEASE_HISTORY_FAILURE + ]; + + guid: string; +} + +export class UpgradeHelmRelease implements HelmReleaseSingleEntity { + guid: string; + type = UPGRADE_HELM_RELEASE; + endpointType = KUBERNETES_ENDPOINT_TYPE; + entity = kubernetesEntityFactory(helmReleaseEntityKey); + entityType = helmReleaseEntityKey; + constructor( + public releaseTitle: string, + public endpointGuid: string, + public namespace: string, + public values: HelmUpgradeValues + ) { + this.guid = getHelmReleaseId(this.endpointGuid, this.namespace, this.releaseTitle); + } + updatingKey = 'upgrading'; + actions = [ + UPGRADE_HELM_RELEASE, + UPGRADE_HELM_RELEASE_SUCCESS, + UPGRADE_HELM_RELEASE_FAILURE + ]; +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads.effects.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads.effects.ts index ea7add12fe..a572f14604 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads.effects.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads.effects.ts @@ -8,6 +8,7 @@ import { catchError, flatMap, mergeMap } from 'rxjs/operators'; import { AppState } from '../../../../../../store/src/app-state'; import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog'; +import { ApiRequestTypes } from '../../../../../../store/src/reducers/api-request-reducer/request-helpers'; import { NormalizedResponse } from '../../../../../../store/src/types/api.types'; import { EntityRequestAction, @@ -16,9 +17,19 @@ import { WrapperRequestActionSuccess, } from '../../../../../../store/src/types/request.types'; import { HelmEffects } from '../../../helm/store/helm.effects'; -import { HelmRelease } from '../workload.types'; +import { HelmRelease, HelmReleaseHistory } from '../workload.types'; +import { workloadsEntityCatalog } from './../workloads-entity-catalog'; import { getHelmReleaseId } from './workloads-entity-factory'; -import { GET_HELM_RELEASE, GET_HELM_RELEASES, GetHelmRelease, GetHelmReleases } from './workloads.actions'; +import { + GET_HELM_RELEASE, + GET_HELM_RELEASE_HISTORY, + GET_HELM_RELEASES, + GetHelmRelease, + GetHelmReleaseHistory, + GetHelmReleases, + UPGRADE_HELM_RELEASE, + UpgradeHelmRelease, +} from './workloads.actions'; @Injectable() export class WorkloadsEffects { @@ -82,7 +93,77 @@ export class WorkloadsEffects { }) ); - private mapHelmRelease(data, endpointId, guid: string) { + @Effect() + fetchHelmReleaseHistory$ = this.actions$.pipe( + ofType(GET_HELM_RELEASE_HISTORY), + flatMap(action => { + const entityKey = entityCatalog.getEntityKey(action); + return this.makeRequest( + action, + `/pp/${this.proxyAPIVersion}/helm/releases/${action.endpointGuid}/${action.namespace}/${action.releaseTitle}/history`, + (response) => { + const processedData = { + entities: { [entityKey]: {} }, + result: [] + } as NormalizedResponse; + + const data: HelmReleaseHistory = { + endpointId: action.endpointGuid, + releaseTitle: action.releaseTitle, + revisions: [] + }; + + for (const version of response) { + data.revisions.push({ + ...version.info, + revision: version.version, + chart: version.chart.metadata, + values: version.chart.values + }); + } + // Store the data against the release guid + processedData.entities[entityKey][action.guid] = data; + processedData.result.push(action.guid); + return processedData; + }, [action.endpointGuid]); + }) + ); + + @Effect() + helmUpgrade$ = this.actions$.pipe( + ofType(UPGRADE_HELM_RELEASE), + flatMap(action => { + const requestType: ApiRequestTypes = 'update'; + const url = `/pp/v1//helm/releases/${action.endpointGuid}/${action.namespace}/${action.releaseTitle}`; + this.store.dispatch(new StartRequestAction(action, requestType)); + // Refresh the workload after upgrade + const fetchAction = workloadsEntityCatalog.release.actions.get(action.releaseTitle, action.endpointGuid, + { namespace: action.namespace }); + return this.httpClient.post(url, action.values).pipe( + mergeMap(() => { + return [ + fetchAction, + new WrapperRequestActionSuccess(null, action) + ]; + }), + catchError(error => { + const { status, message } = HelmEffects.createHelmError(error); + const errorMessage = `Failed to upgrade helm release: ${message}`; + return [ + new WrapperRequestActionFailed(errorMessage, action, requestType, { + endpointIds: [action.endpointGuid], + url: error.url || url, + eventCode: status, + message: errorMessage, + error + }) + ]; + }) + ); + }) + ); + +private mapHelmRelease(data, endpointId, guid: string) { const helmRelease: HelmRelease = { ...data, endpointId diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/release-version-data-source.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/release-version-data-source.ts new file mode 100644 index 0000000000..f98ddea450 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/release-version-data-source.ts @@ -0,0 +1,61 @@ +import { Store } from '@ngrx/store'; +import { Observable, of } from 'rxjs'; + +import { + DataFunction, + ListDataSource, +} from '../../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source'; +import { RowState } from '../../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source-types'; +import { IListConfig } from '../../../../../../core/src/shared/components/list/list.component.types'; +import { PaginationEntityState } from '../../../../../../store/src/types/pagination.types'; +import { helmEntityCatalog } from '../../../helm/helm-entity-catalog'; +import { helmEntityFactory, monocularChartVersionsEntityType } from '../../../helm/helm-entity-factory'; +import { MonocularVersion } from './../../../helm/store/helm.types'; + + +const typeFilterKey = 'versionType'; + + +export class HelmReleaseVersionsDataSource extends ListDataSource { + + private currentVersion: string; + + constructor( + store: Store, + listConfig: IListConfig, + repoName: string, + chartName: string, + version: string, + ) { + super({ + store, + action: helmEntityCatalog.chartVersions.actions.getMultiple(repoName, chartName), + schema: helmEntityFactory(monocularChartVersionsEntityType), + getRowUniqueId: (object: MonocularVersion) => object.id, + paginationKey: helmEntityCatalog.chartVersions.actions.getMultiple(repoName, chartName).paginationKey, + isLocal: true, + transformEntities: [ + (entities: MonocularVersion[], paginationState: PaginationEntityState) => this.endpointTypeFilter(entities, paginationState) + ], + listConfig, + }); + + this.currentVersion = version; + this.getRowState = (row: any): Observable => of({ highlighted: row.attributes.version === this.currentVersion }); + } + + + public endpointTypeFilter: DataFunction = (entities: MonocularVersion[], paginationState: PaginationEntityState) => { + if ( + !paginationState.clientPagination || + !paginationState.clientPagination.filter || + !paginationState.clientPagination.filter.items[typeFilterKey]) { + return entities; + } + + // Filter out development versions if configured + const showAll = paginationState.clientPagination.filter.items[typeFilterKey] === 'all'; + return showAll ? entities : entities.filter(e => e.attributes.version.indexOf('-') === -1); + } +} + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/release-version-list-config.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/release-version-list-config.ts new file mode 100644 index 0000000000..6d712da1d5 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/release-version-list-config.ts @@ -0,0 +1,131 @@ +import { Store } from '@ngrx/store'; +import * as moment from 'moment'; +import { BehaviorSubject, of } from 'rxjs'; +import { first } from 'rxjs/operators'; + +import { ListDataSource } from '../../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source'; +import { + TableCellRadioComponent, +} from '../../../../../../core/src/shared/components/list/list-table/table-cell-radio/table-cell-radio.component'; +import { ITableColumn } from '../../../../../../core/src/shared/components/list/list-table/table.types'; +import { + defaultPaginationPageSizeOptionsTable, + IGlobalListAction, + IListAction, + IListConfig, + IListMultiFilterConfig, + IMultiListAction, + ListViewTypes, +} from '../../../../../../core/src/shared/components/list/list.component.types'; +import { MonocularVersion } from '../../../helm/store/helm.types'; +import { HelmReleaseVersionsDataSource } from './release-version-data-source'; + +const typeFilterKey = 'versionType'; + +export class ReleaseUpgradeVersionsListConfig implements IListConfig { + + public versionsDataSource: ListDataSource; + + private multiFiltersConfigs: IListMultiFilterConfig[]; + + getGlobalActions: () => IGlobalListAction[]; + getMultiActions: () => IMultiListAction[]; + getSingleActions: () => IListAction[]; + + columns: Array> = [ + { + columnId: 'radio', + headerCell: () => '', + cellComponent: TableCellRadioComponent, + class: 'table-column-select', + cellFlex: '0 0 60px' + }, + { + columnId: 'version', + headerCell: () => 'Version', + cellFlex: '2', + cellDefinition: { + valuePath: 'attributes.version' + } + }, + { + columnId: 'created', + headerCell: () => 'Created', + cellFlex: '3', + cellDefinition: { + getValue: row => moment(row.attributes.created).format('LLL') + } + }, + { + columnId: 'age', + headerCell: () => 'Age', + cellFlex: '2', + cellDefinition: { + getValue: row => moment(row.attributes.created).fromNow(true) + } + }, + ]; + pageSizeOptions = defaultPaginationPageSizeOptionsTable; + viewType = ListViewTypes.TABLE_ONLY; + + hideRefresh = true; + + getColumns = () => this.columns; + getMultiFiltersConfigs = (): IListMultiFilterConfig[] => this.multiFiltersConfigs; + + getDataSource = () => this.versionsDataSource; + + constructor( + store: Store, + repoName: string, + chartName: string, + version: string, + ) { + this.getGlobalActions = () => []; + this.getMultiActions = () => []; + this.getSingleActions = () => []; + + this.versionsDataSource = new HelmReleaseVersionsDataSource(store, this, repoName, chartName, version); + + this.multiFiltersConfigs = [{ + hideAllOption: true, + autoSelectFirst: true, + key: typeFilterKey, + label: 'Endpoint Type', + list$: of([ + { + label: 'Release Versions', + item: {}, + value: 'release' + }, + { + label: 'All Versions', + item: {}, + value: 'all' + } + ]), + loading$: of(false), + select: new BehaviorSubject(undefined) + }]; + + // Auto-select first non-development version + setTimeout(() => { + this.versionsDataSource.page$.pipe(first()).subscribe(rs => { + if (rs && rs.length > 0) { + this.versionsDataSource.selectedRowToggle(this.getFirstNonDevelopmentVersion(rs), false); + } + }); + }, 0); + } + + // Get the first version that is a non-development version (no hypen in the version number) + private getFirstNonDevelopmentVersion(rows: MonocularVersion[]): MonocularVersion { + for (const mv of rows) { + if (mv.attributes.version.indexOf('-') === -1) { + return mv + } + } + return rows[0]; + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.html new file mode 100644 index 0000000000..dae9721887 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.html @@ -0,0 +1,37 @@ + + Upgrade Workload + + + + + + +
+
+
+

Enter YAML Value Overrides

+ +
+ + Values + + +
+ +
+
+ + + + +
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.scss new file mode 100644 index 0000000000..8d5007efa3 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.scss @@ -0,0 +1,59 @@ +:host { + flex: 1; +} +.workload-upgrade { + + &__list { + width: 100%; + } + + &__form { + display: flex; + flex: 1; + flex-direction: column; + } + &__heading { + align-items: center; + display: flex; + } + + &__title { + flex: 1; + font-size: 14px; + } + &__button { + height: 36px; + } +} + +form { + flex: 1; + + mat-checkbox { + display: flex; + height: 23px; + margin-top: 10px; + } +} + +.overrides { + &__yaml { + background-color: rgba(0, 0, 0, .1); + font-family: 'Source Code Pro', monospace; + height: 400px; + } + + &_form { + max-width: 100%; + } + + &_form-field { + flex: 1; + height: 100%; + width: 100%; + } +} + +form.overrides_form { + max-width: 100%; +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.spec.ts new file mode 100644 index 0000000000..f77dc54235 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.spec.ts @@ -0,0 +1,37 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HelmReleaseProviders, KubeBaseGuidMock } from '../../kubernetes.testing.module'; +import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; +import { WorkloadsBaseTestingModule } from '../workloads.testing.module'; +import { UpgradeReleaseComponent } from './upgrade-release.component'; + + +describe('UpgradeReleaseComponent', () => { + let component: UpgradeReleaseComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UpgradeReleaseComponent ], + imports: [ + ...WorkloadsBaseTestingModule + ], + providers: [ + KubernetesEndpointService, + KubeBaseGuidMock, + ...HelmReleaseProviders, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UpgradeReleaseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.ts new file mode 100644 index 0000000000..8ef8a83118 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.ts @@ -0,0 +1,114 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Observable, of } from 'rxjs'; +import { filter, first, map, pairwise } from 'rxjs/operators'; + +import { StepComponent, StepOnNextFunction } from '../../../../../../core/src/shared/components/stepper/step/step.component'; +import { ActionState } from '../../../../../../store/src/reducers/api-request-reducer/types'; +import { HelmUpgradeValues } from '../../../helm/store/helm.types'; +import { HelmReleaseHelperService } from '../release/tabs/helm-release-helper.service'; +import { HelmReleaseGuid } from '../workload.types'; +import { workloadsEntityCatalog } from './../workloads-entity-catalog'; +import { ReleaseUpgradeVersionsListConfig } from './release-version-list-config'; + +@Component({ + selector: 'app-upgrade-release', + templateUrl: './upgrade-release.component.html', + styleUrls: ['./upgrade-release.component.scss'], + providers: [ + HelmReleaseHelperService, + { + provide: HelmReleaseGuid, + useFactory: (activatedRoute: ActivatedRoute) => ({ + guid: activatedRoute.snapshot.params.guid + }), + deps: [ + ActivatedRoute + ] + } + ] +}) +export class UpgradeReleaseComponent { + + public cancelUrl; + public listConfig; + public validate$: Observable; + private version; + public overrides: FormGroup; + + // Future + public showAdvancedOptions = false; + + constructor(store: Store, public helper: HelmReleaseHelperService) { + + this.cancelUrl = `/workloads/${this.helper.guid}`; + + // Form for overrides step (Helm Values) + this.overrides = new FormGroup({ + values: new FormControl('') + }); + + this.helper.hasUpgrade(true).pipe( + filter(c => !!c), + first() + ).subscribe(chart => { + const name = chart.upgrade.name; + const repoName = chart.upgrade.repo.name; + const version = chart.release.chart.metadata.version; + this.listConfig = new ReleaseUpgradeVersionsListConfig(store, repoName, name, version); + + // First step is valid when a version has been selected + this.validate$ = this.listConfig.versionsDataSource.selectedRows$.pipe( + map((rows: Map) => { + if (rows && rows.size === 1) { + this.version = rows.values().next().value; + return true; + } + return false; + }) + ); + }); + } + + // Hide/show the advanced options step + toggleAdvancedOptions() { + this.showAdvancedOptions = !this.showAdvancedOptions; + } + + doUpgrade: StepOnNextFunction = (index: number, step: StepComponent) => { + // If we are showing the advanced options, don't upgrade if we aer not on the last step + if (this.showAdvancedOptions && index === 1 ) { + return of({ success: true }); + } + + const values: HelmUpgradeValues = { + ...this.overrides.value, + restartPods: false, + chart: { + name: this.version.relationships.chart.data.name, + repo: this.version.relationships.chart.data.repo.name, + version: this.version.attributes.version, + }, + }; + + // Make the request + return workloadsEntityCatalog.release.api.upgrade(this.helper.releaseTitle, + this.helper.endpointGuid, this.helper.namespace, values).pipe( + // Wait for result of request + filter(state => !!state), + pairwise(), + filter(([oldVal, newVal]) => (oldVal.busy && !newVal.busy)), + map(([, newVal]) => newVal), + map(result => ({ + success: !result.error, + redirect: !result.error, + redirectPayload: { + path: !result.error ? this.cancelUrl : '' + }, + message: !result.error ? '' : result.message + })) + ); + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.types.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.types.ts index bf5b8e708b..321e86b734 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.types.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.types.ts @@ -21,7 +21,11 @@ export interface HelmRelease { chart: { values: any; metadata: { + name: string; + version: string; icon?: string; + description: string; + sources: string[]; }; }; } @@ -67,6 +71,19 @@ export interface HelmReleaseResources extends HelmReleaseEntity { kind: string }; +export interface HelmReleaseRevision { + first_deployed: string; + last_deployed: string; + deleted: boolean; + description: string; + status: string; + revision: number; +} + +export interface HelmReleaseHistory extends HelmReleaseEntity { + revisions: HelmReleaseRevision[], +}; + export interface HelmReleaseKubeAPIResource extends KubeAPIResource { apiVersion: string; kind: string; diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads-entity-catalog.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads-entity-catalog.ts index 6492abd0b9..30a8791541 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads-entity-catalog.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads-entity-catalog.ts @@ -1,7 +1,12 @@ import { StratosCatalogEntity } from '../../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { IFavoriteMetadata } from '../../../../../store/src/types/user-favorites.types'; -import { WorkloadGraphBuilders, WorkloadReleaseBuilders, WorkloadResourceBuilders } from './store/workload-action-builders'; -import { HelmRelease, HelmReleaseGraph, HelmReleaseResources } from './workload.types'; +import { + WorkloadGraphBuilders, + WorkloadReleaseBuilders, + WorkloadResourceBuilders, + WorkloadResourceHistoryBuilders, +} from './store/workload-action-builders'; +import { HelmRelease, HelmReleaseGraph, HelmReleaseHistory, HelmReleaseResources } from './workload.types'; /** * A strongly typed collection of Workload Catalog Entities. @@ -9,8 +14,9 @@ import { HelmRelease, HelmReleaseGraph, HelmReleaseResources } from './workload. */ export class WorkloadsEntityCatalog { release: StratosCatalogEntity; - graph: StratosCatalogEntity - resource: StratosCatalogEntity + graph: StratosCatalogEntity; + resource: StratosCatalogEntity; + history: StratosCatalogEntity; } /** diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts index 05804cb596..44ab5ce4bb 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts @@ -7,6 +7,9 @@ import { SharedModule } from '../../../../../core/src/shared/shared.module'; import { KubernetesModule } from '../kubernetes.module'; import { HelmReleaseCardComponent } from './list-types/helm-release-card/helm-release-card.component'; import { HelmReleaseTabBaseComponent } from './release/helm-release-tab-base/helm-release-tab-base.component'; +import { + HelmReleaseAnalysisTabComponent, +} from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component'; import { HelmReleaseNotesTabComponent } from './release/tabs/helm-release-notes-tab/helm-release-notes-tab.component'; import { HelmReleasePodsTabComponent } from './release/tabs/helm-release-pods/helm-release-pods-tab.component'; import { @@ -17,8 +20,9 @@ import { HelmReleaseSummaryTabComponent } from './release/tabs/helm-release-summ import { HelmReleaseValuesTabComponent } from './release/tabs/helm-release-values-tab/helm-release-values-tab.component'; import { HelmReleasesTabComponent } from './releases-tab/releases-tab.component'; import { WorkloadsStoreModule } from './store/workloads.store.module'; +import { UpgradeReleaseComponent } from './upgrade-release/upgrade-release.component'; import { WorkloadsRouting } from './workloads.routing'; -import { HelmReleaseAnalysisTabComponent } from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component'; +import { HelmReleaseHistoryTabComponent } from './release/tabs/helm-release-history-tab/helm-release-history-tab.component'; import { WorkloadLiveReloadComponent } from './release/workload-live-reload/workload-live-reload.component'; @NgModule({ @@ -29,7 +33,7 @@ import { WorkloadLiveReloadComponent } from './release/workload-live-reload/work WorkloadsStoreModule, WorkloadsRouting, NgxGraphModule, - KubernetesModule + KubernetesModule, ], declarations: [ HelmReleasesTabComponent, @@ -43,6 +47,8 @@ import { WorkloadLiveReloadComponent } from './release/workload-live-reload/work HelmReleaseCardComponent, HelmReleaseAnalysisTabComponent, WorkloadLiveReloadComponent, + UpgradeReleaseComponent, + HelmReleaseHistoryTabComponent, ], entryComponents: [ HelmReleaseCardComponent diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts index dc687ecd76..a7732ac7a5 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts @@ -3,6 +3,10 @@ import { RouterModule, Routes } from '@angular/router'; import { NgxChartsModule } from '@swimlane/ngx-charts'; import { HelmReleaseTabBaseComponent } from './release/helm-release-tab-base/helm-release-tab-base.component'; +import { + HelmReleaseAnalysisTabComponent, +} from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component'; +import { HelmReleaseHistoryTabComponent } from './release/tabs/helm-release-history-tab/helm-release-history-tab.component'; import { HelmReleaseNotesTabComponent } from './release/tabs/helm-release-notes-tab/helm-release-notes-tab.component'; import { HelmReleasePodsTabComponent } from './release/tabs/helm-release-pods/helm-release-pods-tab.component'; import { @@ -12,7 +16,7 @@ import { HelmReleaseServicesTabComponent } from './release/tabs/helm-release-ser import { HelmReleaseSummaryTabComponent } from './release/tabs/helm-release-summary-tab/helm-release-summary-tab.component'; import { HelmReleaseValuesTabComponent } from './release/tabs/helm-release-values-tab/helm-release-values-tab.component'; import { HelmReleasesTabComponent } from './releases-tab/releases-tab.component'; -import { HelmReleaseAnalysisTabComponent } from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component'; +import { UpgradeReleaseComponent } from './upgrade-release/upgrade-release.component'; const routes: Routes = [ { @@ -23,6 +27,11 @@ const routes: Routes = [ component: HelmReleasesTabComponent, pathMatch: 'full', }, + { + path: ':guid/upgrade', + component: UpgradeReleaseComponent, + pathMatch: 'full', + }, { // Helm Release Views path: ':guid', @@ -35,6 +44,7 @@ const routes: Routes = [ { path: 'summary', component: HelmReleaseSummaryTabComponent }, { path: 'notes', component: HelmReleaseNotesTabComponent }, { path: 'values', component: HelmReleaseValuesTabComponent }, + { path: 'history', component: HelmReleaseHistoryTabComponent }, { path: 'pods', component: HelmReleasePodsTabComponent }, { path: 'services', component: HelmReleaseServicesTabComponent }, { path: 'graph', component: HelmReleaseResourceGraphComponent }, diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.testing.module.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.testing.module.ts new file mode 100644 index 0000000000..5f6b193777 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.testing.module.ts @@ -0,0 +1,47 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { CoreModule } from '@angular/flex-layout'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { SharedModule } from '../../../../../core/src/public-api'; +import { AppTestModule } from '../../../../../core/test-framework/core-test.helper'; +import { CATALOGUE_ENTITIES, EntityCatalogFeatureModule } from '../../../../../store/src/entity-catalog.module'; +import { entityCatalog, TestEntityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; +import { generateStratosEntities } from '../../../../../store/src/stratos-entity-generator'; +import { createBasicStoreModule } from '../../../../../store/testing/public-api'; +import { generateHelmEntities } from '../../helm/helm-entity-generator'; +import { HelmTestingModule } from '../../helm/helm-testing.module'; +import { generateKubernetesEntities } from '../kubernetes-entity-generator'; + +@NgModule({ + imports: [{ + ngModule: EntityCatalogFeatureModule, + providers: [ + { + provide: CATALOGUE_ENTITIES, useFactory: () => { + const testEntityCatalog = entityCatalog as TestEntityCatalog; + testEntityCatalog.clear(); + return [ + ...generateStratosEntities(), + ...generateKubernetesEntities(), + ...generateHelmEntities(), + ]; + } + } + ] + }] +}) +export class WorkloadsTestingModule { } + +export const WorkloadsBaseTestingModule = [ + AppTestModule, + RouterTestingModule, + CoreModule, + createBasicStoreModule(), + NoopAnimationsModule, + HttpClientModule, + SharedModule, + HelmTestingModule, + WorkloadsTestingModule +] diff --git a/src/jetstream/plugins/kubernetes/install_release.go b/src/jetstream/plugins/kubernetes/install_release.go index 0505f51f06..15cf67623e 100644 --- a/src/jetstream/plugins/kubernetes/install_release.go +++ b/src/jetstream/plugins/kubernetes/install_release.go @@ -15,6 +15,7 @@ import ( "k8s.io/client-go/kubernetes" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" @@ -34,6 +35,15 @@ type installRequest struct { } `json:"chart"` } +type upgradeRequest struct { + Values string `json:"values"` + Chart struct { + Name string `json:"name"` + Repository string `json:"repo"` + Version string `json:"version"` + } `json:"chart"` +} + // Monocular is a plugin for Monocular type Monocular interface { GetChartStore() *chartsvc.ChartSvcDatastore @@ -51,36 +61,11 @@ func (c *KubernetesSpecification) InstallRelease(ec echo.Context) error { return interfaces.NewJetstreamErrorf("Could not get Create Release Parameters: %v+", err) } - chartID := fmt.Sprintf("%s/%s", params.Chart.Repository, params.Chart.Name) - - log.Debugf("Helm: Installing release %s", chartID) - - downloadURL, err := c.getChart(chartID, params.Chart.Version) - if err != nil { - return interfaces.NewJetstreamErrorf("Could not get the Download URL for the Helm Chart") - } - - log.Debugf("Chart Download URL: %s", downloadURL) - - // NWM: Should we look up Helm Repository endpoint and use the value from that - httpClient := c.portalProxy.GetHttpClient(false) - resp, err := httpClient.Get(downloadURL) - if err != nil { - return interfaces.NewJetstreamErrorf("Could not download Chart Archive: %s", err) - } - if resp.StatusCode != 200 { - return interfaces.NewJetstreamErrorf("Could not download Chart Archive: %s", resp.Status) - } - - defer resp.Body.Close() - - chart, err := loader.LoadArchive(resp.Body) + chart, err := c.loadChart(params.Chart.Repository, params.Chart.Name, params.Chart.Version) if err != nil { - return interfaces.NewJetstreamErrorf("Could not load chart from archive: %v+", err) + return interfaces.NewJetstreamErrorf("Could not load chart: %v+", err) } - log.Debugf("Loaded helm chart: %s", chart.Name()) - endpointGUID := params.Endpoint userGUID := ec.Get("user_id").(string) @@ -154,6 +139,32 @@ func (c *KubernetesSpecification) getChart(chartID, version string) (string, err return "", errors.New("Could not find Chart Version") } +// Load the Helm chart for the given repository, name and version +func (c *KubernetesSpecification) loadChart(repo, name, version string) (*chart.Chart, error) { + + chartID := fmt.Sprintf("%s/%s", repo, name) + downloadURL, err := c.getChart(chartID, version) + if err != nil { + return nil, fmt.Errorf("Could not get the Download URL for the Helm Chart") + } + + log.Debugf("Helm Chart Download URL: %s", downloadURL) + + // NWM: Should we look up Helm Repository endpoint and use the value from that + httpClient := c.portalProxy.GetHttpClient(false) + resp, err := httpClient.Get(downloadURL) + if err != nil { + return nil, fmt.Errorf("Could not download Chart Archive: %s", err) + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("Could not download Chart Archive: %s", resp.Status) + } + + defer resp.Body.Close() + + return loader.LoadArchive(resp.Body) +} + // DeleteRelease will delete a release func (c *KubernetesSpecification) DeleteRelease(ec echo.Context) error { endpointGUID := ec.Param("endpoint") @@ -170,7 +181,6 @@ func (c *KubernetesSpecification) DeleteRelease(ec echo.Context) error { defer hc.Cleanup() uninstall := action.NewUninstall(config) - deleteResponse, err := uninstall.Run(releaseName) if err != nil { return interfaces.NewJetstreamError("Could not delete Helm Release") @@ -178,3 +188,72 @@ func (c *KubernetesSpecification) DeleteRelease(ec echo.Context) error { return ec.JSON(200, deleteResponse) } + +// GetReleaseHistory will get the history for a release +func (c *KubernetesSpecification) GetReleaseHistory(ec echo.Context) error { + endpointGUID := ec.Param("endpoint") + releaseName := ec.Param("name") + namespace := ec.Param("namespace") + + userGUID := ec.Get("user_id").(string) + + config, hc, err := c.GetHelmConfiguration(endpointGUID, userGUID, namespace) + if err != nil { + return interfaces.NewJetstreamErrorf("Could not get Helm Configuration for endpoint: %+v", err) + } + + defer hc.Cleanup() + + history := action.NewHistory(config) + historyResponse, err := history.Run(releaseName) + if err != nil { + return interfaces.NewJetstreamError("Could not get history for the Helm Release") + } + + return ec.JSON(200, historyResponse) +} + +// UpgradeRelease will upgrade the specified release +func (c *KubernetesSpecification) UpgradeRelease(ec echo.Context) error { + endpointGUID := ec.Param("endpoint") + releaseName := ec.Param("name") + namespace := ec.Param("namespace") + + userGUID := ec.Get("user_id").(string) + + bodyReader := ec.Request().Body + buf := new(bytes.Buffer) + buf.ReadFrom(bodyReader) + + var params upgradeRequest + err := json.Unmarshal(buf.Bytes(), ¶ms) + if err != nil { + return interfaces.NewJetstreamErrorf("Could not get Upgrade Release Parameters: %+v", err) + } + + config, hc, err := c.GetHelmConfiguration(endpointGUID, userGUID, namespace) + if err != nil { + return interfaces.NewJetstreamErrorf("Could not get Helm Configuration for endpoint: %+v", err) + } + + defer hc.Cleanup() + + chart, err := c.loadChart(params.Chart.Repository, params.Chart.Name, params.Chart.Version) + if err != nil { + return interfaces.NewJetstreamErrorf("Could not load chart for upgrade: %+v", err) + } + + userSuppliedValues := map[string]interface{}{} + if err := yaml.Unmarshal([]byte(params.Values), &userSuppliedValues); err != nil { + // Could not parse the user's values + return interfaces.NewJetstreamErrorf("Could not parse values: %+v", err) + } + + upgrade := action.NewUpgrade(config) + upgradeResponse, err := upgrade.Run(releaseName, chart, userSuppliedValues) + if err != nil { + return interfaces.NewJetstreamErrorf("Could not upgrade Helm Release: %+v", err) + } + + return ec.JSON(200, upgradeResponse) +} diff --git a/src/jetstream/plugins/kubernetes/main.go b/src/jetstream/plugins/kubernetes/main.go index fb374deb69..628251591c 100644 --- a/src/jetstream/plugins/kubernetes/main.go +++ b/src/jetstream/plugins/kubernetes/main.go @@ -178,8 +178,10 @@ func (c *KubernetesSpecification) AddSessionGroupRoutes(echoGroup *echo.Group) { echoGroup.GET("/helm/releases", c.ListReleases) echoGroup.POST("/helm/install", c.InstallRelease) echoGroup.DELETE("/helm/releases/:endpoint/:namespace/:name", c.DeleteRelease) + echoGroup.GET("/helm/releases/:endpoint/:namespace/:name/history", c.GetReleaseHistory) echoGroup.GET("/helm/releases/:endpoint/:namespace/:name/status", c.GetReleaseStatus) echoGroup.GET("/helm/releases/:endpoint/:namespace/:name", c.GetRelease) + echoGroup.POST("/helm/releases/:endpoint/:namespace/:name", c.UpgradeRelease) // Kube Terminal if c.kubeTerminal != nil {