From 437105c542216eec3f82b355291f7338bb4c99d5 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 22 Sep 2020 10:55:21 +0100 Subject: [PATCH 1/7] Fix issue where chart download link was not absolute - Expected url, got chart file name - We now check for this, if url is not absoulte assume filename and append to repo url --- .../src/custom/helm/store/helm.types.ts | 1 + .../create-release.component.ts | 6 ++- .../upgrade-release.component.ts | 5 +- .../kubernetes/workloads/workload.utils.ts | 7 +++ .../plugins/kubernetes/install_release.go | 47 +++++++++++++++++-- 5 files changed, 59 insertions(+), 7 deletions(-) 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 fc79d7039c..6f804d52a7 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 @@ -56,6 +56,7 @@ export interface HelmUpgradeInstallValues { values: string; chart: HelmChartReference; chartUrl: string; + repoUrl: string; } export interface HelmInstallValues extends HelmUpgradeInstallValues { diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts index 59d4a9d1d7..544d2c492e 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts @@ -19,7 +19,7 @@ import { HelmChartReference, HelmInstallValues } from '../../../helm/store/helm. import { kubeEntityCatalog } from '../../kubernetes-entity-catalog'; import { KUBERNETES_ENDPOINT_TYPE } from '../../kubernetes-entity-factory'; import { KubernetesNamespace } from '../../store/kube.types'; -import { getFirstChartUrl } from '../workload.utils'; +import { getChartRepoUrl, getFirstChartUrl } from '../workload.utils'; import { ChartValuesConfig, ChartValuesEditorComponent } from './../chart-values-editor/chart-values-editor.component'; @Component({ @@ -242,6 +242,10 @@ export class CreateReleaseComponent implements OnInit, OnDestroy { if (values.chartUrl.length === 0) { throw new Error('Could not get Chart URL'); } + values.repoUrl = getChartRepoUrl(chartInfo); + if (values.repoUrl.length === 0) { + throw new Error('Could not get Repo URL'); + } // Make the request return helmEntityCatalog.chart.api.install(values).pipe( // Wait for result of request 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 43d7af5f25..6426a56e38 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 @@ -15,7 +15,7 @@ import { HelmUpgradeValues, MonocularVersion } from '../../../helm/store/helm.ty import { ChartValuesConfig, ChartValuesEditorComponent } from '../chart-values-editor/chart-values-editor.component'; import { HelmReleaseHelperService } from '../release/tabs/helm-release-helper.service'; import { HelmReleaseGuid } from '../workload.types'; -import { getFirstChartUrl } from '../workload.utils'; +import { getChartRepoUrl, getFirstChartUrl } from '../workload.utils'; import { workloadsEntityCatalog } from './../workloads-entity-catalog'; import { ReleaseUpgradeVersionsListConfig } from './release-version-list-config'; @@ -129,7 +129,8 @@ export class UpgradeReleaseComponent { version: this.version.attributes.version, }, monocularEndpoint: this.monocularEndpointId === stratosMonocularEndpointGuid ? null : this.monocularEndpointId, - chartUrl: getFirstChartUrl(this.version) + chartUrl: getFirstChartUrl(this.version), + repoUrl: getChartRepoUrl(this.version) }; // Make the request diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.utils.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.utils.ts index 7c8fb71baa..a5ab5afa58 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.utils.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.utils.ts @@ -7,3 +7,10 @@ export function getFirstChartUrl(chart: ChartVersion): string { } return ''; } + +export function getChartRepoUrl(chart: ChartVersion): string { + if (chart && chart.relationships && chart.relationships.chart && chart.relationships.chart.data && chart.relationships.chart.data.repo) { + return chart.relationships.chart.data.repo.url; + } + return ''; +} diff --git a/src/jetstream/plugins/kubernetes/install_release.go b/src/jetstream/plugins/kubernetes/install_release.go index 7998300c8b..d0f154708f 100644 --- a/src/jetstream/plugins/kubernetes/install_release.go +++ b/src/jetstream/plugins/kubernetes/install_release.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/json" "fmt" + "net/url" + "strings" "github.com/labstack/echo" log "github.com/sirupsen/logrus" @@ -28,6 +30,7 @@ type installRequest struct { Namespace string `json:"releaseNamespace"` Values string `json:"values"` ChartURL string `json:"chartUrl"` + RepoURL string `json:"repoUrl"` Chart struct { Name string `json:"chartName"` Repository string `json:"repo"` @@ -39,6 +42,7 @@ type upgradeRequest struct { MonocularEndpoint string `json:"monocularEndpoint"` Values string `json:"values"` ChartURL string `json:"chartUrl"` + RepoURL string `json:"repoUrl"` Chart struct { Name string `json:"name"` Repository string `json:"repo"` @@ -64,7 +68,7 @@ func (c *KubernetesSpecification) InstallRelease(ec echo.Context) error { return interfaces.NewJetstreamErrorf("Client did not supply Chart download URL") } - chart, err := c.loadChart(params.ChartURL) + chart, err := c.loadChart(params.ChartURL, params.RepoURL) if err != nil { return interfaces.NewJetstreamErrorf("Could not load chart: %v+", err) } @@ -114,12 +118,17 @@ func (c *KubernetesSpecification) InstallRelease(ec echo.Context) error { } // Load the Helm chart for the given repository, name and version -func (c *KubernetesSpecification) loadChart(downloadURL string) (*chart.Chart, error) { +func (c *KubernetesSpecification) loadChart(downloadURL, repoURL string) (*chart.Chart, error) { log.Debugf("Helm Chart Download URL: %s", downloadURL) + validatedDownloadURL, err := c.validateChartDownloadUrl(downloadURL, repoURL) + if err != nil { + return nil, fmt.Errorf("Failed to validate download url of '%+v'. Error: %s", downloadURL, err) + } + // NWM: Should we look up Helm Repository endpoint and use the value from that httpClient := c.portalProxy.GetHttpClient(false) - resp, err := httpClient.Get(downloadURL) + resp, err := httpClient.Get(validatedDownloadURL) if err != nil { return nil, fmt.Errorf("Could not download Chart Archive: %s", err) } @@ -132,6 +141,36 @@ func (c *KubernetesSpecification) loadChart(downloadURL string) (*chart.Chart, e return loader.LoadArchive(resp.Body) } +func (c *KubernetesSpecification) validateChartDownloadUrl(downloadURL, repoURL string) (string, error) { + dURL, err := url.Parse(downloadURL) + if err != nil { + return "", fmt.Errorf("Could not parse download url '%+v': %+v", dURL, err) + } + + if len(dURL.String()) == 0 { + return "", fmt.Errorf("No download URL supplied") + } + + if dURL.IsAbs() { + return dURL.String(), nil + } + + log.Debug("Failed to parse helm download url, assuming this is the file and appending it to the repo url to create download url") + + // Assume download url is actually file name. Append it to repo url + rURL, err := url.Parse(repoURL) + if err != nil { + return "", fmt.Errorf("Combinging download url with repo url. Could not parse repo url '%+v': %+v", dURL, err) + } + + if len(rURL.String()) == 0 { + return "", fmt.Errorf("Combinging download url with repo url. No repo URL supplied") + } + + // Ensure any trailing / is stripped from url + return strings.TrimRight(rURL.String(), "/") + "/" + downloadURL, nil +} + // DeleteRelease will delete a release func (c *KubernetesSpecification) DeleteRelease(ec echo.Context) error { endpointGUID := ec.Param("endpoint") @@ -203,7 +242,7 @@ func (c *KubernetesSpecification) UpgradeRelease(ec echo.Context) error { return interfaces.NewJetstreamErrorf("Client did not supply Chart download URL") } - chart, err := c.loadChart(params.ChartURL) + chart, err := c.loadChart(params.ChartURL, params.RepoURL) if err != nil { return interfaces.NewJetstreamErrorf("Could not load chart for upgrade: %+v", err) } From 37375eb0993118fc66d06fae4b8a9ed2d06543e3 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 22 Sep 2020 12:53:34 +0100 Subject: [PATCH 2/7] Fix issue with Schemas on Helm Hub --- .../chart-details-info.component.ts | 6 +- .../shared/services/charts.service.ts | 32 ++++++++- .../src/custom/helm/store/helm.types.ts | 1 + .../chart-values-editor.component.html | 2 +- .../chart-values-editor.component.ts | 4 ++ .../create-release.component.ts | 11 +-- .../tabs/helm-release-helper.service.ts | 10 +-- .../upgrade-release.component.spec.ts | 5 ++ .../upgrade-release.component.ts | 18 +++-- .../kubernetes/workloads/workloads.module.ts | 6 +- src/jetstream/plugins/monocular/chart.go | 2 +- src/jetstream/plugins/monocular/chart_svc.go | 68 +++++++++++++++++++ src/jetstream/plugins/monocular/main.go | 15 ++-- 13 files changed, 150 insertions(+), 30 deletions(-) diff --git a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-info/chart-details-info.component.ts b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-info/chart-details-info.component.ts index fad154c9d7..7a273df5e6 100644 --- a/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-info/chart-details-info.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/helm/monocular/chart-details/chart-details-info/chart-details-info.component.ts @@ -26,7 +26,7 @@ export class ChartDetailsInfoComponent implements OnInit { @Input() set currentVersion (version: ChartVersion) { this._currentVersion = version; if (version) { - this.getSchema(this._currentVersion); + this.getSchema(this._currentVersion, this.chart); } } @@ -69,8 +69,8 @@ export class ChartDetailsInfoComponent implements OnInit { ); } - private getSchema(currentVersion: ChartVersion) { - this.chartsService.getChartSchema(currentVersion).pipe( + private getSchema(currentVersion: ChartVersion, chart: Chart) { + this.chartsService.getChartSchema(currentVersion, chart).pipe( first(), catchError(() => of(null)) ).subscribe(schema => { 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 196f3f96eb..952a113f4f 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 @@ -7,6 +7,7 @@ import { catchError, map, tap } from 'rxjs/operators'; import { getMonocularEndpoint, stratosMonocularEndpointGuid } from '../../stratos-monocular.helper'; import { Chart } from '../models/chart'; import { ChartVersion } from '../models/chart-version'; +import { RepoAttributes } from '../models/repo'; import { ConfigService } from './config.service'; @@ -22,7 +23,6 @@ export class ChartsService { config: ConfigService, private route: ActivatedRoute, ) { - this.hostname = `${config.backendHostname}/chartsvc`; this.cacheCharts = {}; this.hostname = '/pp/v1/chartsvc'; } @@ -136,8 +136,34 @@ export class ChartsService { * @param version Chart version * @return An observable that will be the json schema */ - getChartSchema(chartVersion: ChartVersion): Observable { - return chartVersion.attributes.schema ? this.http.get(`${this.hostname}${chartVersion.attributes.schema}`) : of(null); + getChartSchema(chartVersion: ChartVersion, chart: Chart): Observable { + const url = this.getChartSchemaURL(chartVersion, chart.attributes.name, chart.attributes.repo); + return url ? this.http.get(url) : of(null); + } + + // Get the URL for obtaining a Chart's schema + getChartSchemaURL(chartVersion: ChartVersion, name: string, repo: RepoAttributes): string { + // Helm Hub does not give us the schema information so we have to use an additional backend API to fetch the chart and check + if (chartVersion.attributes.schema === undefined) { + let url = this.getChartURL(chartVersion, repo); + url = btoa(url); + return `/pp/v1/monocular/schema/${name}/${url}`; + } + + // We have the schema URL, so we can fetch that directly + return chartVersion.attributes.schema ? `${this.hostname}${chartVersion.attributes.schema}`: null; + } + + private getChartURL(chartVersion: ChartVersion, repo: RepoAttributes): string { + let url =''; + const urls = chartVersion.attributes.urls; + if (urls.length > 0) { + url = urls[0]; + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = repo.url + '/' + url; + } + } + return url } /** 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 fc79d7039c..0bee35fc0e 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 @@ -46,6 +46,7 @@ export enum HelmStatus { } export interface HelmChartReference { + endpoint?: string; name: string; repo: string; version: string; diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/chart-values-editor/chart-values-editor.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/chart-values-editor/chart-values-editor.component.html index 2baa79aaf0..2b5bf1b3fd 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/chart-values-editor/chart-values-editor.component.html +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/chart-values-editor/chart-values-editor.component.html @@ -1,5 +1,5 @@ -
+
Loading ...
diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/chart-values-editor/chart-values-editor.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/chart-values-editor/chart-values-editor.component.ts index ff0ace0e14..5d765b4e85 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/chart-values-editor/chart-values-editor.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/chart-values-editor/chart-values-editor.component.ts @@ -98,6 +98,8 @@ export class ChartValuesEditorComponent implements OnInit, OnDestroy, AfterViewI // Observable - are we still loading resources? public loading$: Observable; + public initing = true; + // Observable for tracking if the Monaco editor has loaded private monacoLoaded$ = new BehaviorSubject(false); @@ -191,6 +193,8 @@ export class ChartValuesEditorComponent implements OnInit, OnDestroy, AfterViewI map(([schema, values, loaded]) => !loaded), startWith(true) ); + + this.initing = false; } ngAfterViewInit(): void { diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts index 59d4a9d1d7..7bacee06d1 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts @@ -62,10 +62,13 @@ export class CreateReleaseComponent implements OnInit, OnDestroy { this.cancelUrl = this.chartsService.getChartSummaryRoute(chart.repo, chart.name, chart.version, this.route); this.chart = chart; - this.config = { - valuesUrl: `/pp/v1/chartsvc/v1/assets/${chart.repo}/${chart.name}/versions/${chart.version}/values.yaml`, - schemaUrl: `/pp/v1/chartsvc/v1/assets/${chart.repo}/${chart.name}/versions/${chart.version}/values.schema.json`, - }; + // Fetch the Chart Version metadata so we can get the correct URL for the Chart's JSON Schema + this.chartsService.getVersion(this.chart.repo, this.chart.name, this.chart.version).pipe(first()).subscribe(ch => { + this.config = { + valuesUrl:`/pp/v1/monocular/values/${this.chart.endpoint}/${this.chart.repo}/${chart.name}/${this.chart.version}`, + schemaUrl: this.chartsService.getChartSchemaURL(ch, ch.relationships.chart.data.name, ch.relationships.chart.data.repo) + }; + }) this.setupDetailsStep(); } 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 d3af46d384..93dffcc4e4 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 @@ -257,21 +257,17 @@ export class HelmReleaseHelperService { // We don't know which Helm repository it came from, so use the name and sources to match private isProbablySameChart(a: ChartMetadata, b: ChartMetadata): boolean { // Basic properties must be the same - if ((a.name !== b.name) || (a.sources.length !== b.sources.length)) { + if (a.name !== b.name) { return false; } - // Sources must match + // Must have at least one source in common let count = 0; a.sources.forEach(source => { count += b.sources.findIndex((s) => s === source) === -1 ? 0 : 1; }); - if (count !== a.sources.length) { - return false; - } - - return true; + return count > 0; } } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.spec.ts index f77dc54235..c3151ef19b 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/upgrade-release/upgrade-release.component.spec.ts @@ -1,5 +1,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockChartService } from '../../../helm/monocular/shared/services/chart.service.mock'; +import { ChartsService } from '../../../helm/monocular/shared/services/charts.service'; +import { ConfigService } from '../../../helm/monocular/shared/services/config.service'; import { HelmReleaseProviders, KubeBaseGuidMock } from '../../kubernetes.testing.module'; import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; import { WorkloadsBaseTestingModule } from '../workloads.testing.module'; @@ -20,6 +23,8 @@ describe('UpgradeReleaseComponent', () => { KubernetesEndpointService, KubeBaseGuidMock, ...HelmReleaseProviders, + { provide: ChartsService, useValue: new MockChartService() }, + { provide: ConfigService, useValue: { appName: 'appName' } }, ] }) .compileComponents(); 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 43d7af5f25..ff8d0b9832 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 @@ -10,6 +10,8 @@ import { StepOnNextResult, } from '../../../../../../core/src/shared/components/stepper/step/step.component'; import { ActionState } from '../../../../../../store/src/reducers/api-request-reducer/types'; +import { ChartsService } from '../../../helm/monocular/shared/services/charts.service'; +import { createMonocularProviders } from '../../../helm/monocular/stratos-monocular-providers.helpers'; import { stratosMonocularEndpointGuid } from '../../../helm/monocular/stratos-monocular.helper'; import { HelmUpgradeValues, MonocularVersion } from '../../../helm/store/helm.types'; import { ChartValuesConfig, ChartValuesEditorComponent } from '../chart-values-editor/chart-values-editor.component'; @@ -33,7 +35,8 @@ import { ReleaseUpgradeVersionsListConfig } from './release-version-list-config' deps: [ ActivatedRoute ] - } + }, + ...createMonocularProviders() ] }) export class UpgradeReleaseComponent { @@ -54,7 +57,8 @@ export class UpgradeReleaseComponent { constructor( store: Store, - public helper: HelmReleaseHelperService + public helper: HelmReleaseHelperService, + private chartsService: ChartsService, ) { this.cancelUrl = `/workloads/${this.helper.guid}`; @@ -96,9 +100,15 @@ export class UpgradeReleaseComponent { return this.helper.release$.pipe( first(), tap(release => { + const schemaUrl = this.chartsService.getChartSchemaURL( + this.version, + this.version.relationships.chart.data.name, + this.version.relationships.chart.data.repo + ); + const endpointID = this.monocularEndpointId || stratosMonocularEndpointGuid; this.config = { - schemaUrl: `/pp/v1/chartsvc/v1/assets/${chart.repo.name}/${chart.name}/versions/${version}/values.schema.json`, - valuesUrl: `/pp/v1/chartsvc/v1/assets/${chart.repo.name}/${chart.name}/versions/${version}/values.yaml`, + schemaUrl, + valuesUrl: `/pp/v1/monocular/values/${endpointID}/${chart.repo.name}/${chart.name}/${version}`, releaseValues: release.config }; }), diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts index 7359657da2..341c83d740 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts @@ -14,6 +14,7 @@ import { HelmReleaseTabBaseComponent } from './release/helm-release-tab-base/hel import { HelmReleaseAnalysisTabComponent, } from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component'; +import { HelmReleaseHistoryTabComponent } from './release/tabs/helm-release-history-tab/helm-release-history-tab.component'; import { HelmReleaseNotesTabComponent } from './release/tabs/helm-release-notes-tab/helm-release-notes-tab.component'; import { HelmReleasePodsTabComponent } from './release/tabs/helm-release-pods/helm-release-pods-tab.component'; import { @@ -22,12 +23,11 @@ import { import { HelmReleaseServicesTabComponent } from './release/tabs/helm-release-services/helm-release-services-tab.component'; import { HelmReleaseSummaryTabComponent } from './release/tabs/helm-release-summary-tab/helm-release-summary-tab.component'; import { HelmReleaseValuesTabComponent } from './release/tabs/helm-release-values-tab/helm-release-values-tab.component'; +import { WorkloadLiveReloadComponent } from './release/workload-live-reload/workload-live-reload.component'; import { HelmReleasesTabComponent } from './releases-tab/releases-tab.component'; import { WorkloadsStoreModule } from './store/workloads.store.module'; import { UpgradeReleaseComponent } from './upgrade-release/upgrade-release.component'; import { WorkloadsRouting } from './workloads.routing'; -import { HelmReleaseHistoryTabComponent } from './release/tabs/helm-release-history-tab/helm-release-history-tab.component'; -import { WorkloadLiveReloadComponent } from './release/workload-live-reload/workload-live-reload.component'; // Default config for the Monaco edfior const monacoConfig: NgxMonacoEditorConfig = { @@ -68,7 +68,7 @@ const monacoConfig: NgxMonacoEditorConfig = { HelmReleaseCardComponent ], providers: [ - DatePipe + DatePipe, ] }) export class WorkloadsModule { } diff --git a/src/jetstream/plugins/monocular/chart.go b/src/jetstream/plugins/monocular/chart.go index 70e9c4a763..3b486aff12 100644 --- a/src/jetstream/plugins/monocular/chart.go +++ b/src/jetstream/plugins/monocular/chart.go @@ -51,7 +51,7 @@ type ChartVersion struct { URLs []string `json:"urls"` Readme string `json:"readme,omitempty"` Values string `json:"values,omitempty"` - Schema string `json:"schema,omitempty"` + Schema string `json:"schema"` } //RepoCheck describes the state of a repository in terms its current checksum and last update time. diff --git a/src/jetstream/plugins/monocular/chart_svc.go b/src/jetstream/plugins/monocular/chart_svc.go index 2913536a52..79a192bebf 100644 --- a/src/jetstream/plugins/monocular/chart_svc.go +++ b/src/jetstream/plugins/monocular/chart_svc.go @@ -1,6 +1,7 @@ package monocular import ( + "encoding/base64" "errors" "fmt" "net/http" @@ -197,6 +198,73 @@ func (m *Monocular) getChartAndVersionFile(c echo.Context) error { return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Can not find file %s for the specified chart", filename)) } +func (m *Monocular) getChartValues(c echo.Context) error { + endpointID := c.Param("endpoint") + repo := c.Param("repo") + chartName := c.Param("name") + version := c.Param("version") + + // Built in Monocular + if endpointID == "default" { + filename := "values.yaml" + log.Debugf("Get chart file: %s", filename) + chart, err := m.ChartStore.GetChart(repo, chartName, version) + if err != nil { + return err + } + if m.cacheChart(*chart) == nil { + return c.File(path.Join(m.getChartCacheFolder(*chart), filename)) + } + return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Can not find file %s for the specified chart", filename)) + } + + // Helm Hub + // Change the URL and then forward on + p := fmt.Sprintf("/chartsvc/v1/assets/%s/%s/versions/%s/values.yaml", repo, chartName, version) + monocularEndpoint, err := m.validateExternalMonocularEndpoint(endpointID) + if monocularEndpoint == nil || err != nil { + return echo.NewHTTPError(http.StatusBadRequest, errors.New("No monocular endpoint")) + } + + return m.proxyToMonocularInstance(c, monocularEndpoint, p) +} + +// Check to see if the given chart URL has a schema +func (m *Monocular) checkForJsonSchema(c echo.Context) error { + log.Debug("checkForJsonSchema called") + + chartName := c.Param("name") + encodedChartURL := c.Param("encodedChartURL") + url, err := base64.StdEncoding.DecodeString(encodedChartURL) + if err != nil { + return err + } + + chartURL := string(url) + + chartCachePath := path.Join(m.CacheFolder, "schemas", encodedChartURL) + if err := m.ensureFolder(chartCachePath); err != nil { + log.Warnf("checkForJsonSchema: Could not create folder for chart downloads: %+v", err) + return err + } + + // We can delete the Chart archive - don't need it anymore + defer os.RemoveAll(chartCachePath) + + archiveFile := path.Join(chartCachePath, "chart.tgz") + if err := m.downloadFile(archiveFile, chartURL); err != nil { + return fmt.Errorf("Could not download chart from: %s - %+v", chartURL, err) + } + + // Now extract the files we need + filenames := []string{"values.schema.json"} + if err := extractArchiveFiles(archiveFile, chartName, chartCachePath, filenames); err != nil { + return fmt.Errorf("Could not extract files from chart archive: %s - %+v", archiveFile, err) + } + + return c.File(path.Join(chartCachePath, "values.schema.json")) +} + // This is the simpler version that returns just enough data needed for the Charts list view // This is a slight cheat - the response is not as complete as the Monocular API, but includes // enough for the UI and saves us having to pull out all of the Chart.yaml files diff --git a/src/jetstream/plugins/monocular/main.go b/src/jetstream/plugins/monocular/main.go index b5c46f2e58..9315381a2b 100644 --- a/src/jetstream/plugins/monocular/main.go +++ b/src/jetstream/plugins/monocular/main.go @@ -171,6 +171,9 @@ func (m *Monocular) AddSessionGroupRoutes(echoGroup *echo.Group) { // however cannot be done for things like img src echoGroup.Any("/monocular/:guid/chartsvc/*", m.handleMonocularInstance) + echoGroup.Any("/monocular/schema/:name/:encodedChartURL", m.checkForJsonSchema) + echoGroup.Any("/monocular/values/:endpoint/:repo/:name/:version", m.getChartValues) + echoGroup.POST("/chartrepos/:guid", m.syncRepo) echoGroup.POST("/chartrepos/status", m.getRepoStatuses) @@ -290,7 +293,6 @@ func (m *Monocular) baseHandleMonocularInstance(c echo.Context, monocularEndpoin // 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 { @@ -308,13 +310,18 @@ func (m *Monocular) baseHandleMonocularInstance(c echo.Context, monocularEndpoin parts = parts[2:] } - // Bring all back together - url.Path += "/" + strings.Join(parts, "/") + path = "/" + strings.Join(parts, "/") } + + return m.proxyToMonocularInstance(c, monocularEndpoint, path) +} + +func (m *Monocular) proxyToMonocularInstance(c echo.Context, monocularEndpoint *interfaces.CNSIRecord, path string) error { + url := monocularEndpoint.APIEndpoint log.Debugf("URL to monocular: %v", url.String()) + url.Path += path req, err := http.NewRequest(c.Request().Method, url.String(), nil) - removeBreakingHeaders(c.Request(), req) client := &http.Client{Timeout: 30 * time.Second} From 6966fb3b6799c5b4d04af4383c4b0653fd2402de Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 22 Sep 2020 14:00:22 +0100 Subject: [PATCH 3/7] Update following changes in #503 --- .../shared/services/charts.service.ts | 37 +++++++++++---- .../src/custom/helm/store/helm.types.ts | 1 - .../create-release.component.ts | 11 ++--- .../upgrade-release.component.ts | 4 +- .../kubernetes/workloads/workload.utils.ts | 16 ------- .../plugins/kubernetes/install_release.go | 47 ++----------------- 6 files changed, 36 insertions(+), 80 deletions(-) delete mode 100644 src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.utils.ts 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 952a113f4f..7c747f9f6b 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 @@ -151,19 +151,38 @@ export class ChartsService { } // We have the schema URL, so we can fetch that directly - return chartVersion.attributes.schema ? `${this.hostname}${chartVersion.attributes.schema}`: null; + return chartVersion.attributes.schema ? `${this.hostname}${chartVersion.attributes.schema}` : null; } - private getChartURL(chartVersion: ChartVersion, repo: RepoAttributes): string { - let url =''; - const urls = chartVersion.attributes.urls; - if (urls.length > 0) { - url = urls[0]; - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = repo.url + '/' + url; + getChartURL(chartVersion: ChartVersion, repo?: RepoAttributes): string { + const firstUrl = this.getFirstChartUrl(chartVersion); + if (firstUrl.length > 0) { + // Check if url is absolute, if not assume it's a filename + if (!firstUrl.startsWith('http://') && !firstUrl.startsWith('https://')) { + const repoUrl = repo ? repo.url : ''; + return repoUrl || this.getChartRepoUrl(chartVersion) + '/' + firstUrl; } } - return url + return firstUrl; + } + + private getFirstChartUrl(chart: ChartVersion): string { + if (chart && chart.attributes && chart.attributes.urls && chart.attributes.urls.length > 0) { + return chart.attributes.urls[0]; + } + return ''; + } + + private getChartRepoUrl(chart: ChartVersion): string { + if (chart && + chart.relationships && + chart.relationships.chart && + chart.relationships.chart.data && + chart.relationships.chart.data.repo + ) { + return chart.relationships.chart.data.repo.url; + } + return ''; } /** 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 b0856a00f9..0bee35fc0e 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 @@ -57,7 +57,6 @@ export interface HelmUpgradeInstallValues { values: string; chart: HelmChartReference; chartUrl: string; - repoUrl: string; } export interface HelmInstallValues extends HelmUpgradeInstallValues { diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts index e12cfc75ae..438c58c404 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.ts @@ -19,7 +19,6 @@ import { HelmChartReference, HelmInstallValues } from '../../../helm/store/helm. import { kubeEntityCatalog } from '../../kubernetes-entity-catalog'; import { KUBERNETES_ENDPOINT_TYPE } from '../../kubernetes-entity-factory'; import { KubernetesNamespace } from '../../store/kube.types'; -import { getChartRepoUrl, getFirstChartUrl } from '../workload.utils'; import { ChartValuesConfig, ChartValuesEditorComponent } from './../chart-values-editor/chart-values-editor.component'; @Component({ @@ -65,10 +64,10 @@ export class CreateReleaseComponent implements OnInit, OnDestroy { // Fetch the Chart Version metadata so we can get the correct URL for the Chart's JSON Schema this.chartsService.getVersion(this.chart.repo, this.chart.name, this.chart.version).pipe(first()).subscribe(ch => { this.config = { - valuesUrl:`/pp/v1/monocular/values/${this.chart.endpoint}/${this.chart.repo}/${chart.name}/${this.chart.version}`, + valuesUrl: `/pp/v1/monocular/values/${this.chart.endpoint}/${this.chart.repo}/${chart.name}/${this.chart.version}`, schemaUrl: this.chartsService.getChartSchemaURL(ch, ch.relationships.chart.data.name, ch.relationships.chart.data.repo) }; - }) + }); this.setupDetailsStep(); } @@ -241,14 +240,10 @@ export class CreateReleaseComponent implements OnInit, OnDestroy { throw new Error('Could not get Chart URL'); } // Add the chart url into the values - values.chartUrl = getFirstChartUrl(chartInfo); + values.chartUrl = this.chartsService.getChartURL(chartInfo); if (values.chartUrl.length === 0) { throw new Error('Could not get Chart URL'); } - values.repoUrl = getChartRepoUrl(chartInfo); - if (values.repoUrl.length === 0) { - throw new Error('Could not get Repo URL'); - } // Make the request return helmEntityCatalog.chart.api.install(values).pipe( // Wait for result of request 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 69c6b04d9c..7557184de4 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 @@ -17,7 +17,6 @@ import { HelmUpgradeValues, MonocularVersion } from '../../../helm/store/helm.ty import { ChartValuesConfig, ChartValuesEditorComponent } from '../chart-values-editor/chart-values-editor.component'; import { HelmReleaseHelperService } from '../release/tabs/helm-release-helper.service'; import { HelmReleaseGuid } from '../workload.types'; -import { getChartRepoUrl, getFirstChartUrl } from '../workload.utils'; import { workloadsEntityCatalog } from './../workloads-entity-catalog'; import { ReleaseUpgradeVersionsListConfig } from './release-version-list-config'; @@ -139,8 +138,7 @@ export class UpgradeReleaseComponent { version: this.version.attributes.version, }, monocularEndpoint: this.monocularEndpointId === stratosMonocularEndpointGuid ? null : this.monocularEndpointId, - chartUrl: getFirstChartUrl(this.version), - repoUrl: getChartRepoUrl(this.version) + chartUrl: this.chartsService.getChartURL(this.version) }; // Make the request diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.utils.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.utils.ts deleted file mode 100644 index a5ab5afa58..0000000000 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ChartVersion } from '../../helm/monocular/shared/models/chart-version'; - -// Get first URL for a Chart or return empty string if none -export function getFirstChartUrl(chart: ChartVersion): string { - if (chart && chart.attributes && chart.attributes.urls && chart.attributes.urls.length > 0) { - return chart.attributes.urls[0]; - } - return ''; -} - -export function getChartRepoUrl(chart: ChartVersion): string { - if (chart && chart.relationships && chart.relationships.chart && chart.relationships.chart.data && chart.relationships.chart.data.repo) { - return chart.relationships.chart.data.repo.url; - } - return ''; -} diff --git a/src/jetstream/plugins/kubernetes/install_release.go b/src/jetstream/plugins/kubernetes/install_release.go index d0f154708f..7998300c8b 100644 --- a/src/jetstream/plugins/kubernetes/install_release.go +++ b/src/jetstream/plugins/kubernetes/install_release.go @@ -4,8 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "net/url" - "strings" "github.com/labstack/echo" log "github.com/sirupsen/logrus" @@ -30,7 +28,6 @@ type installRequest struct { Namespace string `json:"releaseNamespace"` Values string `json:"values"` ChartURL string `json:"chartUrl"` - RepoURL string `json:"repoUrl"` Chart struct { Name string `json:"chartName"` Repository string `json:"repo"` @@ -42,7 +39,6 @@ type upgradeRequest struct { MonocularEndpoint string `json:"monocularEndpoint"` Values string `json:"values"` ChartURL string `json:"chartUrl"` - RepoURL string `json:"repoUrl"` Chart struct { Name string `json:"name"` Repository string `json:"repo"` @@ -68,7 +64,7 @@ func (c *KubernetesSpecification) InstallRelease(ec echo.Context) error { return interfaces.NewJetstreamErrorf("Client did not supply Chart download URL") } - chart, err := c.loadChart(params.ChartURL, params.RepoURL) + chart, err := c.loadChart(params.ChartURL) if err != nil { return interfaces.NewJetstreamErrorf("Could not load chart: %v+", err) } @@ -118,17 +114,12 @@ func (c *KubernetesSpecification) InstallRelease(ec echo.Context) error { } // Load the Helm chart for the given repository, name and version -func (c *KubernetesSpecification) loadChart(downloadURL, repoURL string) (*chart.Chart, error) { +func (c *KubernetesSpecification) loadChart(downloadURL string) (*chart.Chart, error) { log.Debugf("Helm Chart Download URL: %s", downloadURL) - validatedDownloadURL, err := c.validateChartDownloadUrl(downloadURL, repoURL) - if err != nil { - return nil, fmt.Errorf("Failed to validate download url of '%+v'. Error: %s", downloadURL, err) - } - // NWM: Should we look up Helm Repository endpoint and use the value from that httpClient := c.portalProxy.GetHttpClient(false) - resp, err := httpClient.Get(validatedDownloadURL) + resp, err := httpClient.Get(downloadURL) if err != nil { return nil, fmt.Errorf("Could not download Chart Archive: %s", err) } @@ -141,36 +132,6 @@ func (c *KubernetesSpecification) loadChart(downloadURL, repoURL string) (*chart return loader.LoadArchive(resp.Body) } -func (c *KubernetesSpecification) validateChartDownloadUrl(downloadURL, repoURL string) (string, error) { - dURL, err := url.Parse(downloadURL) - if err != nil { - return "", fmt.Errorf("Could not parse download url '%+v': %+v", dURL, err) - } - - if len(dURL.String()) == 0 { - return "", fmt.Errorf("No download URL supplied") - } - - if dURL.IsAbs() { - return dURL.String(), nil - } - - log.Debug("Failed to parse helm download url, assuming this is the file and appending it to the repo url to create download url") - - // Assume download url is actually file name. Append it to repo url - rURL, err := url.Parse(repoURL) - if err != nil { - return "", fmt.Errorf("Combinging download url with repo url. Could not parse repo url '%+v': %+v", dURL, err) - } - - if len(rURL.String()) == 0 { - return "", fmt.Errorf("Combinging download url with repo url. No repo URL supplied") - } - - // Ensure any trailing / is stripped from url - return strings.TrimRight(rURL.String(), "/") + "/" + downloadURL, nil -} - // DeleteRelease will delete a release func (c *KubernetesSpecification) DeleteRelease(ec echo.Context) error { endpointGUID := ec.Param("endpoint") @@ -242,7 +203,7 @@ func (c *KubernetesSpecification) UpgradeRelease(ec echo.Context) error { return interfaces.NewJetstreamErrorf("Client did not supply Chart download URL") } - chart, err := c.loadChart(params.ChartURL, params.RepoURL) + chart, err := c.loadChart(params.ChartURL) if err != nil { return interfaces.NewJetstreamErrorf("Could not load chart for upgrade: %+v", err) } From 7d8e9ff269af91e20e81f117c10be7e8379c3737 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 22 Sep 2020 15:11:27 +0100 Subject: [PATCH 4/7] Fix issue with missing schema on upgrade for local chart --- .../monocular/shared/services/charts.service.ts | 9 +++++++++ .../upgrade-release.component.ts | 17 ++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) 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 7c747f9f6b..80590f39ba 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 @@ -213,6 +213,15 @@ export class ChartsService { ); } + getVersionFromEndpoint(endpoint: string, repo: string, chartName: string, version: string): Observable { + const requestArgs = { headers: { 'x-cap-cnsi-list': endpoint !== stratosMonocularEndpointGuid ? endpoint :'' } }; + return this.http.get( + `${this.hostname}/v1/charts/${repo}/${chartName}/versions/${version}`, requestArgs).pipe( + map(this.extractData), + catchError(this.handleError) + ); + } + /** * Get the URL for retrieving the chart's icon * 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 7557184de4..8d4b789437 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 @@ -1,7 +1,7 @@ import { Component, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; -import { Observable, of } from 'rxjs'; +import { combineLatest, Observable, of } from 'rxjs'; import { filter, first, map, pairwise, tap } from 'rxjs/operators'; import { @@ -94,17 +94,16 @@ export class UpgradeReleaseComponent { onNext = (): Observable => { const chart = this.version.relationships.chart.data; const version = this.version.attributes.version; + const endpointID = this.monocularEndpointId || stratosMonocularEndpointGuid; + // Fetch the release metadata so that we have the values used to install the current release - return this.helper.release$.pipe( + return combineLatest( + [this.helper.release$, this.chartsService.getVersionFromEndpoint(endpointID, chart.repo.name, chart.name, version)] + ).pipe( first(), - tap(release => { - const schemaUrl = this.chartsService.getChartSchemaURL( - this.version, - this.version.relationships.chart.data.name, - this.version.relationships.chart.data.repo - ); - const endpointID = this.monocularEndpointId || stratosMonocularEndpointGuid; + tap(([release, chartVersionDetail]) => { + const schemaUrl = this.chartsService.getChartSchemaURL(chartVersionDetail, chart.name, chart.repo); this.config = { schemaUrl, valuesUrl: `/pp/v1/monocular/values/${endpointID}/${chart.repo.name}/${chart.name}/${version}`, From 4fbaefbc52ec351936dbfcbc9db606d758fb5eaa Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 22 Sep 2020 15:44:38 +0100 Subject: [PATCH 5/7] Fix failing unit test --- .../workloads/create-release/create-release.component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.spec.ts index 00adc0ba34..9de5331552 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/create-release/create-release.component.spec.ts @@ -47,6 +47,7 @@ describe('CreateReleaseComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CreateReleaseComponent); + httpMock.expectOne('/pp/v1/chartsvc/v1/charts/undefined/undefined/versions/undefined'); component = fixture.componentInstance; fixture.detectChanges(); }); From 8e542f1e623e6b8e7360b8b6accb4a23729ddbb2 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 22 Sep 2020 16:45:55 +0100 Subject: [PATCH 6/7] Ensure kube terminal does not hang if it takes a while to start up --- .../plugins/kubernetes/terminal/helpers.go | 19 +++-- .../plugins/kubernetes/terminal/start.go | 70 +++++++++++++------ 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/jetstream/plugins/kubernetes/terminal/helpers.go b/src/jetstream/plugins/kubernetes/terminal/helpers.go index 634cfff1ab..08f5a918a8 100644 --- a/src/jetstream/plugins/kubernetes/terminal/helpers.go +++ b/src/jetstream/plugins/kubernetes/terminal/helpers.go @@ -23,8 +23,9 @@ import ( ) const ( - helmEndpointType = "helm" - helmRepoEndpointType = "repo" + helmEndpointType = "helm" + helmRepoEndpointType = "repo" + startingProgressMessage = "Waiting for Kubernetes Terminal to start up ..." ) // PodCreationData stores the clients and names used to create pod and secret @@ -87,6 +88,8 @@ func (k *KubeTerminal) createPod(c echo.Context, kubeConfig, kubeVersion string, Type: "Opaque", } + sendProgressMessage(ws, startingProgressMessage) + setResourcMetadata(&secretSpec.ObjectMeta, sessionID) secretSpec.Data = make(map[string][]byte) @@ -98,6 +101,7 @@ func (k *KubeTerminal) createPod(c echo.Context, kubeConfig, kubeVersion string, secretSpec.Data["helm-setup"] = []byte(helmSetup) } + sendProgressMessage(ws, startingProgressMessage) _, err = secretClient.Create(secretSpec) if err != nil { log.Warnf("Kubernetes Terminal: Unable to create Secret: %+v", err) @@ -154,6 +158,8 @@ func (k *KubeTerminal) createPod(c echo.Context, kubeConfig, kubeVersion string, } podSpec.Spec.Volumes = volumesSpec + sendProgressMessage(ws, startingProgressMessage) + // Create a new pod pod, err := podClient.Create(podSpec) if err != nil { @@ -165,12 +171,12 @@ func (k *KubeTerminal) createPod(c echo.Context, kubeConfig, kubeVersion string, result.PodClient = podClient result.PodName = podName - sendProgressMessage(ws, "Waiting for Kubernetes Terminal to start up ...") - // Wait for the pod to be running timeout := 60 statusOptions := metav1.GetOptions{} for { + // This ensures we keep the web socket alive while the container is creating + sendProgressMessage(ws, startingProgressMessage) status, err := podClient.Get(pod.Name, statusOptions) if err == nil && status.Status.Phase == "Running" { break @@ -201,6 +207,11 @@ func setResourcMetadata(metadata *metav1.ObjectMeta, sessionID string) { // Cleanup the pod and secret func (k *KubeTerminal) cleanupPodAndSecret(podData *PodCreationData) error { + if podData == nil { + // Already been cleaned up + return nil + } + if len(podData.PodName) > 0 { //captureBashHistory(podData) podData.PodClient.Delete(podData.PodName, nil) diff --git a/src/jetstream/plugins/kubernetes/terminal/start.go b/src/jetstream/plugins/kubernetes/terminal/start.go index 63c90fe519..c6414daea5 100644 --- a/src/jetstream/plugins/kubernetes/terminal/start.go +++ b/src/jetstream/plugins/kubernetes/terminal/start.go @@ -41,6 +41,15 @@ type terminalSize struct { const ( // Time allowed to write a message to the peer. writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Time to wait before force close on connection. + closeGracePeriod = 10 * time.Second ) // Start handles web-socket request to launch a Kubernetes Terminal @@ -60,7 +69,7 @@ func (k *KubeTerminal) Start(c echo.Context) error { if !ok { return errors.New("Could not get token") } - + // This is the kube config for the kubernetes endpoint that we want configured in the Terminal kubeConfig, err := k.Kube.GetKubeConfigForEndpoint(cnsiRecord.APIEndpoint.String(), tokenRecord, "") if err != nil { @@ -79,7 +88,10 @@ func (k *KubeTerminal) Start(c echo.Context) error { defer ws.Close() defer pingTicker.Stop() - // We are now in web socket land - we don't want any middleware to change the HTTP response + // At this point we aer using web sockets, so we can not return errors to the client as the connection + // has been upgraded to a web socket + + // We are now in web socket land - we don't want any middleware to change the HTTP response c.Set("Stratos-WebSocket", "true") // Send a message to say that we are creating the pod @@ -95,8 +107,8 @@ func (k *KubeTerminal) Start(c echo.Context) error { k.cleanupPodAndSecret(podData) // Send error message - sendProgressMessage(ws, "!" + err.Error()) - return err + sendProgressMessage(ws, "!"+err.Error()) + return nil } // API Endpoint to SSH/exec into a container @@ -131,28 +143,33 @@ func (k *KubeTerminal) Start(c echo.Context) error { stdoutDone := make(chan bool) go pumpStdout(ws, wsConn, stdoutDone) + go ping(ws, stdoutDone) // If the downstream connection is closed, close the other web socket as well - ws.SetCloseHandler(func (code int, text string) error { + ws.SetCloseHandler(func(code int, text string) error { wsConn.Close() + // Cleanup + k.cleanupPodAndSecret(podData) + podData = nil return nil }) + // Wait a while when reading - can take some time for the container to launch + ws.SetReadDeadline(time.Now().Add(pongWait)) + ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + // Read the input from the web socket and pipe it to the SSH client for { _, r, err := ws.ReadMessage() if err != nil { - // Check to see if this was because the web socket was closed cleanly - closed := false - select { - case msg := <-stdoutDone: - closed = msg - } - if !closed { - log.Errorf("Kubernetes terminal: error reading message from web socket: %+v", err) - } - log.Debug("Kube Terminal cleaning up ....") + // Error reading - so clean up k.cleanupPodAndSecret(podData) + podData = nil + + ws.SetWriteDeadline(time.Now().Add(writeWait)) + ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + time.Sleep(closeGracePeriod) + ws.Close() // No point returning an error - we've already upgraded to web sockets, so we can't use the HTTP response now return nil @@ -160,7 +177,6 @@ func (k *KubeTerminal) Start(c echo.Context) error { res := KeyCode{} json.Unmarshal(r, &res) - if res.Cols == 0 { slice := make([]byte, 1) slice[0] = 0 @@ -177,14 +193,11 @@ func (k *KubeTerminal) Start(c echo.Context) error { wsConn.WriteMessage(websocket.TextMessage, slice) } } - - // Cleanup - log.Error("Kubernetes Terminal is cleaning up") - - return k.cleanupPodAndSecret(podData) } func pumpStdout(ws *websocket.Conn, source *websocket.Conn, done chan bool) { + source.SetReadDeadline(time.Now().Add(pongWait)) + source.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) for { _, r, err := source.ReadMessage() if err != nil { @@ -202,3 +215,18 @@ func pumpStdout(ws *websocket.Conn, source *websocket.Conn, done chan bool) { } } } + +func ping(ws *websocket.Conn, done chan bool) { + ticker := time.NewTicker(pingPeriod) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil { + log.Errorf("Web socket ping error: %+v", err) + } + case <-done: + return + } + } +} From 63bfafe74c7977d7b7f8bb0a6ef59ce1e876e67d Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 22 Sep 2020 17:05:57 +0100 Subject: [PATCH 7/7] Fix timeout issue --- src/jetstream/plugins/kubernetes/terminal/start.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/jetstream/plugins/kubernetes/terminal/start.go b/src/jetstream/plugins/kubernetes/terminal/start.go index c6414daea5..a37a26e9a8 100644 --- a/src/jetstream/plugins/kubernetes/terminal/start.go +++ b/src/jetstream/plugins/kubernetes/terminal/start.go @@ -196,8 +196,6 @@ func (k *KubeTerminal) Start(c echo.Context) error { } func pumpStdout(ws *websocket.Conn, source *websocket.Conn, done chan bool) { - source.SetReadDeadline(time.Now().Add(pongWait)) - source.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) for { _, r, err := source.ReadMessage() if err != nil {