diff --git a/.prow.yaml b/.prow.yaml index d6ffe15b29..9d6bd07ea1 100644 --- a/.prow.yaml +++ b/.prow.yaml @@ -1,5 +1,5 @@ presubmits: -- name: pull-dashboard-headless +- name: pull-dashboard-test-headless always_run: true decorate: true clone_uri: "ssh://git@github.com/kubermatic/dashboard-v2.git" @@ -24,7 +24,7 @@ presubmits: name: kubermatic-codecov key: token -- name: pull-dashboard-e2e +- name: pull-dashboard-test-e2e always_run: true decorate: true clone_uri: "ssh://git@github.com/kubermatic/dashboard-v2.git" @@ -48,7 +48,7 @@ presubmits: - image: quay.io/kubermatic/e2e-kind-cypress:v1.1.1 command: - make - - run-e2e-ci-v2 + - run-e2e-ci securityContext: privileged: true resources: diff --git a/Makefile b/Makefile index c0eca90509..c6a62edcf8 100644 --- a/Makefile +++ b/Makefile @@ -32,9 +32,6 @@ test-headless: install ./hack/upload-coverage.sh run-e2e-ci: install - ./hack/e2e/run_ci_e2e_test.sh - -run-e2e-ci-v2: install ./hack/e2e/ci-e2e.sh dist: install diff --git a/angular.json b/angular.json index 3af2bb41f8..1267e22a76 100644 --- a/angular.json +++ b/angular.json @@ -105,7 +105,7 @@ "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "kubermatic:build", - "proxyConfig": "./proxy.conf.json", + "proxyConfig": "./proxy.conf.js", "port": 8000, "hmrWarning": false }, @@ -114,7 +114,7 @@ "browserTarget": "kubermatic:build:production" }, "local": { - "proxyConfig": "./proxy-local.conf.json" + "proxyConfig": "./proxy-local.conf.js" }, "e2e": { "hmr": false, @@ -123,7 +123,7 @@ "e2e-local": { "hmr": false, "browserTarget": "kubermatic:build:e2e-local", - "proxyConfig": "./proxy-local.conf.json" + "proxyConfig": "./proxy-local.conf.js" } } }, diff --git a/hack/e2e/run_ci_e2e_test.sh b/hack/e2e/run_ci_e2e_test.sh deleted file mode 100755 index 10c91ab0fd..0000000000 --- a/hack/e2e/run_ci_e2e_test.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -CONTROLLER_IMAGE="quay.io/kubermatic/cluster-exposer:v2.0.0" - -if [[ -z ${JOB_NAME} ]]; then - echo "This script should only be running in a CI environment." - exit 0 -fi - -if [[ -z ${PROW_JOB_ID} ]]; then - echo "Build id env variable has to be set." - exit 0 -fi - -export CYPRESS_KUBERMATIC_DEX_DEV_E2E_USERNAME="roxy@loodse.com" -export CYPRESS_KUBERMATIC_DEX_DEV_E2E_USERNAME_2="roxy2@loodse.com" -export CYPRESS_KUBERMATIC_DEX_DEV_E2E_PASSWORD="password" - -export CYPRESS_RECORD_KEY=7859bcb8-1d2a-4d56-b7f5-ca70b93f944c - -function cleanup { - kubectl delete service -l "prow.k8s.io/id=$PROW_JOB_ID" - - # Kill all descendant processes - pkill -P $$ -} -trap cleanup EXIT - -# Set docker config -echo $IMAGE_PULL_SECRET_DATA | base64 -d > /config.json - -sed 's/localhost/localhost dex.oauth/' < /etc/hosts > /hosts -cat /hosts > /etc/hosts - -# Start docker daemon -dockerd > /dev/null 2> /dev/null & - -# Wait for it to start -while (! docker stats --no-stream ); do - # Docker takes a few seconds to initialize - echo "Waiting for Docker..." - sleep 1 -done - -# Load kind image -docker load --input /kindest.tar - -deploy.sh -DOCKER_CONFIG=/ docker run --name controller -d -v /root/.kube/config:/inner -v /etc/kubeconfig/kubeconfig:/outer --network host --privileged ${CONTROLLER_IMAGE} --kubeconfig-inner "/inner" --kubeconfig-outer "/outer" --namespace "default" --build-id "$PROW_JOB_ID" -docker logs -f controller & - -expose.sh - -npm run versioninfo -WAIT_ON_TIMEOUT=600000 npm run e2e:local diff --git a/proxy-local.conf.js b/proxy-local.conf.js new file mode 100644 index 0000000000..11a5d5e090 --- /dev/null +++ b/proxy-local.conf.js @@ -0,0 +1,13 @@ +const PROXY_CONFIG = [ + { + context: [ + "/api/**", + ], + target: "http://localhost:8080", + changeOrigin: true, + secure: false, + ws: true, + } +]; + +module.exports = PROXY_CONFIG; diff --git a/proxy-local.conf.json b/proxy-local.conf.json deleted file mode 100644 index ea2209ec0f..0000000000 --- a/proxy-local.conf.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "/api/**": { - "target": "http://localhost:8080", - "secure": false, - "changeOrigin": true - } -} diff --git a/proxy.conf.js b/proxy.conf.js new file mode 100644 index 0000000000..ff893e812f --- /dev/null +++ b/proxy.conf.js @@ -0,0 +1,16 @@ +const PROXY_CONFIG = [ + { + context: [ + "/api/**", + ], + target: "https://dev.kubermatic.io", + changeOrigin: true, + headers: { + 'Origin': 'https://dev.kubermatic.io', + }, + secure: false, + ws: true, + } +]; + +module.exports = PROXY_CONFIG; diff --git a/proxy.conf.json b/proxy.conf.json deleted file mode 100644 index dede5407dd..0000000000 --- a/proxy.conf.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "/api/**": { - "target": "https://dev.kubermatic.io", - "secure": false, - "changeOrigin": true - } -} diff --git a/src/app/cluster/cluster-details/cluster-details.component.html b/src/app/cluster/cluster-details/cluster-details.component.html index 3259741c96..12bc6b7c8a 100644 --- a/src/app/cluster/cluster-details/cluster-details.component.html +++ b/src/app/cluster/cluster-details/cluster-details.component.html @@ -42,7 +42,7 @@ target="_blank" mat-flat-button [disabled]="!isClusterRunning || (!isEditEnabled() && isOpenshiftCluster())" - *ngIf="(settings.adminSettings | async).enableDashboard"> + *ngIf="(settings.adminSettings | async)?.enableDashboard"> {{getConnectName()}} diff --git a/src/app/core/services/settings/settings.service.ts b/src/app/core/services/settings/settings.service.ts index 88c4bcc0be..33e0cdd843 100644 --- a/src/app/core/services/settings/settings.service.ts +++ b/src/app/core/services/settings/settings.service.ts @@ -1,7 +1,8 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {iif, merge, Observable, of, Subject, timer} from 'rxjs'; -import {catchError, map, shareReplay, switchMap} from 'rxjs/operators'; +import {BehaviorSubject, iif, merge, Observable, of, Subject, timer} from 'rxjs'; +import {catchError, delay, map, retryWhen, shareReplay, switchMap, tap} from 'rxjs/operators'; +import {webSocket} from 'rxjs/webSocket'; import {Auth} from '..'; import {environment} from '../../../../environments/environment'; @@ -31,15 +32,19 @@ const DEFAULT_ADMIN_SETTINGS: AdminSettings = { enableOIDCKubeconfig: false, }; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class SettingsService { - private readonly restRoot: string = environment.restRoot; + private readonly restRoot = environment.restRoot; + private readonly wsProtocol = window.location.protocol.replace('http', 'ws'); + private readonly wsRoot = `${this.wsProtocol}//${window.location.host}/${this.restRoot}/ws`; private _userSettings$: Observable; - private _userSettingsRefresh$: Subject = new Subject(); - private _adminSettings$: Observable; - private _adminSettingsRefresh$: Subject = new Subject(); + private _userSettingsRefresh$ = new Subject(); + private readonly _adminSettings$ = new BehaviorSubject(DEFAULT_ADMIN_SETTINGS); + private _adminSettingsWatch$: Observable; private _admins$: Observable; - private _adminsRefresh$: Subject = new Subject(); + private _adminsRefresh$ = new Subject(); private _refreshTimer$ = timer(0, this._appConfigService.getRefreshTimeBase() * 5); constructor( @@ -86,15 +91,22 @@ export class SettingsService { } get adminSettings(): Observable { - if (!this._adminSettings$) { - this._adminSettings$ = - merge(this._refreshTimer$, this._adminSettingsRefresh$) - .pipe(switchMap( - () => - iif(() => this._auth.authenticated(), this._getAdminSettings(true), of(DEFAULT_ADMIN_SETTINGS)))) - .pipe(map(settings => this._defaultAdminSettings(settings))) - .pipe(shareReplay({refCount: true, bufferSize: 1})); + // Subscribe to websocket and proxy all the settings updates coming from the API to the subject that is + // exposed in this method. Thanks to that it is possible to have default value and retry mechanism that + // will run in the background if connection will fail. Subscription to the API should happen only once. + // Behavior subject is used internally to always emit last value when subscription happens. + if (!this._adminSettingsWatch$) { + const webSocket$ = + webSocket(`${this.wsRoot}/admin/settings`) + .asObservable() + .pipe(retryWhen( + // Display error in the console for debugging purposes, otherwise it would be ignored. + // tslint:disable-next-line:no-console + errors => errors.pipe(tap(console.debug), delay(this._appConfigService.getRefreshTimeBase() * 3)))); + this._adminSettingsWatch$ = iif(() => this._auth.authenticated(), webSocket$, of(DEFAULT_ADMIN_SETTINGS)); + this._adminSettingsWatch$.subscribe(settings => this._adminSettings$.next(this._defaultAdminSettings(settings))); } + return this._adminSettings$; } @@ -102,12 +114,6 @@ export class SettingsService { return DEFAULT_ADMIN_SETTINGS; } - private _getAdminSettings(defaultOnError = false): Observable { - const url = `${this.restRoot}/admin/settings`; - const observable = this._httpClient.get(url); - return defaultOnError ? observable.pipe(catchError(() => of(DEFAULT_ADMIN_SETTINGS))) : observable; - } - private _defaultAdminSettings(settings: AdminSettings): AdminSettings { if (!settings) { return DEFAULT_ADMIN_SETTINGS; @@ -120,10 +126,6 @@ export class SettingsService { return settings; } - refreshAdminSettings(): void { - this._adminSettingsRefresh$.next(); - } - patchAdminSettings(patch: any): Observable { const url = `${this.restRoot}/admin/settings`; return this._httpClient.patch(url, patch); diff --git a/src/app/settings/admin/admin-settings.component.ts b/src/app/settings/admin/admin-settings.component.ts index b98a2bb51b..c7013eae0f 100644 --- a/src/app/settings/admin/admin-settings.component.ts +++ b/src/app/settings/admin/admin-settings.component.ts @@ -58,19 +58,18 @@ export class AdminSettingsComponent implements OnInit, OnChanges, OnDestroy { this._settingsService.adminSettings.pipe(takeUntil(this._unsubscribe)).subscribe(settings => { if (!_.isEqual(settings, this.apiSettings)) { - if (this.apiSettings) { + if (this._shouldDisplayUpdateNotification()) { this._notificationService.success('Successfully applied external settings update'); } this._applySettings(settings); } }); - this._settingsChange.pipe(debounceTime(1000)) + this._settingsChange.pipe(debounceTime(500)) .pipe(takeUntil(this._unsubscribe)) .pipe(switchMap(() => this._settingsService.patchAdminSettings(this._getPatch()))) .subscribe(settings => { this._applySettings(settings); - this._settingsService.refreshAdminSettings(); }); this._settingsService.userSettings.pipe(takeUntil(this._unsubscribe)).subscribe(settings => { @@ -88,6 +87,10 @@ export class AdminSettingsComponent implements OnInit, OnChanges, OnDestroy { this._unsubscribe.complete(); } + private _shouldDisplayUpdateNotification(): boolean { + return this.apiSettings && this.apiSettings !== this._settingsService.defaultAdminSettings; + } + private _applySettings(settings: AdminSettings): void { this.apiSettings = settings; this.settings = _.cloneDeep(this.apiSettings); diff --git a/src/app/settings/admin/custom-link-form/custom-links-form.component.ts b/src/app/settings/admin/custom-link-form/custom-links-form.component.ts index 58e8ca484f..adc67b1255 100644 --- a/src/app/settings/admin/custom-link-form/custom-links-form.component.ts +++ b/src/app/settings/admin/custom-link-form/custom-links-form.component.ts @@ -1,4 +1,4 @@ -import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms'; import * as _ from 'lodash'; @@ -9,7 +9,7 @@ import {CustomLink, CustomLinkLocation} from '../../../shared/utils/custom-link- templateUrl: './custom-links-form.component.html', styleUrls: ['./custom-links-form.component.scss'], }) -export class CustomLinksFormComponent implements OnInit { +export class CustomLinksFormComponent implements OnInit, OnChanges { @Input() customLinks: CustomLink[] = []; @Output() customLinksChange = new EventEmitter(); @Input() apiCustomLinks: CustomLink[] = []; @@ -22,6 +22,16 @@ export class CustomLinksFormComponent implements OnInit { } ngOnInit(): void { + this._buildForm(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.customLinks.currentValue !== changes.customLinks.previousValue) { + this._buildForm(); + } + } + + private _buildForm(): void { this.form = this._formBuilder.group({customLinks: this._formBuilder.array([])}); this.customLinks.forEach( customLink => this._addCustomLink(customLink.label, customLink.url, customLink.icon, customLink.location)); diff --git a/src/app/wizard/wizard.component.ts b/src/app/wizard/wizard.component.ts index 52d2a36763..58fcd0f1fc 100644 --- a/src/app/wizard/wizard.component.ts +++ b/src/app/wizard/wizard.component.ts @@ -61,9 +61,8 @@ export class WizardComponent implements OnInit, OnDestroy { this._settingsService.adminSettings.pipe(first()).subscribe( settings => this.addNodeData.count = settings.defaultNodeCount); - this._settingsService.adminSettings.pipe(takeUntil(this._unsubscribe)).subscribe(settings => { - this.settings = settings; - }); + this._settingsService.adminSettings.pipe(takeUntil(this._unsubscribe)) + .subscribe(settings => this.settings = settings); this.updateSteps(); this._projectService.selectedProject.pipe(takeUntil(this._unsubscribe)) diff --git a/src/environments/environment.e2e.local.ts b/src/environments/environment.e2e.local.ts index 0e50727ca6..6447136b80 100644 --- a/src/environments/environment.e2e.local.ts +++ b/src/environments/environment.e2e.local.ts @@ -6,8 +6,6 @@ export const environment = { customCSS: '../../assets/custom/style.css', refreshTimeBase: 1000, // Unit: ms restRoot: 'api/v1', - restRootV3: 'api/v3', - digitalOceanRestRoot: 'https://api.digitalocean.com/v2', oidcProviderUrl: 'http://dex.oauth:5556/dex/auth', oidcConnectorId: 'local', animations: false, diff --git a/src/environments/environment.e2e.ts b/src/environments/environment.e2e.ts index 2bcd4f99d2..fb1af9ccfd 100644 --- a/src/environments/environment.e2e.ts +++ b/src/environments/environment.e2e.ts @@ -6,8 +6,6 @@ export const environment = { customCSS: '../../assets/custom/style.css', refreshTimeBase: 1000, // Unit: ms restRoot: 'api/v1', - restRootV3: 'api/v3', - digitalOceanRestRoot: 'https://api.digitalocean.com/v2', oidcProviderUrl: 'https://dev.kubermatic.io/dex/auth', oidcConnectorId: 'local', animations: false, diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 3ae28b20dc..7e51146b7c 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -6,8 +6,6 @@ export const environment = { customCSS: '../assets/custom/style.css', refreshTimeBase: 1000, // Unit: ms restRoot: '/api/v1', - restRootV3: '/api/v3', - digitalOceanRestRoot: 'https://api.digitalocean.com/v2', oidcProviderUrl: window.location.protocol + '//' + window.location.host + '/dex/auth', oidcConnectorId: null, animations: true, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index bb1146c3a0..c7ad222995 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -11,8 +11,6 @@ export const environment = { customCSS: '../../assets/custom/style.css', refreshTimeBase: 1000, // Unit: ms restRoot: 'api/v1', - restRootV3: 'api/v3', - digitalOceanRestRoot: 'https://api.digitalocean.com/v2', oidcProviderUrl: 'https://dev.kubermatic.io/dex/auth', oidcConnectorId: null, animations: true,