diff --git a/src/frontend/packages/core/src/app.module.ts b/src/frontend/packages/core/src/app.module.ts index 840ed60377..64f5ca665b 100644 --- a/src/frontend/packages/core/src/app.module.ts +++ b/src/frontend/packages/core/src/app.module.ts @@ -89,9 +89,6 @@ const storeDebugImports = environment.production ? [] : [ }) class AppStoreDebugModule { } -/** - * `HttpXsrfTokenExtractor` which retrieves the token from a cookie. - */ @NgModule({ declarations: [ diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.ts index e354112437..38d4b1e417 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.ts @@ -1,13 +1,17 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { combineLatest, filter, first, map } from 'rxjs/operators'; import { RouterNav } from '../../../../../../store/src/actions/router.actions'; import { GeneralEntityAppState } from '../../../../../../store/src/app-state'; import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog'; +import { + StratosCatalogEndpointEntity, +} from '../../../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { IStratosEndpointDefinition } from '../../../../../../store/src/entity-catalog/entity-catalog.types'; import { selectSessionData } from '../../../../../../store/src/reducers/auth.reducer'; +import { stratosEntityCatalog } from '../../../../../../store/src/stratos-entity-catalog'; import { BASE_REDIRECT_QUERY } from '../../../../shared/components/stepper/stepper.types'; import { TileConfigManager } from '../../../../shared/components/tile/tile-selector.helpers'; import { ITileConfig, ITileData } from '../../../../shared/components/tile/tile-selector.types'; @@ -17,6 +21,10 @@ interface ICreateEndpointTilesData extends ITileData { parentType: string; } +type EndpointsByType = { + [endpointType: string]: number, +}; + @Component({ selector: 'app-create-endpoint-base-step', templateUrl: './create-endpoint-base-step.component.html', @@ -67,7 +75,7 @@ export class CreateEndpointBaseStepComponent { } // Both A & B are equal. Unlikely. return 0; - } + }; get selectedTile() { return this.pSelectedTile; @@ -83,13 +91,15 @@ export class CreateEndpointBaseStepComponent { })); } } - constructor(public store: Store, ) { + constructor(public store: Store,) { // Need to filter the endpoint types on the tech preview flag this.tileSelectorConfig$ = store.select(selectSessionData()).pipe( + combineLatest(this.getEndpointTypesByCount()), first(), - map(sessionData => { + map(([sessionData, endpointTypesByCount]) => { const techPreviewIsEnabled = sessionData.config.enableTechPreview || false; return entityCatalog.getAllEndpointTypes(techPreviewIsEnabled) + .filter(endpoint => this.filterByEndpointCount(endpoint, endpointTypesByCount)) .sort((endpointA, endpointB) => this.sortEndpointTiles(endpointA.definition, endpointB.definition)) .map(catalogEndpoint => { const endpoint = catalogEndpoint.definition; @@ -112,4 +122,38 @@ export class CreateEndpointBaseStepComponent { ); } + private getEndpointDefinitionKey = (type: string, subType: string): string => type + '_sep_' + subType; + private getEndpointTypesByCount = (): Observable => + stratosEntityCatalog.endpoint.store.getAll.getPaginationService().entities$.pipe( + filter(endpoints => !!endpoints), + map(endpoints => { + const endpointsByType: { [endpointType: string]: number; } = {}; + return endpoints.reduce((res, endpoint) => { + const type = this.getEndpointDefinitionKey(endpoint.cnsi_type, endpoint.sub_type); + if (!res[type]) { + res[type] = 0; + } + res[type]++; + return res; + }, endpointsByType); + }), + ); + private filterByEndpointCount = (endpoint: StratosCatalogEndpointEntity, endpointTypesByCount: EndpointsByType) => { + // No limit applied, always show endpoint + if (typeof endpoint.definition.registeredLimit !== 'number') { + return true; + } + // Zero limit, never show endpoint + if (endpoint.definition.registeredLimit === 0) { + return false; + } + + // Check that the limit is not exceeded by endpoints already registered + const type = endpoint.definition.parentType ? + this.getEndpointDefinitionKey(endpoint.definition.parentType, endpoint.definition.type) : + this.getEndpointDefinitionKey(endpoint.definition.type, ''); + const count = endpointTypesByCount[type] || 0; + return count < endpoint.definition.registeredLimit; + }; + } diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss index 78aa3599eb..cc87470846 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss @@ -26,6 +26,7 @@ font-size: 40px; height: 40px; margin-right: 12px; + text-align: center; width: 40px; } &__label { diff --git a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.scss b/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.scss index 84f88ba55f..bbb07aa386 100644 --- a/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.scss +++ b/src/frontend/packages/core/src/shared/components/favorites-meta-card/favorites-meta-card.component.scss @@ -69,7 +69,6 @@ font-size: 14px; font-weight: normal; margin-right: 8px; - margin-top: -2px; padding: 2px 4px; } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.html index 075cdf838c..0223537772 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.html @@ -1,5 +1,5 @@ - +
Deleting
@@ -9,7 +9,8 @@ -
+
@@ -18,20 +19,24 @@
- +
- - + - -
+ + +
+
@@ -48,6 +53,7 @@ - + \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.ts index 59a95244d6..039954d7f2 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.ts @@ -1,5 +1,5 @@ import { Component, ContentChild, ContentChildren, Input, OnDestroy, QueryList } from '@angular/core'; -import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { combineLatest, Observable, of as observableOf, of, Subscription } from 'rxjs'; import { first, map, tap } from 'rxjs/operators'; import { FavoritesConfigMapper } from '../../../../../../../../store/src/favorite-config-mapper'; @@ -13,7 +13,7 @@ import { MetaCardItemComponent } from '../meta-card-item/meta-card-item.componen import { MetaCardTitleComponent } from '../meta-card-title/meta-card-title.component'; -export function createMetaCardMenuItemSeparator() { +export function createMetaCardMenuItemSeparator(): MenuItem { return { label: '-', separator: true, @@ -86,7 +86,7 @@ export class MetaCardComponent implements OnDestroy { this.pActionMenu = actionMenu.map(menuItem => { if (!menuItem.can) { menuItem.separator = menuItem.label === '-'; - menuItem.can = observableOf(!menuItem.separator); + menuItem.can = of(true); } if (!menuItem.disabled) { menuItem.disabled = observableOf(false); @@ -94,7 +94,10 @@ export class MetaCardComponent implements OnDestroy { return menuItem; }); - this.showMenu$ = combineLatest(actionMenu.map(menuItem => menuItem.can)).pipe( + const nonSeparators = actionMenu + .filter(menuItem => !menuItem.separator) + .map(menuItem => menuItem.can); + this.showMenu$ = combineLatest(nonSeparators).pipe( map(cans => cans.some(can => can)) ); } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts index dfdd1548a2..82bb85e5a4 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts @@ -93,13 +93,18 @@ export class EndpointCardComponent extends CardCell implements On @Input('dataSource') set dataSource(ds: BaseEndpointsDataSource) { this.pDs = ds; + // Don't show card menu if the ds only provides a single endpoint type (for instance the cf endpoint page) if (ds && !ds.dsEndpointType && !this.cardMenu) { - this.cardMenu = this.endpointListHelper.endpointActions().map(endpointAction => ({ - label: endpointAction.label, - action: () => endpointAction.action(this.pRow), - can: endpointAction.createVisible(this.rowObs) - })); + this.cardMenu = this.endpointListHelper.endpointActions(true).map(endpointAction => { + const separator = endpointAction.label === '-'; + return { + label: endpointAction.label, + action: () => endpointAction.action(this.pRow), + can: endpointAction.createVisible ? endpointAction.createVisible(this.rowObs) : null, + separator + }; + }); // Add a copy address to clipboard this.cardMenu.push(createMetaCardMenuItemSeparator()); diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index e1ebaabf50..41347976e7 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -1,7 +1,7 @@ import { ComponentFactoryResolver, ComponentRef, Injectable, ViewContainerRef } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; -import { combineLatest, Observable } from 'rxjs'; +import { combineLatest, Observable, of } from 'rxjs'; import { map, pairwise } from 'rxjs/operators'; import { RouterNav } from '../../../../../../../store/src/actions/router.actions'; @@ -19,6 +19,7 @@ import { import { SnackBarService } from '../../../../services/snackbar.service'; import { ConfirmationDialogConfig } from '../../../confirmation-dialog.config'; import { ConfirmationDialogService } from '../../../confirmation-dialog.service'; +import { createMetaCardMenuItemSeparator } from '../../list-cards/meta-card/meta-card-base/meta-card.component'; import { IListAction } from '../../list.component.types'; import { TableCellCustom } from '../../list.types'; @@ -37,6 +38,27 @@ function isEndpointListDetailsComponent(obj: any): EndpointListDetailsComponent return obj ? obj.isEndpointListDetailsComponent ? obj as EndpointListDetailsComponent : null : null; } +/** + * Combine the result of all createVisibles functions for the given actions + */ +function combineCreateVisibles( + customActions: IListAction[] +): (row$: Observable) => Observable { + const createVisiblesFns = customActions + .map(action => action.createVisible) + .filter(createVisible => !!createVisible); + if (createVisiblesFns.length === 0) { + return () => of(false); + } else { + return (row$: Observable) => { + const createVisibles = createVisiblesFns.map(createVisible => createVisible(row$)); + return combineLatest(createVisibles).pipe( + map(allRes => allRes.some(res => res)) + ); + }; + } +} + @Injectable() export class EndpointListHelper { constructor( @@ -48,7 +70,23 @@ export class EndpointListHelper { private snackBarService: SnackBarService, ) { } - endpointActions(): IListAction[] { + endpointActions(includeSeparators = false): IListAction[] { + // Add any additional actions that are per endpoint type + const customActions = entityCatalog.getAllEndpointTypes() + .map(endpoint => endpoint.definition.endpointListActions) + .filter(endpointListActions => !!endpointListActions) + .map(endpointListActions => endpointListActions(this.store)) + .reduce((res, actions) => res.concat(actions), []); + + if (includeSeparators && customActions.length) { + // Only show the separator if we have custom actions to separate AND at least one is visible + const createVisibleFn = combineCreateVisibles(customActions); + customActions.splice(0, 0, { + ...createMetaCardMenuItemSeparator(), + createVisible: createVisibleFn + }); + } + return [ { action: (item) => { @@ -126,7 +164,8 @@ export class EndpointListHelper { label: 'Edit endpoint', description: 'Edit the endpoint', createVisible: () => this.currentUserPermissionsService.can(StratosCurrentUserPermissions.ENDPOINT_REGISTER) - } + }, + ...customActions ]; } diff --git a/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.ts b/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.ts index 49c3e95cf2..18c8c144de 100644 --- a/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.ts +++ b/src/frontend/packages/core/src/shared/components/tile-selector/tile-selector.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; + import { ITileConfig } from '../tile/tile-selector.types'; @@ -12,6 +13,9 @@ export class TileSelectorComponent { public hiddenOptions: ITileConfig[] = []; public showingMore = false; @Input() set options(options: ITileConfig[]) { + if (!options) { + return; + } const groupedOptions = options.reduce((grouped, option) => { if (option.hidden) { grouped.hidden.push(option); @@ -20,9 +24,9 @@ export class TileSelectorComponent { } return grouped; }, { - show: [], - hidden: [] - }); + show: [], + hidden: [] + }); this.pOptions = groupedOptions.show; this.hiddenOptions = groupedOptions.hidden; } diff --git a/src/frontend/packages/core/xsrf.module.ts b/src/frontend/packages/core/xsrf.module.ts index 5d875f273f..b1456f72eb 100644 --- a/src/frontend/packages/core/xsrf.module.ts +++ b/src/frontend/packages/core/xsrf.module.ts @@ -13,6 +13,9 @@ import { tap } from 'rxjs/operators'; const STRATOS_XSRF_HEADER_NAME = 'X-XSRF-Token'; +/** + * `HttpXsrfTokenExtractor` which retrieves the token from a cookie. + */ @Injectable() export class HttpXsrfHeaderExtractor implements HttpXsrfTokenExtractor { diff --git a/src/frontend/packages/store/src/entity-catalog/entity-catalog.ts b/src/frontend/packages/store/src/entity-catalog/entity-catalog.ts index 5565256852..224f05b471 100644 --- a/src/frontend/packages/store/src/entity-catalog/entity-catalog.ts +++ b/src/frontend/packages/store/src/entity-catalog/entity-catalog.ts @@ -176,7 +176,7 @@ class EntityCatalog { return Array.from(this.endpoints.values()); } - public getAllEndpointTypes(techPreviewEnabled = false) { + public getAllEndpointTypes(techPreviewEnabled = false): StratosCatalogEndpointEntity[] { const baseEndpoints = Array.from(this.endpoints.values()) .filter(item => !item.definition.techPreview || item.definition.techPreview && techPreviewEnabled); return baseEndpoints.reduce((allEndpoints, baseEndpoint) => { diff --git a/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts b/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts index fa574a1754..b0dfe4151f 100644 --- a/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts +++ b/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts @@ -1,7 +1,8 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { GeneralEntityAppState } from '../app-state'; +import { IListAction } from '../../../core/src/shared/components/list/list.component.types'; +import { AppState, GeneralEntityAppState } from '../app-state'; import { ApiErrorMessageHandler, EntitiesFetchHandler, @@ -119,6 +120,10 @@ export interface IStratosEndpointDefinition) => IListAction[]; } export interface StratosEndpointExtensionDefinition extends Omit { } @@ -206,7 +216,7 @@ export interface IStratosEntityBuilder { getLines?(): EntityRowBuilder[]; getSubTypeLabels?(entityMetadata: T): { singular: string, - plural: string + plural: string, }; /** * Actions that don't effect an individual entity i.e. create new diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/chart-view/monocular.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/chart-view/monocular.component.ts index fb74ce9981..f8cb93cdff 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/chart-view/monocular.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/chart-view/monocular.component.ts @@ -1,10 +1,17 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { createMonocularProviders } from '../monocular/stratos-monocular-providers.helpers'; +import { getMonocularEndpoint } from '../monocular/stratos-monocular.helper'; + + @Component({ selector: 'app-monocular', templateUrl: './monocular.component.html', styleUrls: ['./monocular.component.scss'], + providers: [ + ...createMonocularProviders() + ] }) export class MonocularChartViewComponent implements OnInit { @@ -27,7 +34,7 @@ export class MonocularChartViewComponent implements OnInit { if (!!parts.version) { breadcrumbs.push( - { value: this.title, routerLink: `/monocular/charts/${parts.repo}/${parts.chartName}` } + { value: this.title, routerLink: `/monocular/charts/${getMonocularEndpoint(this.route)}/${parts.repo}/${parts.chartName}` } ); this.title = parts.version; } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/create-release/create-release.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/create-release/create-release.component.ts index d830c232f6..c57b1175cf 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/create-release/create-release.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/create-release/create-release.component.ts @@ -16,12 +16,17 @@ import { kubeEntityCatalog } from '../../kubernetes/kubernetes-entity-catalog'; import { KUBERNETES_ENDPOINT_TYPE } from '../../kubernetes/kubernetes-entity-factory'; import { KubernetesNamespace } from '../../kubernetes/store/kube.types'; import { helmEntityCatalog } from '../helm-entity-catalog'; +import { createMonocularProviders } from '../monocular/stratos-monocular-providers.helpers'; +import { getMonocularEndpoint, stratosMonocularEndpointGuid } from '../monocular/stratos-monocular.helper'; import { HelmInstallValues } from '../store/helm.types'; @Component({ selector: 'app-create-release', templateUrl: './create-release.component.html', styleUrls: ['./create-release.component.scss'], + providers: [ + ...createMonocularProviders() + ] }) export class CreateReleaseComponent implements OnInit, OnDestroy { @@ -61,7 +66,7 @@ export class CreateReleaseComponent implements OnInit, OnDestroy { private confirmDialog: ConfirmationDialogService, ) { const chart = this.route.snapshot.params; - this.cancelUrl = `/monocular/charts/${chart.repo}/${chart.chartName}/${chart.version}`; + this.cancelUrl = `/monocular/charts/${getMonocularEndpoint(this.route)}/${chart.repo}/${chart.chartName}/${chart.version}`; this.setupDetailsStep(); @@ -207,13 +212,13 @@ export class CreateReleaseComponent implements OnInit, OnDestroy { // this.overridesYamlAutosize.resizeToFitContent(true); this.overridesYamlTextArea.nativeElement.focus(); }, 1); - } + }; submit: StepOnNextFunction = () => { return this.createNamespace().pipe( switchMap(createRes => createRes.success ? this.installChart() : of(createRes)) ); - } + }; createNamespace(): Observable { if (!this.details.controls.createNamespace.value || this.createdNamespace) { @@ -245,11 +250,17 @@ export class CreateReleaseComponent implements OnInit, OnDestroy { } installChart(): Observable { + const endpoint = getMonocularEndpoint(this.route, null, null); // Build the request body const values: HelmInstallValues = { ...this.details.value, ...this.overrides.value, - chart: this.route.snapshot.params + chart: { + chartName: this.route.snapshot.params.chartName, + repo: this.route.snapshot.params.repo, + version: this.route.snapshot.params.version, + }, + monocularEndpoint: endpoint === stratosMonocularEndpointGuid ? null : endpoint }; // Make the request 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 302dd8a060..6cdf2402a9 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 @@ -11,9 +11,11 @@ export const getMonocularChartId = (entity: MonocularChart) => entity.id; export const getHelmVersionId = (entity: HelmVersion) => entity.endpointId; export const HELM_ENDPOINT_TYPE = 'helm'; +export const HELM_REPO_ENDPOINT_TYPE = 'repo'; +export const HELM_HUB_ENDPOINT_TYPE = 'hub'; const entityCache: { - [key: string]: EntitySchema + [key: string]: EntitySchema, } = {}; export class HelmEntitySchema extends EntitySchema { @@ -49,7 +51,7 @@ entityCache[helmVersionsEntityType] = new HelmEntitySchema( entityCache[monocularChartVersionsEntityType] = new HelmEntitySchema( monocularChartVersionsEntityType, {}, - { idAttribute: getHelmVersionId } + { idAttribute: getMonocularChartId } ); export function helmEntityFactory(key: string): EntitySchema { 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 c9ef3d8afa..8ac9945a41 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 @@ -1,18 +1,28 @@ +import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { IListAction } from '../../../../core/src/shared/components/list/list.component.types'; +import { AppState } from '../../../../store/src/app-state'; import { StratosBaseCatalogEntity, StratosCatalogEndpointEntity, StratosCatalogEntity, } from '../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { StratosEndpointExtensionDefinition } from '../../../../store/src/entity-catalog/entity-catalog.types'; +import { EndpointModel } from '../../../../store/src/types/endpoint.types'; import { IFavoriteMetadata } from '../../../../store/src/types/user-favorites.types'; import { helmEntityCatalog } from './helm-entity-catalog'; import { HELM_ENDPOINT_TYPE, + HELM_HUB_ENDPOINT_TYPE, + HELM_REPO_ENDPOINT_TYPE, helmEntityFactory, helmVersionsEntityType, monocularChartsEntityType, monocularChartVersionsEntityType, } from './helm-entity-factory'; +import { HelmHubRegistrationComponent } from './helm-hub-registration/helm-hub-registration.component'; import { HelmChartActionBuilders, helmChartActionBuilders, @@ -25,18 +35,52 @@ import { HelmVersion, MonocularChart, MonocularVersion } from './store/helm.type export function generateHelmEntities(): StratosBaseCatalogEntity[] { + const helmRepoRenderPriority = 10; const endpointDefinition: StratosEndpointExtensionDefinition = { type: HELM_ENDPOINT_TYPE, - label: 'Helm Repository', - labelPlural: 'Helm Repositories', - icon: 'helm', - iconFont: 'stratos-icons', logoUrl: '/core/assets/custom/helm.svg', - urlValidation: undefined, - unConnectable: true, - techPreview: false, authTypes: [], - renderPriority: 10, + registeredLimit: 0, + icon: 'helm', + iconFont: 'stratos-icons', + label: 'Helm', + labelPlural: 'Helms', + subTypes: [ + { + type: HELM_REPO_ENDPOINT_TYPE, + label: 'Helm Repository', + labelPlural: 'Helm Repositories', + logoUrl: '/core/assets/custom/helm.svg', + urlValidation: undefined, + unConnectable: true, + techPreview: false, + authTypes: [], + endpointListActions: (store: Store): IListAction[] => { + return [{ + action: (item: EndpointModel) => helmEntityCatalog.chart.api.synchronise(item), + label: 'Synchronize', + description: '', + createVisible: row => row.pipe( + map(item => item.cnsi_type === HELM_ENDPOINT_TYPE && item.sub_type === HELM_REPO_ENDPOINT_TYPE) + ), + createEnabled: () => of(true) + }]; + }, + renderPriority: helmRepoRenderPriority, + registeredLimit: null, + }, + { + type: HELM_HUB_ENDPOINT_TYPE, + label: 'Helm Hub', + labelPlural: 'Helm Hubs', + authTypes: [], + unConnectable: true, + logoUrl: '/core/assets/custom/helm.svg', + renderPriority: helmRepoRenderPriority + 1, + registrationComponent: HelmHubRegistrationComponent, + registeredLimit: 1, + }, + ], }; return [ @@ -50,7 +94,7 @@ export function generateHelmEntities(): StratosBaseCatalogEntity[] { function generateEndpointEntity(endpointDefinition: StratosEndpointExtensionDefinition) { helmEntityCatalog.endpoint = new StratosCatalogEndpointEntity( endpointDefinition, - metadata => `/monocular/repos/${metadata.guid}`, + metadata => `/monocular/charts`, ); return helmEntityCatalog.endpoint; } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.html b/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.html new file mode 100644 index 0000000000..fe749d24f3 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.html @@ -0,0 +1,10 @@ + + +
+

Helm Hub is a public repository for Helm Charts.

+

To browse and install these charts you need to register Helm Hub as an endpoint by clicking `Register` below. +

+

To disable Helm Hub simply unregister the Helm Hub endpoint

+
+
+
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.scss b/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.scss similarity index 100% rename from src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.scss rename to src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.scss diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.spec.ts new file mode 100644 index 0000000000..df9c494821 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.spec.ts @@ -0,0 +1,33 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EndpointsService } from '../../../../../core/src/core/endpoints.service'; +import { UserService } from '../../../../../core/src/core/user.service'; +import { BaseTestModules } from '../../../../../core/test-framework/core-test.helper'; +import { HelmHubRegistrationComponent } from './helm-hub-registration.component'; + +describe('HelmHubRegistrationComponent', () => { + let component: HelmHubRegistrationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [...BaseTestModules], + declarations: [HelmHubRegistrationComponent], + providers: [ + EndpointsService, + UserService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HelmHubRegistrationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.ts new file mode 100644 index 0000000000..2b1a2ceeb3 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm-hub-registration/helm-hub-registration.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { filter, map, pairwise } from 'rxjs/operators'; + +import { StepOnNextFunction } from '../../../../../core/src/shared/components/stepper/step/step.component'; +import { ActionState } from '../../../../../store/src/reducers/api-request-reducer/types'; +import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; +import { HELM_ENDPOINT_TYPE, HELM_HUB_ENDPOINT_TYPE } from '../helm-entity-factory'; + +@Component({ + selector: 'app-helm-hub-registration', + templateUrl: './helm-hub-registration.component.html', + styleUrls: ['./helm-hub-registration.component.scss'] +}) +export class HelmHubRegistrationComponent { + + onNext: StepOnNextFunction = () => { + return stratosEntityCatalog.endpoint.api.register( + HELM_ENDPOINT_TYPE, + HELM_HUB_ENDPOINT_TYPE, + 'Helm Hub', + 'https://hub.helm.sh/api', + false + ).pipe( + pairwise(), + filter(([oldV, newV]) => oldV.busy && !newV.busy), + map(([, newV]) => newV), + map(state => ({ + success: !state.error, + message: state.message, + redirect: !state.error + })) + ); + }; + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm.module.ts b/src/frontend/packages/suse-extensions/src/custom/helm/helm.module.ts index d03d15582b..0bba8cacac 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/helm.module.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm.module.ts @@ -8,27 +8,8 @@ import { CreateReleaseModule } from './create-release/create-release.module'; import { HelmRoutingModule } from './helm.routing'; import { MonocularChartCardComponent } from './list-types/monocular-chart-card/monocular-chart-card.component'; import { MonocularTabBaseComponent } from './monocular-tab-base/monocular-tab-base.component'; -import { ChartDetailsInfoComponent } from './monocular/chart-details/chart-details-info/chart-details-info.component'; -import { ChartDetailsReadmeComponent } from './monocular/chart-details/chart-details-readme/chart-details-readme.component'; -import { ChartDetailsUsageComponent } from './monocular/chart-details/chart-details-usage/chart-details-usage.component'; -import { - ChartDetailsVersionsComponent, -} from './monocular/chart-details/chart-details-versions/chart-details-versions.component'; -import { ChartDetailsComponent } from './monocular/chart-details/chart-details.component'; -import { ChartIndexComponent } from './monocular/chart-index/chart-index.component'; -import { ChartItemComponent } from './monocular/chart-item/chart-item.component'; -import { ChartListComponent } from './monocular/chart-list/chart-list.component'; -import { ChartsComponent } from './monocular/charts/charts.component'; -import { ListFiltersComponent } from './monocular/list-filters/list-filters.component'; -import { ListItemComponent } from './monocular/list-item/list-item.component'; -import { LoaderComponent } from './monocular/loader/loader.component'; -import { PanelComponent } from './monocular/panel/panel.component'; -import { ChartsService } from './monocular/shared/services/charts.service'; -import { ConfigService } from './monocular/shared/services/config.service'; -import { MenuService } from './monocular/shared/services/menu.service'; -import { ReposService } from './monocular/shared/services/repos.service'; +import { MonocularModule } from './monocular/monocular.module'; import { CatalogTabComponent } from './tabs/catalog-tab/catalog-tab.component'; -import { RepositoryTabComponent } from './tabs/repository-tab/repository-tab.component'; @NgModule({ imports: [ @@ -37,33 +18,15 @@ import { RepositoryTabComponent } from './tabs/repository-tab/repository-tab.com SharedModule, HelmRoutingModule, CreateReleaseModule, + MonocularModule ], declarations: [ - PanelComponent, - ChartIndexComponent, - ChartListComponent, - ChartItemComponent, - ListItemComponent, - ListFiltersComponent, - LoaderComponent, - ChartsComponent, - ChartIndexComponent, - ChartDetailsComponent, - ChartDetailsUsageComponent, - ChartDetailsVersionsComponent, - ChartDetailsReadmeComponent, - ChartDetailsInfoComponent, MonocularTabBaseComponent, - RepositoryTabComponent, CatalogTabComponent, MonocularChartCardComponent, MonocularChartViewComponent, ], providers: [ - ChartsService, - ConfigService, - MenuService, - ReposService ], entryComponents: [ MonocularChartCardComponent, diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm.routing.ts b/src/frontend/packages/suse-extensions/src/custom/helm/helm.routing.ts index 0f67b21db6..37dc491734 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/helm.routing.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm.routing.ts @@ -5,7 +5,6 @@ import { MonocularChartViewComponent } from './chart-view/monocular.component'; import { CreateReleaseComponent } from './create-release/create-release.component'; import { MonocularTabBaseComponent } from './monocular-tab-base/monocular-tab-base.component'; import { CatalogTabComponent } from './tabs/catalog-tab/catalog-tab.component'; -import { RepositoryTabComponent } from './tabs/repository-tab/repository-tab.component'; const monocular: Routes = [ { @@ -15,14 +14,12 @@ const monocular: Routes = [ { path: '', redirectTo: 'charts', pathMatch: 'full' }, { path: 'charts', component: CatalogTabComponent }, { path: 'charts/:repo', component: CatalogTabComponent }, - { path: 'repos', component: RepositoryTabComponent }, - { path: 'repos/:guid', component: RepositoryTabComponent }, ] }, - { pathMatch: 'full', path: 'charts/:repo/:chartName/:version', component: MonocularChartViewComponent }, - { path: 'charts/:repo/:chartName', component: MonocularChartViewComponent }, - { pathMatch: 'full', path: 'install/:repo/:chartName/:version', component: CreateReleaseComponent }, - { pathMatch: 'full', path: 'install/:repo/:chartName', component: CreateReleaseComponent }, + { pathMatch: 'full', path: 'charts/:endpoint/:repo/:chartName/:version', component: MonocularChartViewComponent }, + { path: 'charts/:endpoint/:repo/:chartName', component: MonocularChartViewComponent }, + { pathMatch: 'full', path: 'install/:endpoint/:repo/:chartName/:version', component: CreateReleaseComponent }, + { path: 'install/:endpoint/:repo/:chartName', component: CreateReleaseComponent }, ]; @NgModule({ diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/helm.setup.module.ts b/src/frontend/packages/suse-extensions/src/custom/helm/helm.setup.module.ts index 3070b56567..3a57d47e51 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/helm.setup.module.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/helm.setup.module.ts @@ -11,6 +11,7 @@ import { EntityCatalogModule } from '../../../../store/src/entity-catalog.module import { EndpointHealthCheck } from '../../../../store/src/entity-catalog/entity-catalog.types'; import { HELM_ENDPOINT_TYPE } from './helm-entity-factory'; import { generateHelmEntities } from './helm-entity-generator'; +import { HelmHubRegistrationComponent } from './helm-hub-registration/helm-hub-registration.component'; import { HelmStoreModule } from './helm.store.module'; @NgModule({ @@ -20,6 +21,9 @@ import { HelmStoreModule } from './helm.store.module'; CommonModule, SharedModule, HelmStoreModule, + ], + declarations: [ + HelmHubRegistrationComponent ] }) export class HelmSetupModule { diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-charts-data-source.ts b/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-charts-data-source.ts index 45e3929eed..00d04b583f 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-charts-data-source.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-charts-data-source.ts @@ -2,7 +2,8 @@ import { Store } from '@ngrx/store'; import { ListDataSource } from '../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source'; import { IListConfig } from '../../../../../core/src/shared/components/list/list.component.types'; -import { AppState } from '../../../../../store/src/app-state'; +import { AppState, IRequestEntityTypeState } from '../../../../../store/src/app-state'; +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; import { PaginationEntityState } from '../../../../../store/src/types/pagination.types'; import { helmEntityCatalog } from '../helm-entity-catalog'; import { MonocularChart } from '../store/helm.types'; @@ -11,7 +12,8 @@ export class MonocularChartsDataSource extends ListDataSource { constructor( store: Store, - listConfig: IListConfig + listConfig: IListConfig, + endpoints: IRequestEntityTypeState ) { const action = helmEntityCatalog.chart.actions.getMultiple(); super({ @@ -22,13 +24,18 @@ export class MonocularChartsDataSource extends ListDataSource { paginationKey: action.paginationKey, isLocal: true, listConfig, - transformEntities: [{ type: 'filter', field: 'name' }, - (entities: MonocularChart[], paginationState: PaginationEntityState) => { - const repository = paginationState.clientPagination.filter.items.repository; - return entities.filter(e => { - return !(repository && repository !== e.attributes.repo.name); - }); - } + transformEntities: [ + { type: 'filter', field: 'name' }, + (entities: MonocularChart[], paginationState: PaginationEntityState) => { + const repository = paginationState.clientPagination.filter.items.repository; + if (!repository) { + return entities; + } + return entities.filter(e => e.monocularEndpointId ? + repository === endpoints[e.monocularEndpointId].name : + repository === e.attributes.repo.name + ); + } ] }); } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-charts-list-config.service.ts b/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-charts-list-config.service.ts index 5585990f79..cd41a26b0c 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-charts-list-config.service.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-charts-list-config.service.ts @@ -14,13 +14,14 @@ import { import { ListView } from '../../../../../store/src/actions/list.actions'; import { AppState } from '../../../../../store/src/app-state'; import { defaultHelmKubeListPageSize } from '../../kubernetes/list-types/kube-helm-list-types'; +import { HELM_ENDPOINT_TYPE } from '../helm-entity-factory'; import { MonocularChart } from '../store/helm.types'; import { MonocularChartCardComponent } from './monocular-chart-card/monocular-chart-card.component'; import { MonocularChartsDataSource } from './monocular-charts-data-source'; @Injectable() export class MonocularChartsListConfig implements IListConfig { - AppsDataSource: MonocularChartsDataSource; + dataSource: MonocularChartsDataSource; isLocal = true; multiFilterConfigs: IListMultiFilterConfig[]; @@ -74,26 +75,36 @@ export class MonocularChartsListConfig implements IListConfig { noEntries: 'There are no charts' }; + private initialised: Observable; + constructor( store: Store, private endpointsService: EndpointsService, private route: ActivatedRoute, ) { - this.AppsDataSource = new MonocularChartsDataSource(store, this); + + this.initialised = endpointsService.endpoints$.pipe( + filter(endpoints => !!endpoints), + map(endpoints => { + this.dataSource = new MonocularChartsDataSource(store, this, endpoints); + return true; + }), + ); } getGlobalActions = () => []; getMultiActions = () => []; getSingleActions = () => []; getColumns = () => this.columns; - getDataSource = () => this.AppsDataSource; + getDataSource = () => this.dataSource; getMultiFiltersConfigs = () => [this.createRepositoryFilterConfig()]; + getInitialised = () => this.initialised; private createRepositoryFilterConfig(): IListMultiFilterConfig { return { key: 'repository', - label: 'Repository', - allLabel: 'All Repositories', + label: 'Source', + allLabel: 'All Sources', list$: this.helmRepositories(), loading$: observableOf(false), select: new BehaviorSubject(this.route.snapshot.params.repo) @@ -105,7 +116,7 @@ export class MonocularChartsListConfig implements IListConfig { map(endpoints => { const repos = []; Object.values(endpoints).forEach(ep => { - if (ep.cnsi_type === 'helm') { + if (ep.cnsi_type === HELM_ENDPOINT_TYPE) { repos.push({ label: ep.name, item: ep.name, value: ep.name }); } }); diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-repository-list-config.service.ts b/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-repository-list-config.service.ts deleted file mode 100644 index 654ec79973..0000000000 --- a/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-repository-list-config.service.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable, NgZone } from '@angular/core'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { ActivatedRoute } from '@angular/router'; -import { Store } from '@ngrx/store'; -import { UnregisterEndpoint } from 'frontend/packages/store/src/actions/endpoint.actions'; -import { entityCatalog } from 'frontend/packages/store/src/entity-catalog/entity-catalog'; -import { selectDeletionInfo } from 'frontend/packages/store/src/selectors/api.selectors'; -import { of as observableOf } from 'rxjs'; -import { pairwise } from 'rxjs/operators'; - -import { CurrentUserPermissionsService } from '../../../../../core/src/core/permissions/current-user-permissions.service'; -import { StratosCurrentUserPermissions } from '../../../../../core/src/core/permissions/stratos-user-permissions.checker'; -import { environment } from '../../../../../core/src/environments/environment'; -import { ConfirmationDialogConfig } from '../../../../../core/src/shared/components/confirmation-dialog.config'; -import { ConfirmationDialogService } from '../../../../../core/src/shared/components/confirmation-dialog.service'; -import { ITableColumn } from '../../../../../core/src/shared/components/list/list-table/table.types'; -import { - EndpointCardComponent, -} from '../../../../../core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component'; -import { - TableCellEndpointStatusComponent, -} from '../../../../../core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-status/table-cell-endpoint-status.component'; -import { - IListAction, - IListConfig, - ListViewTypes, -} from '../../../../../core/src/shared/components/list/list.component.types'; -import { AppState } from '../../../../../store/src/app-state'; -import { getFullEndpointApiUrl } from '../../../../../store/src/endpoint-utils'; -import { endpointEntityType, STRATOS_ENDPOINT_TYPE } from '../../../../../store/src/helpers/stratos-entity-factory'; -import { EntityMonitorFactory } from '../../../../../store/src/monitors/entity-monitor.factory.service'; -import { InternalEventMonitorFactory } from '../../../../../store/src/monitors/internal-event-monitor.factory'; -import { PaginationMonitorFactory } from '../../../../../store/src/monitors/pagination-monitor.factory'; -import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; -import { defaultHelmKubeListPageSize } from '../../kubernetes/list-types/kube-helm-list-types'; -import { MonocularRepositoryDataSource } from './monocular-repository-list-source'; - -@Injectable() -export class MonocularRepositoryListConfig implements IListConfig { - isLocal = true; - dataSource: MonocularRepositoryDataSource; - viewType = ListViewTypes.TABLE_ONLY; - cardComponent = EndpointCardComponent; - text = { - title: '', - filter: 'Filter Repositories', - noEntries: 'There are no repositories' - }; - pageSizeOptions = defaultHelmKubeListPageSize; - enableTextFilter = true; - columns: ITableColumn[] = [ - { - columnId: 'name', - headerCell: () => 'Name', - cellDefinition: { - valuePath: 'name' - }, - sort: { - type: 'sort', - orderKey: 'name', - field: 'name' - }, - cellFlex: '2' - }, - { - columnId: 'address', - headerCell: () => 'Address', - cellDefinition: { - getValue: getFullEndpointApiUrl - }, - sort: { - type: 'sort', - orderKey: 'address', - field: 'api_endpoint.Host' - }, - cellFlex: '7' - }, - { - columnId: 'status', - headerCell: () => 'Status', - cellComponent: TableCellEndpointStatusComponent, - cellConfig: { - showLabel: true - }, - cellFlex: '2' - }, - ]; - - private endpointEntityKey = entityCatalog.getEntityKey(STRATOS_ENDPOINT_TYPE, endpointEntityType); - - private listActionSyncRepository: IListAction = { - action: (item: EndpointModel) => { - const requestArgs = { - headers: null, - params: null - }; - const proxyAPIVersion = environment.proxyAPIVersion; - const url = `/pp/${proxyAPIVersion}/chartrepos/${item.guid}`; - const req = this.httpClient.post(url, requestArgs); - req.subscribe(ok => { - this.snackBar.open('Helm Repository synchronization started', 'Dismiss', { duration: 3000 }); - // Refresh repository status - this.dataSource.refresh(); - }, err => { - this.snackBar.open(`Failed to Synchronize Helm Repository '${item.name}'`, 'Dismiss', { duration: 5000 }); - }); - return req; - }, - label: 'Synchronize', - description: '', - createVisible: () => observableOf(true), - createEnabled: () => observableOf(true) - }; - - private deleteRepository: IListAction = { - action: (item) => { - const confirmation = new ConfirmationDialogConfig( - 'Delete Helm Repository', - `Are you sure you want to delete repository '${item.name}'?`, - 'Delete', - true - ); - this.confirmDialog.open(confirmation, () => { - this.store.dispatch(new UnregisterEndpoint(item.guid, item.cnsi_type)); - this.handleDeleteAction(item, ([oldVal, newVal]) => { - this.snackBar.open(`Delete Helm Repository '${item.name}'`, 'Dismiss', { duration: 3000 }); - }); - }); - }, - label: 'Delete', - description: 'Delete Helm Repository', - createVisible: () => this.currentUserPermissionsService.can(StratosCurrentUserPermissions.ENDPOINT_REGISTER) - }; - - private handleDeleteAction(item, handleChange) { - this.handleAction(selectDeletionInfo( - this.endpointEntityKey, - item.guid, - ), handleChange); - } - - private handleAction(storeSelect, handleChange) { - const disSub = this.store.select(storeSelect).pipe( - pairwise()) - .subscribe(([oldVal, newVal]) => { - if (!newVal.error && (oldVal.busy && !newVal.busy)) { - handleChange([oldVal, newVal]); - disSub.unsubscribe(); - } - }); - } - - - constructor( - public store: Store, - public activatedRoute: ActivatedRoute, - paginationMonitorFactory: PaginationMonitorFactory, - entityMonitorFactory: EntityMonitorFactory, - internalEventMonitorFactory: InternalEventMonitorFactory, - ngZone: NgZone, - public httpClient: HttpClient, - public snackBar: MatSnackBar, - public confirmDialog: ConfirmationDialogService, - public currentUserPermissionsService: CurrentUserPermissionsService, - ) { - const highlighted = activatedRoute.snapshot.params.guid; - this.dataSource = new MonocularRepositoryDataSource( - this.store, - this, - highlighted, - paginationMonitorFactory, - entityMonitorFactory, - internalEventMonitorFactory, - ngZone, - ); - } - - public getColumns = () => this.columns; - public getGlobalActions = () => []; - public getMultiActions = () => []; - public getSingleActions = () => [this.listActionSyncRepository, this.deleteRepository]; - public getMultiFiltersConfigs = () => []; - public getDataSource = () => this.dataSource; -} diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-repository-list-source.ts b/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-repository-list-source.ts deleted file mode 100644 index fec0503321..0000000000 --- a/src/frontend/packages/suse-extensions/src/custom/helm/list-types/monocular-repository-list-source.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NgZone } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { interval, Observable, of as observableOf, Subscription } from 'rxjs'; - -import { safeUnsubscribe } from '../../../../../core/src/core/utils.service'; -import { RowState } from '../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source-types'; -import { - BaseEndpointsDataSource, - syncPaginationSection, -} from '../../../../../core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source'; -import { IListConfig } from '../../../../../core/src/shared/components/list/list.component.types'; -import { GetAllEndpoints } from '../../../../../store/src/actions/endpoint.actions'; -import { AppState } from '../../../../../store/src/app-state'; -import { EntityMonitorFactory } from '../../../../../store/src/monitors/entity-monitor.factory.service'; -import { InternalEventMonitorFactory } from '../../../../../store/src/monitors/internal-event-monitor.factory'; -import { PaginationMonitorFactory } from '../../../../../store/src/monitors/pagination-monitor.factory'; -import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; -import { HELM_ENDPOINT_TYPE } from '../helm-entity-factory'; - -export class MonocularRepositoryDataSource extends BaseEndpointsDataSource { - - private polls: Subscription[]; - private highlighted; - constructor( - store: Store, - listConfig: IListConfig, - highlighted: string, - paginationMonitorFactory: PaginationMonitorFactory, - entityMonitorFactory: EntityMonitorFactory, - internalEventMonitorFactory: InternalEventMonitorFactory, - ngZone: NgZone, - ) { - const action = new GetAllEndpoints(); - const paginationKey = 'mono-endpoints'; - // We do this here to ensure we sync up with main endpoint table data. - syncPaginationSection(store, action, paginationKey); - action.paginationKey = paginationKey; - super( - store, - listConfig, - action, - HELM_ENDPOINT_TYPE, - paginationMonitorFactory, - entityMonitorFactory, - internalEventMonitorFactory, - false); - this.highlighted = highlighted; - this.getRowState = (row: any): Observable => observableOf({ highlighted: row.guid === this.highlighted }); - this.polls = []; - ngZone.runOutsideAngular(() => { - this.polls.push( - interval(10000).subscribe(() => { - ngZone.run(() => { - store.dispatch(action); - }); - }) - ); - }); - } - - destroy() { - safeUnsubscribe(...(this.polls || [])); - } - -} diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular-tab-base/monocular-tab-base.component.html b/src/frontend/packages/suse-extensions/src/custom/helm/monocular-tab-base/monocular-tab-base.component.html index 106aa1b41e..73783346ce 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular-tab-base/monocular-tab-base.component.html +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular-tab-base/monocular-tab-base.component.html @@ -1,4 +1,4 @@ - +

Helm

\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular-tab-base/monocular-tab-base.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular-tab-base/monocular-tab-base.component.ts index e6cb223a67..236ad2c157 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular-tab-base/monocular-tab-base.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular-tab-base/monocular-tab-base.component.ts @@ -14,10 +14,6 @@ import { HELM_ENDPOINT_TYPE } from '../helm-entity-factory'; }) export class MonocularTabBaseComponent implements OnInit { - public tabLinks = [ - { link: 'charts', label: 'Charts', icon: 'folder_open' }, - { link: 'repos', label: 'Repositories', icon: 'products', iconFont: 'stratos-icons' } - ]; public endpointIds$: Observable; constructor(private store: Store) { } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular.interceptor.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular.interceptor.ts new file mode 100644 index 0000000000..ddb24966a1 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular.interceptor.ts @@ -0,0 +1,67 @@ +import { HttpBackend, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { stratosMonocularEndpointGuid } from './monocular/stratos-monocular.helper'; + +/** + * Add information to request to monocular to differ between stratos and helm hub monocular instances + */ +@Injectable() +export class MonocularInterceptor implements HttpInterceptor { + + constructor(private route: ActivatedRoute) { } + + /** + * The interceptor should only run for http clients provided in the helm module, but just in case only apply self for specific urls.. + */ + private readonly includeUrls = [ + '/pp/v1/chartsvc' + ]; + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const validUrl = this.includeUrls.find(part => req.url.indexOf(part) >= 0); + const endpoint = this.route.snapshot.params.endpoint; + const hasEndpoint = !!endpoint; + const isExternalMonocular = endpoint !== stratosMonocularEndpointGuid; + + const newReq = validUrl && hasEndpoint && isExternalMonocular ? req.clone({ + // Endpoint guid will be helm hub's endpoint + headers: req.headers.set('x-cap-cnsi-list', endpoint) + }) : req; + + return next.handle(newReq); + } +} + +class HttpInterceptorHandler implements HttpHandler { + constructor(private next: HttpHandler, private interceptor: HttpInterceptor) { } + + handle(req: HttpRequest): Observable> { + return this.interceptor.intercept(req, this.next); + } +} +export class HttpInterceptingHandler implements HttpHandler { + private chain: HttpHandler = null; + + constructor( + private backend: HttpBackend, + private interceptors: HttpInterceptor[], + private intercept?: (req: HttpRequest) => HttpRequest + ) { } + + handle(req: HttpRequest): Observable> { + if (this.intercept) { + req = this.intercept(req); + } + + if (this.chain === null) { + this.chain = this.interceptors.reduceRight( + (next, interceptor) => new HttpInterceptorHandler(next, interceptor), + this.backend + ); + } + return this.chain.handle(req); + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.html b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.html index 5801bc679a..aa041718ae 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.html +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.html @@ -1,11 +1,13 @@
- +
- +
- +
-
+ \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.ts index d6c91d4a44..73025fe6ac 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.ts @@ -2,9 +2,11 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; import { DomSanitizer } from '@angular/platform-browser'; +import { ActivatedRoute } from '@angular/router'; import { EndpointsService } from '../../../../../../../core/src/core/endpoints.service'; import { Chart } from '../../shared/models/chart'; +import { getMonocularEndpoint } from '../../stratos-monocular.helper'; @Component({ selector: 'app-chart-details-usage', @@ -22,7 +24,8 @@ export class ChartDetailsUsageComponent implements OnInit { private mdIconRegistry: MatIconRegistry, private sanitizer: DomSanitizer, public snackBar: MatSnackBar, - public endpointsService: EndpointsService + public endpointsService: EndpointsService, + private route: ActivatedRoute, ) { } ngOnInit() { @@ -35,24 +38,8 @@ export class ChartDetailsUsageComponent implements OnInit { ); } - // Show an snack bar to confirm the user that the code has been copied - showSnackBar(): void { - this.snackBar.open('Copied to the clipboard', '', { - duration: 1500 - }); - } - - get showRepoInstructions(): boolean { - return this.chart.attributes.repo.name !== 'stable'; - } - - get repoAddInstructions(): string { - return `helm repo add ${this.chart.attributes.repo.name} ${this.chart - .attributes.repo.url}`; - } - - get installInstructions(): string { - return `helm install ${this.chart.id} --version ${this.currentVersion}`; + get installUrl(): string { + return `/monocular/install/${getMonocularEndpoint(this.route, this.chart)}/${this.chart.id}/${this.currentVersion}`; } } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-versions/chart-details-versions.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-versions/chart-details-versions.component.ts index 9e17557519..553b028fc9 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-versions/chart-details-versions.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-versions/chart-details-versions.component.ts @@ -1,7 +1,9 @@ import { Component, Input } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { ChartAttributes } from '../../shared/models/chart'; import { ChartVersion } from '../../shared/models/chart-version'; +import { getMonocularEndpoint } from '../../stratos-monocular.helper'; @Component({ selector: 'app-chart-details-versions', @@ -13,9 +15,11 @@ export class ChartDetailsVersionsComponent { @Input() currentVersion: ChartVersion; showAllVersions: boolean; + constructor(private route: ActivatedRoute) { } + goToVersionUrl(version: ChartVersion): string { const chart: ChartAttributes = version.relationships.chart.data; - return `/monocular/charts/${chart.repo.name}/${chart.name}/${version.attributes.version}`; + return `/monocular/charts/${getMonocularEndpoint(this.route)}/${chart.repo.name}/${chart.name}/${version.attributes.version}`; } isSelected(version: ChartVersion): boolean { diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details.component.ts index 6e0d22df33..76262da43a 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details.component.ts @@ -6,6 +6,7 @@ import { Chart } from '../shared/models/chart'; import { ChartVersion } from '../shared/models/chart-version'; import { ChartsService } from '../shared/services/charts.service'; import { ConfigService } from '../shared/services/config.service'; +import { getMonocularEndpoint } from '../stratos-monocular.helper'; @Component({ selector: 'app-chart-details', @@ -55,6 +56,6 @@ export class ChartDetailsComponent implements OnInit { updateMetaTags(): void { } goToRepoUrl(): string { - return `/charts/${this.chart.attributes.repo.name}`; + return `/charts/${getMonocularEndpoint(null, this.chart)}/${this.chart.attributes.repo.name}`; } } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-item/chart-item.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-item/chart-item.component.spec.ts index 47188d55a5..b9512b6483 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-item/chart-item.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-item/chart-item.component.spec.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { LoggerService } from '../../../../../../core/src/core/logger.service'; import { createBasicStoreModule } from '../../../../../../store/testing/public-api'; @@ -22,7 +23,17 @@ describe('Component: ChartItem', () => { HttpClient, ConfigService, ChartsService, - LoggerService + LoggerService, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + params: { + }, + queryParams: {} + }, + } + } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-item/chart-item.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-item/chart-item.component.ts index ae51cd2620..e17d058c7d 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-item/chart-item.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-item/chart-item.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Chart } from '../shared/models/chart'; import { ChartsService } from '../shared/services/charts.service'; +import { getMonocularEndpoint } from '../stratos-monocular.helper'; @Component({ selector: 'app-chart-item', @@ -26,11 +27,11 @@ export class ChartItemComponent implements OnInit { } goToDetailUrl(): string { - return `/monocular/charts/${this.chart.attributes.repo.name}/${this.chart.attributes + return `/monocular/charts/${getMonocularEndpoint(null, this.chart)}/${this.chart.attributes.repo.name}/${this.chart.attributes .name}`; } goToRepoUrl(): string { - return `/monocular/charts/${this.chart.attributes.repo.name}`; + return `/monocular/charts/${getMonocularEndpoint(null, this.chart)}/${this.chart.attributes.repo.name}`; } } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/monocular.module.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/monocular.module.ts new file mode 100644 index 0000000000..240e637d44 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/monocular.module.ts @@ -0,0 +1,53 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { CoreModule, SharedModule } from '../../../../../core/src/public-api'; +import { ChartDetailsInfoComponent } from './chart-details/chart-details-info/chart-details-info.component'; +import { ChartDetailsReadmeComponent } from './chart-details/chart-details-readme/chart-details-readme.component'; +import { ChartDetailsUsageComponent } from './chart-details/chart-details-usage/chart-details-usage.component'; +import { ChartDetailsVersionsComponent } from './chart-details/chart-details-versions/chart-details-versions.component'; +import { ChartDetailsComponent } from './chart-details/chart-details.component'; +import { ChartIndexComponent } from './chart-index/chart-index.component'; +import { ChartItemComponent } from './chart-item/chart-item.component'; +import { ChartListComponent } from './chart-list/chart-list.component'; +import { ChartsComponent } from './charts/charts.component'; +import { ListFiltersComponent } from './list-filters/list-filters.component'; +import { ListItemComponent } from './list-item/list-item.component'; +import { LoaderComponent } from './loader/loader.component'; +import { PanelComponent } from './panel/panel.component'; +import { createMonocularProviders } from './stratos-monocular-providers.helpers'; + +const components = [ + PanelComponent, + ChartListComponent, + ChartItemComponent, + ListItemComponent, + ListFiltersComponent, + LoaderComponent, + ChartsComponent, + ChartIndexComponent, + ChartDetailsComponent, + ChartDetailsUsageComponent, + ChartDetailsVersionsComponent, + ChartDetailsReadmeComponent, + ChartDetailsInfoComponent, +]; + +@NgModule({ + imports: [ + CoreModule, + CommonModule, + SharedModule, + ], + declarations: [ + ...components, + ], + providers: [ + // Note - not really needed here, given need to bring in with a component where route with endpoint id param exists + ...createMonocularProviders() + ], + exports: [ + ...components + ] +}) +export class MonocularModule { } \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/models/chart.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/models/chart.ts index 5c2a35dac3..4953b56f06 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/models/chart.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/models/chart.ts @@ -8,6 +8,7 @@ export class Chart { links: string[]; attributes: ChartAttributes; relationships: ChartRelationships; + monocularEndpointId?: string; } @@ -29,6 +30,6 @@ class ChartRelationships { class ChartVersionRelationship { data: ChartVersionAttributes; links: { - self: string + self: string, }; } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/services/charts.service.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/services/charts.service.ts index 85cfae1317..77f23474d7 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/services/charts.service.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/services/charts.service.ts @@ -1,9 +1,11 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, throwError } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, of, throwError } from 'rxjs'; import { catchError, map, tap } from 'rxjs/operators'; import { LoggerService } from '../../../../../../../core/src/core/logger.service'; +import { getMonocularEndpoint, stratosMonocularEndpointGuid } from '../../stratos-monocular.helper'; import { Chart } from '../models/chart'; import { ChartVersion } from '../models/chart-version'; import { ConfigService } from './config.service'; @@ -19,13 +21,31 @@ export class ChartsService { constructor( private http: HttpClient, config: ConfigService, - private loggerService: LoggerService + private loggerService: LoggerService, + private route: ActivatedRoute, ) { this.hostname = `${config.backendHostname}/chartsvc`; this.cacheCharts = {}; this.hostname = '/pp/v1/chartsvc'; } + /** + * Update url to go to a monocular instance other than stratos + * These are used as img src values so won't hit our http interceptor + */ + private updateStratosUrl(chart: Chart, url: string): string { + const endpoint = getMonocularEndpoint(this.route, chart); + if (!endpoint || endpoint === stratosMonocularEndpointGuid) { + return url; + } + const parts = url.split('/'); + const chartsvcIndex = parts.findIndex(part => part === 'chartsvc'); + if (chartsvcIndex >= 0) { + parts.splice(chartsvcIndex, 0, `monocular/${endpoint}`); + } + return parts.join('/'); + } + /** * Get all charts from the API * @@ -48,7 +68,7 @@ export class ChartsService { observer.next(this.cacheCharts[repo]); }); } else { - return this.http.get<{ data: any }>(url).pipe( + return this.http.get<{ data: any, }>(url).pipe( map(r => this.extractData(r)), tap((data) => this.storeCache(data, repo)), catchError(this.handleError) @@ -119,7 +139,7 @@ export class ChartsService { * @return An observable that will be the json schema */ getChartSchema(chartVersion: ChartVersion): Observable { - return this.http.get(`${this.hostname}${chartVersion.attributes.schema}`); + return chartVersion.attributes.schema ? this.http.get(`${this.hostname}${chartVersion.attributes.schema}`) : of(null); } /** @@ -130,7 +150,7 @@ export class ChartsService { * @return An observable containing an array of ChartVersions */ getVersions(repo: string, chartName: string): Observable { - return this.http.get<{ data: any }>(`${this.hostname}/v1/charts/${repo}/${chartName}/versions`).pipe( + return this.http.get<{ data: any; }>(`${this.hostname}/v1/charts/${repo}/${chartName}/versions`).pipe( map(m => this.extractData(m)), catchError(this.handleError) ); @@ -157,7 +177,7 @@ export class ChartsService { */ getChartIconURL(chart: Chart): string { if (chart.attributes.icon) { - return `${this.hostname}${chart.attributes.icon}`; + return this.updateStratosUrl(chart, `${this.hostname}${chart.attributes.icon}`); } else { return '/core/assets/custom/placeholder.png'; } @@ -175,7 +195,7 @@ export class ChartsService { } - private extractData(res: { data: any }) { + private extractData(res: { data: any; }) { return res.data || {}; } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/services/repos.service.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/services/repos.service.ts index 9ac9582e7e..23a0880309 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/services/repos.service.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/shared/services/repos.service.ts @@ -8,7 +8,6 @@ import { RepoAttributes } from '../models/repo'; import { ConfigService } from './config.service'; - @Injectable() export class ReposService { @@ -34,7 +33,7 @@ export class ReposService { ); } - private extractData(res: { data: any }) { + private extractData(res: { data: any; }) { return res.data || {}; } diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/stratos-monocular-providers.helpers.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/stratos-monocular-providers.helpers.ts new file mode 100644 index 0000000000..ac028762ec --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/stratos-monocular-providers.helpers.ts @@ -0,0 +1,28 @@ +import { HTTP_INTERCEPTORS, HttpBackend, HttpClient, HttpInterceptor } from '@angular/common/http'; + +import { HttpInterceptingHandler, MonocularInterceptor } from '../monocular.interceptor'; +import { ChartsService } from './shared/services/charts.service'; +import { ConfigService } from './shared/services/config.service'; +import { MenuService } from './shared/services/menu.service'; +import { ReposService } from './shared/services/repos.service'; + +/** + * Helm Method to ensure http client with custom monocular interceptor is used in the monocular services + */ +export const createMonocularProviders = () => [ + ChartsService, + ConfigService, + MenuService, + ReposService, + MonocularInterceptor, + { + provide: HttpClient, + useFactory: (httpBackend: HttpBackend, interceptors: HttpInterceptor[], monocularInterceptor: MonocularInterceptor) => { + return new HttpClient(new HttpInterceptingHandler(httpBackend, [ + ...interceptors, + monocularInterceptor + ])); + }, + deps: [HttpBackend, HTTP_INTERCEPTORS, MonocularInterceptor] + } +]; \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/stratos-monocular.helper.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/stratos-monocular.helper.ts new file mode 100644 index 0000000000..7a79192d1f --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/stratos-monocular.helper.ts @@ -0,0 +1,19 @@ +import { ActivatedRoute } from '@angular/router'; + +import { Chart } from './shared/models/chart'; + + +/** + * Stratos Monocular has no concept of an endpoint (it has monocular repo endpoints...) so give it a default string + * Note - This could be the guid for the helm hub endpoint + */ +export const stratosMonocularEndpointGuid = 'default'; + +/** + * Add the monocular endpoint id to a url. This could be the helm hub endpoint guid or `default` for stratos monocular + */ +export const getMonocularEndpoint = (route?: ActivatedRoute, chart?: Chart, ifEmpty = stratosMonocularEndpointGuid) => { + const endpointFromRoute = route ? route.snapshot.params.endpoint : null; + const endpointFromChart = chart ? chart.monocularEndpointId : null; + return endpointFromRoute || endpointFromChart || ifEmpty; +}; 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 df443d3ea6..708227b098 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,31 +1,49 @@ import { OrchestratedActionBuilders } from '../../../../../store/src/entity-catalog/action-orchestrator/action-orchestrator'; -import { GetHelmChartVersions, GetHelmVersions, GetMonocularCharts, HelmInstall } from './helm.actions'; +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; +import { GetHelmChartVersions, GetHelmVersions, GetMonocularCharts, HelmInstall, HelmSynchronise } from './helm.actions'; import { HelmInstallValues } from './helm.types'; export interface HelmChartActionBuilders extends OrchestratedActionBuilders { getMultiple: () => GetMonocularCharts, // Helm install added to chart action builder and not helm release/workload to ensure action & effect are available in this module // (others may not have loaded) - install: (values: HelmInstallValues) => HelmInstall + install: (values: HelmInstallValues) => HelmInstall, + synchronise: (endpoint: EndpointModel) => HelmSynchronise; } export const helmChartActionBuilders: HelmChartActionBuilders = { getMultiple: () => new GetMonocularCharts(), - install: (values: HelmInstallValues) => new HelmInstall(values) -} + install: (values: HelmInstallValues) => new HelmInstall(values), + synchronise: (endpoint: EndpointModel) => new HelmSynchronise(endpoint) +}; export interface HelmVersionActionBuilders extends OrchestratedActionBuilders { - getMultiple: () => GetHelmVersions + getMultiple: () => GetHelmVersions; } export const helmVersionActionBuilders: HelmVersionActionBuilders = { getMultiple: () => new GetHelmVersions() -} +}; export interface HelmChartVersionsActionBuilders extends OrchestratedActionBuilders { - getMultiple: (repoName: string, chartName: string) => GetHelmChartVersions + getMultiple: ( + endpointGuid: string, + paginationKey: string, + extraArgs: { + monocularEndpoint: string, + repoName: string, + chartName: string; + }) => GetHelmChartVersions; } export const helmChartVersionsActionBuilders: HelmChartVersionsActionBuilders = { - getMultiple: (repoName: string, chartName: string) => new GetHelmChartVersions(repoName, chartName) -} + getMultiple: ( + endpointGuid: string, + paginationKey: string, + extraArgs: { + monocularEndpoint: string, + repoName: string, + chartName: string; + }) => + new GetHelmChartVersions(extraArgs.monocularEndpoint, extraArgs.repoName, extraArgs.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 df552269dd..883cdd4414 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 @@ -1,3 +1,6 @@ +import { Action } from '@ngrx/store'; + +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; import { PaginatedAction } from '../../../../../store/src/types/pagination.types'; import { EntityRequestAction } from '../../../../../store/src/types/request.types'; import { @@ -25,6 +28,8 @@ export const HELM_INSTALL = '[Helm] Install'; export const HELM_INSTALL_SUCCESS = '[Helm] Install Success'; export const HELM_INSTALL_FAILURE = '[Helm] Install Failure'; +export const HELM_SYNCHRONISE = '[Helm] Synchronise'; + export interface MonocularPaginationAction extends PaginatedAction, EntityRequestAction { } export class GetMonocularCharts implements MonocularPaginationAction { @@ -48,6 +53,15 @@ export class GetMonocularCharts implements MonocularPaginationAction { flattenPagination = true; } +export class HelmSynchronise implements Action { + public type = HELM_SYNCHRONISE; + public guid: string; + + constructor(public endpoint: EndpointModel) { + this.guid = endpoint.guid; + } +} + export class HelmInstall implements EntityRequestAction { type = HELM_INSTALL; endpointType = HELM_ENDPOINT_TYPE; @@ -80,7 +94,7 @@ export class GetHelmVersions implements MonocularPaginationAction { } export class GetHelmChartVersions implements MonocularPaginationAction { - constructor(public repoName: string, public chartName: string) { + constructor(public monocularEndpoint: string, public repoName: string, public chartName: string) { this.paginationKey = `'monocular-chart-versions-${repoName}-${chartName}`; } type = GET_MONOCULAR_CHART_VERSIONS; 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 31508d886f..440f452122 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 @@ -1,9 +1,10 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Action, Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; -import { catchError, flatMap, mergeMap } from 'rxjs/operators'; +import { combineLatest, Observable, of } from 'rxjs'; +import { catchError, flatMap, map, mergeMap, withLatestFrom } from 'rxjs/operators'; import { environment } from '../../../../../core/src/environments/environment'; import { GET_ENDPOINTS_SUCCESS, GetAllEndpointsSuccess } from '../../../../../store/src/actions/endpoint.actions'; @@ -12,6 +13,7 @@ import { AppState } from '../../../../../store/src/app-state'; import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; import { isJetstreamError } from '../../../../../store/src/jetstream'; import { ApiRequestTypes } from '../../../../../store/src/reducers/api-request-reducer/request-helpers'; +import { endpointOfTypeSelector } from '../../../../../store/src/selectors/endpoint.selectors'; import { NormalizedResponse } from '../../../../../store/src/types/api.types'; import { EntityRequestAction, @@ -20,7 +22,9 @@ import { WrapperRequestActionSuccess, } from '../../../../../store/src/types/request.types'; import { helmEntityCatalog } from '../helm-entity-catalog'; -import { getHelmVersionId, getMonocularChartId, HELM_ENDPOINT_TYPE } from '../helm-entity-factory'; +import { getHelmVersionId, getMonocularChartId, HELM_ENDPOINT_TYPE, HELM_HUB_ENDPOINT_TYPE } from '../helm-entity-factory'; +import { Chart } from '../monocular/shared/models/chart'; +import { stratosMonocularEndpointGuid } from '../monocular/stratos-monocular.helper'; import { GET_HELM_VERSIONS, GET_MONOCULAR_CHART_VERSIONS, @@ -29,17 +33,60 @@ import { GetHelmVersions, GetMonocularCharts, HELM_INSTALL, + HELM_SYNCHRONISE, HelmInstall, + HelmSynchronise, } from './helm.actions'; import { HelmVersion } from './helm.types'; +type MonocularChartsResponse = { + data: Chart[]; +}; + +const mapMonocularChartResponse = (entityKey: string, response: MonocularChartsResponse): NormalizedResponse => { + const base: NormalizedResponse = { + entities: { [entityKey]: {} }, + result: [] + }; + + const items = response.data as Array; + const processedData: NormalizedResponse = 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; +}; + +const mergeMonocularChartResponses = (entityKey: string, responses: MonocularChartsResponse[]): NormalizedResponse => { + const combined = responses.reduce((res, response) => { + res.data = res.data.concat(response.data); + return res; + }, { data: [] }); + return mapMonocularChartResponse(entityKey, combined); +}; + +const addMonocularId = (endpointId: string, response: MonocularChartsResponse): MonocularChartsResponse => { + const data = response.data.map(chart => ({ + ...chart, + monocularEndpointId: endpointId + })); + return { + data + }; +}; + @Injectable() export class HelmEffects { constructor( private httpClient: HttpClient, private actions$: Actions, - private store: Store + private store: Store, + public snackBar: MatSnackBar, ) { } // Endpoints that we know are synchronizing @@ -53,7 +100,7 @@ export class HelmEffects { updateOnSyncFinished$ = this.actions$.pipe( ofType(GET_ENDPOINTS_SUCCESS), flatMap(action => { - // Look to see if we have any endpoints that are sycnhronizing + // Look to see if we have any endpoints that are synchronizing let updated = false; Object.values(action.payload.entities.stratosEndpoint).forEach(endpoint => { if (endpoint.cnsi_type === HELM_ENDPOINT_TYPE && endpoint.endpoint_metadata) { @@ -78,25 +125,43 @@ export class HelmEffects { @Effect() fetchCharts$ = this.actions$.pipe( ofType(GET_MONOCULAR_CHARTS), - flatMap(action => { + withLatestFrom(this.store), + flatMap(([action, appState]) => { const entityKey = entityCatalog.getEntityKey(action); - return this.makeRequest(action, `/pp/${this.proxyAPIVersion}/chartsvc/v1/charts`, (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; - }, []); + this.store.dispatch(new StartRequestAction(action)); + + const helmEndpoints = Object.values(endpointOfTypeSelector(HELM_ENDPOINT_TYPE)(appState)); + const helmHubEndpoint = helmEndpoints.find(endpoint => endpoint.sub_type === HELM_HUB_ENDPOINT_TYPE); + + // See https://github.com/SUSE/stratos/issues/466. It would be better to use the standard proxy for this request and go out to all + // valid helm sub types + const stratosMonocular = this.httpClient.get(`/pp/${this.proxyAPIVersion}/chartsvc/v1/charts`); + const helmHubMonocular = helmHubEndpoint ? this.createHelmHubRequest(helmHubEndpoint.guid) : of({ data: [] }); + + return combineLatest([ + stratosMonocular, + helmHubMonocular + ]).pipe( + map(res => mergeMonocularChartResponses(entityKey, res)), + mergeMap((response: NormalizedResponse) => [new WrapperRequestActionSuccess(response, action)]), + catchError(error => { + const { status, message } = HelmEffects.createHelmError(error); + const endpointIds = helmEndpoints.map(e => e.guid); + if (helmHubEndpoint) { + endpointIds.push(helmHubEndpoint.guid); + } + return [ + new WrapperRequestActionFailed(message, action, 'fetch', { + endpointIds, + url: null, + eventCode: status, + message, + error + }) + ]; + }) + ); }) ); @@ -106,10 +171,10 @@ export class HelmEffects { flatMap(action => { const entityKey = entityCatalog.getEntityKey(action); return this.makeRequest(action, `/pp/${this.proxyAPIVersion}/helm/versions`, (response) => { - const processedData = { + const processedData: NormalizedResponse = { entities: { [entityKey]: {} }, result: [] - } as NormalizedResponse; + }; // Go through each endpoint ID Object.keys(response).forEach(endpoint => { @@ -136,23 +201,25 @@ export class HelmEffects { 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; + (response) => { + const base: NormalizedResponse = { + entities: { [entityKey]: {} }, + result: [] + }; - 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; - }, []); + 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; + }, [], { + 'x-cap-cnsi-list': action.monocularEndpoint !== stratosMonocularEndpointGuid ? action.monocularEndpoint : '' + }); }) ); @@ -187,6 +254,26 @@ export class HelmEffects { }) ); + @Effect() + helmSynchronise$ = this.actions$.pipe( + ofType(HELM_SYNCHRONISE), + flatMap(action => { + const requestArgs = { + headers: null, + params: null + }; + const proxyAPIVersion = environment.proxyAPIVersion; + const url = `/pp/${proxyAPIVersion}/chartrepos/${action.endpoint.guid}`; + const req = this.httpClient.post(url, requestArgs); + req.subscribe(ok => { + this.snackBar.open('Helm Repository synchronization started', 'Dismiss', { duration: 3000 }); + }, err => { + this.snackBar.open(`Failed to Synchronize Helm Repository '${action.endpoint.name}'`, 'Dismiss', { duration: 5000 }); + }); + return []; + }) + ); + private static createHelmErrorMessage(err: any): string { if (err) { if (err.error && err.error.message) { @@ -222,15 +309,24 @@ export class HelmEffects { }; } + private createHelmHubRequest(endpointId: string): Observable { + return this.httpClient.get(`/pp/${this.proxyAPIVersion}/chartsvc/v1/charts`, { + headers: { + 'x-cap-cnsi-list': endpointId + } + }).pipe(map(res => addMonocularId(endpointId, res))); + } + private makeRequest( action: EntityRequestAction, url: string, mapResult: (response: any) => NormalizedResponse, - endpointIds: string[] + endpointIds: string[], + headers = {} ): Observable { this.store.dispatch(new StartRequestAction(action)); const requestArgs = { - headers: null, + headers, params: null }; return this.httpClient.get(url, requestArgs).pipe( 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 4ddf46021c..7c9c4afa9c 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,5 +1,5 @@ -import { Chart } from './../monocular/shared/models/chart'; -import { ChartVersion } from './../monocular/shared/models/chart-version'; +import { Chart } from '../monocular/shared/models/chart'; +import { ChartVersion } from '../monocular/shared/models/chart-version'; export interface MonocularRepository { name: string; @@ -47,10 +47,26 @@ export enum HelmStatus { export interface HelmInstallValues { endpoint: string; + monocularEndpoint: string; releaseName: string; releaseNamespace: string; values: string; - chart: string; + chart: { + name: string; + repo: string; + version: string; + }; +} + +export interface HelmUpgradeValues { + values: string; + chart: { + name: string; + repo: string; + version: string; + }; + restartPods?: boolean; + monocularEndpoint?: string; } export interface HelmUpgradeValues { diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.html b/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.html deleted file mode 100644 index dd2b6cb351..0000000000 --- a/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.spec.ts deleted file mode 100644 index 57e2407ba1..0000000000 --- a/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HelmBaseTestModules } from '../../helm-testing.module'; -import { RepositoryTabComponent } from './repository-tab.component'; - -describe('RepositoryTabComponent', () => { - let component: RepositoryTabComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - ...HelmBaseTestModules - ], - declarations: [RepositoryTabComponent] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(RepositoryTabComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.ts deleted file mode 100644 index cea4439854..0000000000 --- a/src/frontend/packages/suse-extensions/src/custom/helm/tabs/repository-tab/repository-tab.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; - -import { ListConfig } from '../../../../../../core/src/shared/components/list/list.component.types'; -import { MonocularRepositoryListConfig } from '../../list-types/monocular-repository-list-config.service'; - -@Component({ - selector: 'app-repository-tab', - templateUrl: './repository-tab.component.html', - styleUrls: ['./repository-tab.component.scss'], - providers: [{ - provide: ListConfig, - useClass: MonocularRepositoryListConfig, - }] -}) -export class RepositoryTabComponent { - -} 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 749d08e560..99899a0b92 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 @@ -3,6 +3,7 @@ import { combineLatest, Observable } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { helmEntityCatalog } from '../../../../helm/helm-entity-catalog'; +import { ChartAttributes } from '../../../../helm/monocular/shared/models/chart'; import { ChartMetadata } from '../../../../helm/store/helm.types'; import { kubeEntityCatalog } from '../../../kubernetes-entity-catalog'; import { ContainerStateCollection, KubernetesPod } from '../../../store/kube.types'; @@ -60,6 +61,13 @@ class Version { } } +type InternalHelmUpgrade = { + release: HelmRelease, + upgrade: ChartAttributes, + version: string, + monocularEndpointId: string; +}; + @Injectable() export class HelmReleaseHelperService { @@ -206,7 +214,7 @@ export class HelmReleaseHelperService { return false; } - public hasUpgrade(returnLatest = false): Observable { + public hasUpgrade(returnLatest = false): Observable { const updates = combineLatest(this.getCharts(), this.release$); return updates.pipe( map(([charts, release]) => { @@ -219,7 +227,8 @@ export class HelmReleaseHelperService { return { release, upgrade: c.attributes, - version: c.relationships.latestChartVersion.data.version + version: c.relationships.latestChartVersion.data.version, + monocularEndpointId: c.monocularEndpointId }; } } @@ -233,8 +242,9 @@ export class HelmReleaseHelperService { return { release, upgrade: releaseChart.attributes, - version: releaseChart.relationships.latestChartVersion.data.version - } + version: releaseChart.relationships.latestChartVersion.data.version, + monocularEndpointId: releaseChart.monocularEndpointId + }; } } return null; 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 a572f14604..7c235a2766 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 @@ -163,7 +163,7 @@ export class WorkloadsEffects { }) ); -private mapHelmRelease(data, endpointId, guid: string) { + private mapHelmRelease(data, endpointId, guid: string) { const helmRelease: HelmRelease = { ...data, endpointId @@ -201,7 +201,7 @@ private mapHelmRelease(data, endpointId, guid: string) { message: errorMessage, error }) - ] + ]; }) ); } 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 index f98ddea450..204cc8b66a 100644 --- 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 @@ -1,15 +1,11 @@ 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 { 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'; @@ -26,13 +22,19 @@ export class HelmReleaseVersionsDataSource extends ListDataSource object.id, - paginationKey: helmEntityCatalog.chartVersions.actions.getMultiple(repoName, chartName).paginationKey, + action, + schema: action.entity[0], + getRowUniqueId: (object: MonocularVersion) => action.entity[0].getId(object), + paginationKey: action.paginationKey, isLocal: true, transformEntities: [ (entities: MonocularVersion[], paginationState: PaginationEntityState) => this.endpointTypeFilter(entities, paginationState) @@ -45,7 +47,7 @@ export class HelmReleaseVersionsDataSource extends ListDataSource = (entities: MonocularVersion[], paginationState: PaginationEntityState) => { + public endpointTypeFilter(entities: MonocularVersion[], paginationState: PaginationEntityState): MonocularVersion[] { if ( !paginationState.clientPagination || !paginationState.clientPagination.filter || @@ -55,7 +57,7 @@ export class HelmReleaseVersionsDataSource extends ListDataSource e.attributes.version.indexOf('-') === -1); + 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 index 6d712da1d5..fe811a1a78 100644 --- 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 @@ -22,7 +22,7 @@ import { HelmReleaseVersionsDataSource } from './release-version-data-source'; const typeFilterKey = 'versionType'; -export class ReleaseUpgradeVersionsListConfig implements IListConfig { +export class ReleaseUpgradeVersionsListConfig implements IListConfig { public versionsDataSource: ListDataSource; @@ -32,7 +32,7 @@ export class ReleaseUpgradeVersionsListConfig implements IListConfig { getMultiActions: () => IMultiListAction[]; getSingleActions: () => IListAction[]; - columns: Array> = [ + columns: Array> = [ { columnId: 'radio', headerCell: () => '', @@ -80,33 +80,34 @@ export class ReleaseUpgradeVersionsListConfig implements IListConfig { repoName: string, chartName: string, version: string, + monocularEndpoint: string ) { this.getGlobalActions = () => []; this.getMultiActions = () => []; this.getSingleActions = () => []; - this.versionsDataSource = new HelmReleaseVersionsDataSource(store, this, repoName, chartName, version); + this.versionsDataSource = new HelmReleaseVersionsDataSource(store, this, repoName, chartName, version, monocularEndpoint); 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) - }]; + 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(() => { @@ -122,7 +123,7 @@ export class ReleaseUpgradeVersionsListConfig implements IListConfig { private getFirstNonDevelopmentVersion(rows: MonocularVersion[]): MonocularVersion { for (const mv of rows) { if (mv.attributes.version.indexOf('-') === -1) { - return mv + return mv; } } return rows[0]; 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 index 8ef8a83118..34f8ef5c45 100644 --- 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 @@ -7,7 +7,8 @@ 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 { stratosMonocularEndpointGuid } from '../../../helm/monocular/stratos-monocular.helper'; +import { HelmUpgradeValues, MonocularVersion } 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'; @@ -33,15 +34,20 @@ import { ReleaseUpgradeVersionsListConfig } from './release-version-list-config' export class UpgradeReleaseComponent { public cancelUrl; - public listConfig; + public listConfig: ReleaseUpgradeVersionsListConfig; public validate$: Observable; - private version; + private version: MonocularVersion; public overrides: FormGroup; + private monocularEndpointId: string; + // Future public showAdvancedOptions = false; - constructor(store: Store, public helper: HelmReleaseHelperService) { + constructor( + store: Store, + public helper: HelmReleaseHelperService + ) { this.cancelUrl = `/workloads/${this.helper.guid}`; @@ -57,7 +63,8 @@ export class UpgradeReleaseComponent { 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); + this.listConfig = new ReleaseUpgradeVersionsListConfig(store, repoName, name, version, chart.monocularEndpointId); + this.monocularEndpointId = chart.monocularEndpointId; // First step is valid when a version has been selected this.validate$ = this.listConfig.versionsDataSource.selectedRows$.pipe( @@ -79,36 +86,37 @@ export class UpgradeReleaseComponent { 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 ) { + if (this.showAdvancedOptions && index === 1) { return of({ success: true }); } const values: HelmUpgradeValues = { - ...this.overrides.value, + values: this.overrides.controls.values.value, restartPods: false, chart: { name: this.version.relationships.chart.data.name, repo: this.version.relationships.chart.data.repo.name, version: this.version.attributes.version, }, + monocularEndpoint: this.monocularEndpointId === stratosMonocularEndpointGuid ? null : this.monocularEndpointId }; // 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 - })) - ); - } + // 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/workloads.routing.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts index a7732ac7a5..a9773315f8 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 @@ -28,6 +28,7 @@ const routes: Routes = [ pathMatch: 'full', }, { + // guid = kube endpoint path: ':guid/upgrade', component: UpgradeReleaseComponent, pathMatch: 'full', diff --git a/src/jetstream/datastore/20200902162200_HelmSubtype.go b/src/jetstream/datastore/20200902162200_HelmSubtype.go new file mode 100644 index 0000000000..ebb97be983 --- /dev/null +++ b/src/jetstream/datastore/20200902162200_HelmSubtype.go @@ -0,0 +1,21 @@ +package datastore + +import ( + "database/sql" + + "bitbucket.org/liamstask/goose/lib/goose" +) + +func init() { + RegisterMigration(20200902162200, "HelmSubtype", func(txn *sql.Tx, conf *goose.DBConf) error { + + // Make sure all previous helm endpoints type shave the correct 'repo' sub type + updateHelmRepoSubtype := "UPDATE cnsis SET sub_type='repo' WHERE cnsi_type='helm';" + _, err := txn.Exec(updateHelmRepoSubtype) + if err != nil { + return err + } + + return nil + }) +} diff --git a/src/jetstream/passthrough.go b/src/jetstream/passthrough.go index 4a6cfeeba0..b104ba5fed 100644 --- a/src/jetstream/passthrough.go +++ b/src/jetstream/passthrough.go @@ -221,7 +221,7 @@ func (p *portalProxy) proxy(c echo.Context) error { } func (p *portalProxy) ProxyRequest(c echo.Context, uri *url.URL) (map[string]*interfaces.CNSIRequest, error) { - log.Debug("proxy") + log.Debug("ProxyRequest") cnsiList := strings.Split(c.Request().Header.Get("x-cap-cnsi-list"), ",") shouldPassthrough := "true" == c.Request().Header.Get("x-cap-passthrough") longRunning := "true" == c.Request().Header.Get(longRunningTimeoutHeader) diff --git a/src/jetstream/plugins/kubernetes/install_release.go b/src/jetstream/plugins/kubernetes/install_release.go index 15cf67623e..575c0b7065 100644 --- a/src/jetstream/plugins/kubernetes/install_release.go +++ b/src/jetstream/plugins/kubernetes/install_release.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" - "github.com/helm/monocular/chartsvc" "github.com/labstack/echo" log "github.com/sirupsen/logrus" "sigs.k8s.io/yaml" @@ -24,11 +23,12 @@ import ( const chartCollection = "charts" type installRequest struct { - Endpoint string `json:"endpoint"` - Name string `json:"releaseName"` - Namespace string `json:"releaseNamespace"` - Values string `json:"values"` - Chart struct { + Endpoint string `json:"endpoint"` + MonocularEndpoint string `json:"monocularEndpoint"` + Name string `json:"releaseName"` + Namespace string `json:"releaseNamespace"` + Values string `json:"values"` + Chart struct { Name string `json:"chartName"` Repository string `json:"repo"` Version string `json:"version"` @@ -36,17 +36,19 @@ type installRequest struct { } type upgradeRequest struct { - Values string `json:"values"` - Chart struct { + MonocularEndpoint string `json:"monocularEndpoint"` + Values string `json:"values"` + Chart struct { Name string `json:"name"` Repository string `json:"repo"` Version string `json:"version"` } `json:"chart"` + RestartPods bool `json:"restartPods"` } // Monocular is a plugin for Monocular type Monocular interface { - GetChartStore() *chartsvc.ChartSvcDatastore + GetChartDownloadUrl(monocularEndpoint, chartID, chartVersion string) (string, error) } // InstallRelease will install a Helm 3 release @@ -61,7 +63,7 @@ func (c *KubernetesSpecification) InstallRelease(ec echo.Context) error { return interfaces.NewJetstreamErrorf("Could not get Create Release Parameters: %v+", err) } - chart, err := c.loadChart(params.Chart.Repository, params.Chart.Name, params.Chart.Version) + chart, err := c.loadChart(params.MonocularEndpoint, params.Chart.Repository, params.Chart.Name, params.Chart.Version) if err != nil { return interfaces.NewJetstreamErrorf("Could not load chart: %v+", err) } @@ -110,7 +112,7 @@ func (c *KubernetesSpecification) InstallRelease(ec echo.Context) error { return ec.JSON(200, release) } -func (c *KubernetesSpecification) getChart(chartID, version string) (string, error) { +func (c *KubernetesSpecification) getChart(monocularEndpoint, chartID, version string) (string, error) { helm := c.portalProxy.GetPlugin("monocular") if helm == nil { return "", errors.New("Could not find monocular plugin") @@ -121,31 +123,16 @@ func (c *KubernetesSpecification) getChart(chartID, version string) (string, err return "", errors.New("Could not find monocular plugin interface") } - store := monocular.GetChartStore() - chart, err := store.GetChart(chartID) - if err != nil { - return "", errors.New("Could not find Chart") - } - - // Find the download URL for the version - for _, chartVersion := range chart.ChartVersions { - if chartVersion.Version == version { - if len(chartVersion.URLs) == 1 { - return chartVersion.URLs[0], nil - } - } - } - - return "", errors.New("Could not find Chart Version") + return monocular.GetChartDownloadUrl(monocularEndpoint, chartID, version) } // Load the Helm chart for the given repository, name and version -func (c *KubernetesSpecification) loadChart(repo, name, version string) (*chart.Chart, error) { +func (c *KubernetesSpecification) loadChart(monocularEndpoint, repo, name, version string) (*chart.Chart, error) { chartID := fmt.Sprintf("%s/%s", repo, name) - downloadURL, err := c.getChart(chartID, version) + downloadURL, err := c.getChart(monocularEndpoint, chartID, version) if err != nil { - return nil, fmt.Errorf("Could not get the Download URL for the Helm Chart") + return nil, fmt.Errorf("Could not get the Download URL for the Helm Chart: %+v", err) } log.Debugf("Helm Chart Download URL: %s", downloadURL) @@ -238,7 +225,7 @@ func (c *KubernetesSpecification) UpgradeRelease(ec echo.Context) error { defer hc.Cleanup() - chart, err := c.loadChart(params.Chart.Repository, params.Chart.Name, params.Chart.Version) + chart, err := c.loadChart(params.MonocularEndpoint, params.Chart.Repository, params.Chart.Name, params.Chart.Version) if err != nil { return interfaces.NewJetstreamErrorf("Could not load chart for upgrade: %+v", err) } diff --git a/src/jetstream/plugins/metrics/main.go b/src/jetstream/plugins/metrics/main.go index b04ae68f3f..535e52706d 100644 --- a/src/jetstream/plugins/metrics/main.go +++ b/src/jetstream/plugins/metrics/main.go @@ -348,9 +348,7 @@ func (m *MetricsSpecification) UpdateMetadata(info *interfaces.Info, userGUID st for _, values := range info.Endpoints { for _, endpoint := range values { // Look to see if we can find the metrics provider for this URL - log.Debugf("Processing endpoint: %+v", endpoint) log.Debugf("Processing endpoint: %+v", endpoint.CNSIRecord) - if provider, ok := hasMetricsProvider(metricsProviders, endpoint.DopplerLoggingEndpoint); ok { endpoint.Metadata["metrics"] = provider.EndpointGUID endpoint.Metadata["metrics_job"] = provider.Job diff --git a/src/jetstream/plugins/monocular/main.go b/src/jetstream/plugins/monocular/main.go index 0a6b38438e..01f12dc905 100644 --- a/src/jetstream/plugins/monocular/main.go +++ b/src/jetstream/plugins/monocular/main.go @@ -1,14 +1,17 @@ package monocular import ( + "encoding/json" "errors" "fmt" + "io/ioutil" "math/rand" "net/http" "os" "os/exec" "path/filepath" "strings" + "time" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" "github.com/labstack/echo" @@ -16,10 +19,15 @@ import ( "github.com/helm/monocular/chartsvc" "github.com/helm/monocular/chartsvc/foundationdb" + "github.com/helm/monocular/chartsvc/models" + "github.com/helm/monocular/chartsvc/utils" ) const ( helmEndpointType = "helm" + helmHubEndpointType = "hub" + helmRepoEndpointType = "repo" + stratosPrefix = "/pp/v1/" prefix = "/pp/v1/chartsvc/" kubeReleaseNameEnvVar = "STRATOS_HELM_RELEASE" foundationDBURLEnvVar = "FDB_URL" @@ -41,16 +49,20 @@ type Monocular struct { devSyncPID int } +type HelmHubChart struct { + utils.ApiResponse + Attributes *models.ChartVersion `json:"attributes"` +} + +type HelmHubChartResponse struct { + Data HelmHubChart `json:"data"` +} + // Init creates a new Monocular func Init(portalProxy interfaces.PortalProxy) (interfaces.StratosPlugin, error) { return &Monocular{portalProxy: portalProxy}, nil } -// GetChartStore gets the chart store -func (m *Monocular) GetChartStore() *chartsvc.ChartSvcDatastore { - return m.RepoQueryStore -} - // Init performs plugin initialization func (m *Monocular) Init() error { log.Debug("Monocular init .... ") @@ -135,26 +147,31 @@ func (m *Monocular) syncOnStartup() { // Get the repositories that we currently have repos, err := foundationdb.ListRepositories() if err != nil { - log.Errorf("Chart Repostiory Startup: Unable to sync repositories: %v+", err) + log.Errorf("Chart Repository Startup: Unable to sync repositories: %v+", err) return } // Get all of the helm endpoints endpoints, err := m.portalProxy.ListEndpoints() if err != nil { - log.Errorf("Chart Repostiory Startup: Unable to sync repositories: %v+", err) + log.Errorf("Chart Repository Startup: Unable to sync repositories: %v+", err) return } helmRepos := make([]string, 0) for _, ep := range endpoints { if ep.CNSIType == helmEndpointType { - helmRepos = append(helmRepos, ep.Name) - - // Is this an endpoint that we don't have charts for ? - if !arrayContainsString(repos, ep.Name) { - log.Infof("Syncing helm repository to chart store: %s", ep.Name) - m.Sync(interfaces.EndpointRegisterAction, ep) + if ep.SubType == helmRepoEndpointType { + helmRepos = append(helmRepos, ep.Name) + + // Is this an endpoint that we don't have charts for ? + if !arrayContainsString(repos, ep.Name) { + log.Infof("Syncing helm repository to chart store: %s", ep.Name) + m.Sync(interfaces.EndpointRegisterAction, ep) + } + } else { + metadata := "{}" + m.portalProxy.UpdateEndpointMetadata(ep.GUID, metadata) } } } @@ -195,7 +212,7 @@ func (m *Monocular) ConfigureChartSVC(fdbURL *string, fDB *string, cACertFile st } func (m *Monocular) OnEndpointNotification(action interfaces.EndpointAction, endpoint *interfaces.CNSIRecord) { - if endpoint.CNSIType == helmEndpointType { + if endpoint.CNSIType == helmEndpointType && endpoint.SubType == helmRepoEndpointType { m.Sync(action, endpoint) } } @@ -222,15 +239,57 @@ func (m *Monocular) AddAdminGroupRoutes(echoGroup *echo.Group) { // AddSessionGroupRoutes adds the session routes for this plugin to the Echo server func (m *Monocular) AddSessionGroupRoutes(echoGroup *echo.Group) { + // Requests to Monocular Instances + echoGroup.Any("/chartsvc/*", m.handleAPI) + // Reach out to a monocular instance other than Stratos (like helm hub). This is usually done via `x-cap-cnsi-list` + // however cannot be done for things like img src + echoGroup.Any("/monocular/:guid/chartsvc/*", m.handleMonocularInstance) + // API for Helm Chart Repositories echoGroup.GET("/chartrepos", m.ListRepos) - echoGroup.Any("/chartsvc/*", m.handleAPI) echoGroup.POST("/chartrepos/status", m.GetRepoStatuses) echoGroup.POST("/chartrepos/:guid", m.SyncRepo) } +// isExternalMonocularRequest .. Should this request go out to an external monocular instance? IF so returns external monocular endpoint +func (m *Monocular) isExternalMonocularRequest(c echo.Context) (*interfaces.CNSIRecord, error) { + cnsiList := strings.Split(c.Request().Header.Get("x-cap-cnsi-list"), ",") + + // If this has a cnsi then test if it for an external monocular instance + if len(cnsiList) == 1 && len(cnsiList[0]) > 0 { + return m.validateExternalMonocularEndpoint(cnsiList[0]) + } + + return nil, nil +} + +// validateExternalMonocularEndpoint .. Is this endpoint related to an external moncular instance (not stratos's) +func (m *Monocular) validateExternalMonocularEndpoint(cnsi string) (*interfaces.CNSIRecord, error) { + endpoint, err := m.portalProxy.GetCNSIRecord(cnsi) + if err != nil { + err := errors.New("Failed to fetch endpoint") + return nil, echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + if endpoint.CNSIType == helmEndpointType && endpoint.SubType != helmRepoEndpointType { + return &endpoint, nil + } + + return nil, nil +} + // Forward requests to the Chart Service API func (m *Monocular) handleAPI(c echo.Context) error { + externalMonocularEndpoint, err := m.isExternalMonocularRequest(c) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // If this request is associated with an external monocular instance forward the request on to it + if externalMonocularEndpoint != nil { + return m.baseHandleMonocularInstance(c, externalMonocularEndpoint) + } + // Modify the path to remove our prefix for the Chart Service API path := c.Request().URL.Path log.Debugf("URL to chartsvc requested: %v", path) @@ -242,3 +301,150 @@ func (m *Monocular) handleAPI(c echo.Context) error { m.chartSvcRoutes.ServeHTTP(c.Response().Writer, c.Request()) return nil } + +func (m *Monocular) handleMonocularInstance(c echo.Context) error { + log.Debug("handleMonocularInstance") + guid := c.Param("guid") + monocularEndpoint, err := m.validateExternalMonocularEndpoint(guid) + if monocularEndpoint == nil || err != nil { + err := errors.New("No monocular endpoint") + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return m.baseHandleMonocularInstance(c, monocularEndpoint) +} + +func removeBreakingHeaders(oldRequest, emptyRequest *http.Request) { + for k, v := range oldRequest.Header { + switch { + // Skip these + // - "Referer" causes CF to fail with a 403 + // - "Connection", "X-Cap-*" and "Cookie" are consumed by us + // - "Accept-Encoding" must be excluded otherwise the transport will expect us to handle the encoding/compression + // - X-Forwarded-* headers - these will confuse Cloud Foundry in some cases (e.g. load balancers) + case k == "Connection", k == "Cookie", k == "Referer", k == "Accept-Encoding", + strings.HasPrefix(strings.ToLower(k), "x-cap-"), + strings.HasPrefix(strings.ToLower(k), "x-forwarded-"): + + // Forwarding everything else + default: + emptyRequest.Header[k] = v + } + } +} + +// baseHandleMonocularInstance .. Forward request to monocular of endpoint +func (m *Monocular) baseHandleMonocularInstance(c echo.Context, monocularEndpoint *interfaces.CNSIRecord) error { + log.Debug("baseHandleMonocularInstance") + // Generic proxy is handled last, after plugins. + + if monocularEndpoint == nil { + err := errors.New("No monocular endpoint") + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // We should be able to use `DoProxySingleRequest`, which goes through to `doRequest`, however the actual forwarded request is handled + // by the 'authHandler' associated with the endpoint OR defaults to an OAuth request. For this case there's no auth at all so falls over. + // Tracked in https://github.com/SUSE/stratos/issues/466 + + url := monocularEndpoint.APIEndpoint + path := c.Request().URL.Path + log.Debug("URL to monocular requested: %v", path) + if strings.Index(path, stratosPrefix) == 0 { + // drop stratos pp/v1 + path = path[len(stratosPrefix)-1:] + + // drop leading slash + if path[0] == '/' { + path = path[1:] + } + + // drop monocular/:guid + parts := strings.Split(path, "/") + if parts[0] == "monocular" { + parts = parts[2:] + } + + // Bring all back together + url.Path += "/" + strings.Join(parts, "/") + } + log.Debugf("URL to monocular: %v", url.String()) + + req, err := http.NewRequest(c.Request().Method, url.String(), nil) + + removeBreakingHeaders(c.Request(), req) + + client := &http.Client{Timeout: 30 * time.Second} + res, err := client.Do(req) + + if err != nil { + c.Response().Status = 500 + c.Response().Write([]byte(err.Error())) + } else if res.Body != nil { + c.Response().Status = res.StatusCode + c.Response().Header().Set("Content-Type", res.Header.Get("Content-Type")) + body, _ := ioutil.ReadAll(res.Body) + c.Response().Write(body) + defer res.Body.Close() + } else { + c.Response().Status = 200 + } + + return nil +} + +// GetChartDownloadUrl ... Get the download url for the bits required to install the given chart +func (m *Monocular) GetChartDownloadUrl(monocularEndpoint, chartID, version string) (string, error) { + if len(monocularEndpoint) > 0 { + // Fetch the monocular endpoint for the url + endpoint, err := m.validateExternalMonocularEndpoint(monocularEndpoint) + if err != nil { + return "", err + } + url := endpoint.APIEndpoint + + // Fetch the chart, this will give us the url to download the bits + url.Path += "/chartsvc/v1/charts/" + chartID + "/versions/" + version + req, err := http.NewRequest(http.MethodGet, url.String(), nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + client := &http.Client{Timeout: 30 * time.Second} + res, err := client.Do(req) + if err != nil { + return "", err + } else if res.StatusCode >= 400 { + return "", fmt.Errorf("Couldn't download monocular chart (%+v) from '%+v'", res.StatusCode, req.URL) + } else if res.Body != nil { + body, _ := ioutil.ReadAll(res.Body) + defer res.Body.Close() + + // Reach into the chart response for the download URL + chartVersionResponse := &HelmHubChartResponse{} + err := json.Unmarshal(body, chartVersionResponse) + if err != nil { + return "", err + } + if len(chartVersionResponse.Data.Attributes.URLs) < 1 { + return "", errors.New("Response contained no chart package urls") + } + return chartVersionResponse.Data.Attributes.URLs[0], err + } else { + return "", errors.New("No body in response to chart request") + } + } else { + store := m.RepoQueryStore + chart, err := store.GetChart(chartID) + if err != nil { + return "", errors.New("Could not find Chart") + } + + // Find the download URL for the version + for _, chartVersion := range chart.ChartVersions { + if chartVersion.Version == version { + if len(chartVersion.URLs) == 1 { + return chartVersion.URLs[0], nil + } + } + } + return "", errors.New("Could not find Chart Version") + } +}