From 37f53d56f80f5b530db894ed718862d62659aad6 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Fri, 26 Jun 2020 10:59:00 +0100 Subject: [PATCH] Add ability to import K8S endpoints from kube config file (#381) * Fix table passing issue * WIP: Import from kube config file * Minor tidy ups * Remove debugging code * Fix for error message being swallowed * Improvements to load, add edit name - tidied up selection component - move non-ui logic into helper - move record of clusters into helper - tidied up config helper - fixed select all intermediate state on bizare selections & first load - cleans a lot of table datasource interfaces * Specific fixes for upstream - Fix table header alignment - Fix checkbox table column alignment * Rename name column component, fix default context selection when invalid * Tidying up, add skip ssl, fix register of new * Allow user to skip connect by not suppling user * Tidying up, set AZK type, fixes * Minor fixes, subtle edit symbols * Fix case where cluster is register only but cannot connect, allow user to review final step * Add detection for EKS * Remove border between row that's errored and it's errror description row * Fix unit tests * Set initial state of skip ssl checkbox given request to kube * Fix unit tests * Remove some console.logs * Multiple small changes/fixes - tidy up - remove /kubeconfig.yaml fetch - fix select all at indeterminate state (should always select all) - fix apply for auto skip ssl * Fix connect error status message, change title of register endpoint stepper, minor changes * Improve table row error - remove border radius after expansion change - error rows have large indent to help show associated row - remove old styles that weren't applied following expansion change * Spacing * Changes following review * Changes following review * Fix build warning which is silent in dev world * Fixes following merge * Fix e2e test * Add icon for kube config import Co-authored-by: Richard Cox --- .../kube-config-auth.helper.ts | 158 +++++++++ .../kube-config-import.component.html | 3 + .../kube-config-import.component.scss | 8 + .../kube-config-import.component.spec.ts | 29 ++ .../kube-config-import.component.ts | 300 ++++++++++++++++++ ...-config-table-import-status.component.html | 1 + ...-config-table-import-status.component.scss | 0 ...nfig-table-import-status.component.spec.ts | 29 ++ ...be-config-table-import-status.component.ts | 26 ++ .../kube-config-registration.component.html | 11 + .../kube-config-registration.component.scss | 0 ...kube-config-registration.component.spec.ts | 35 ++ .../kube-config-registration.component.ts | 8 + .../kube-config-selection.component.html | 17 + .../kube-config-selection.component.scss | 35 ++ .../kube-config-selection.component.spec.ts | 29 ++ .../kube-config-selection.component.ts | 210 ++++++++++++ .../kube-config-table-cert.component.html | 6 + .../kube-config-table-cert.component.scss | 0 .../kube-config-table-cert.component.spec.ts | 33 ++ .../kube-config-table-cert.component.ts | 73 +++++ .../kube-config-table-name.component.html | 9 + .../kube-config-table-name.component.scss | 12 + .../kube-config-table-name.component.spec.ts | 34 ++ .../kube-config-table-name.component.ts | 11 + .../kube-config-table-select.component.html | 1 + .../kube-config-table-select.component.scss | 0 ...kube-config-table-select.component.spec.ts | 35 ++ .../kube-config-table-select.component.ts | 22 ++ ...onfig-table-sub-type-select.component.html | 3 + ...onfig-table-sub-type-select.component.scss | 0 ...ig-table-sub-type-select.component.spec.ts | 35 ++ ...-config-table-sub-type-select.component.ts | 33 ++ ...be-config-table-user-select.component.html | 9 + ...be-config-table-user-select.component.scss | 0 ...config-table-user-select.component.spec.ts | 39 +++ ...kube-config-table-user-select.component.ts | 31 ++ .../kube-config.helper.ts | 201 ++++++++++++ .../kube-config.types.ts | 99 ++++++ .../kubernetes/kubernetes-entity-generator.ts | 64 ++-- .../custom/kubernetes/kubernetes.module.ts | 6 +- .../kubernetes/kubernetes.setup.module.ts | 40 ++- .../frontend/assets/custom/kube_import.png | Bin 0 -> 13298 bytes .../table-cell-edit-variable.component.html | 3 +- .../table-cell-edit-variable.component.scss | 4 + .../cf-quotas-data-source.service.ts | 2 +- .../cf-space-quotas-data-source.service.ts | 2 +- .../packages/core/sass/_all-theme.scss | 2 +- .../core/sass/components/mat-table.scss | 2 +- .../packages/core/src/core/utils.service.ts | 8 + .../src/features/endpoints/connect.service.ts | 19 ++ .../create-endpoint-base-step.component.html | 2 +- .../create-endpoint-base-step.component.ts | 9 +- .../create-endpoint.component.html | 8 +- .../create-endpoint.component.ts | 30 +- .../app-action-monitor-icon.component.ts | 11 +- .../app-action-monitor.component.html | 5 +- .../app-action-monitor.component.ts | 18 +- .../file-input/file-input.component.html | 3 +- .../file-input/file-input.component.ts | 15 +- .../list-data-source-types.ts | 33 +- .../table-cell-edit.component.html | 21 +- .../table-cell-edit.component.scss | 12 +- .../table-cell-edit.component.ts | 23 +- .../table-header-select.component.html | 5 +- .../table-row/table-row.component.html | 12 +- .../table-row/table-row.component.scss | 24 +- .../table-row/table-row.component.theme.scss | 29 +- .../table-row/table-row.component.ts | 9 + .../list/list-table/table.component.html | 2 +- .../list/list-table/table.component.scss | 14 +- .../list/list-table/table.component.ts | 1 + .../packages/core/src/shared/shared.module.ts | 4 +- .../entity-catalog/entity-catalog.types.ts | 2 + .../api-request-reducer/fail-request.ts | 2 +- .../src/reducers/api-request-reducer/types.ts | 8 + .../store/src/selectors/api.selectors.ts | 4 +- src/jetstream/default.config.properties | 4 +- src/jetstream/plugins/kubernetes/main.go | 32 +- src/test-e2e/endpoints/register-dialog.po.ts | 2 +- 80 files changed, 1963 insertions(+), 118 deletions(-) create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-auth.helper.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.html create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.scss create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.spec.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.helper.ts create mode 100644 custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.types.ts create mode 100644 custom-src/frontend/assets/custom/kube_import.png diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-auth.helper.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-auth.helper.ts new file mode 100644 index 0000000000..4f2c119a4c --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-auth.helper.ts @@ -0,0 +1,158 @@ +import { ComponentFactoryResolver, Injector } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; + +import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; +import { ConnectEndpointData } from '../../../features/endpoints/connect.service'; +import { RowState } from '../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { KUBERNETES_ENDPOINT_TYPE } from '../kubernetes-entity-factory'; +import { EndpointAuthTypeConfig, IAuthForm } from './../../../core/extension/extension-types'; +import { KubeConfigFileCluster, KubeConfigFileUser } from './kube-config.types'; + +/** + * Auth helper tries to figure out the Kubernetes sub-type and auth to use + * based on the kube config file contents + */ +export class KubeConfigAuthHelper { + + authTypes: { [name: string]: EndpointAuthTypeConfig } = {}; + + public subTypes = []; + + constructor() { + const epTypeInfo = entityCatalog.getAllEndpointTypes(false); + const k8s = epTypeInfo.find(entity => entity.type === KUBERNETES_ENDPOINT_TYPE); + if (k8s && k8s.definition) { + const defn = k8s.definition; + + // Collect all of the auth types + defn.authTypes.forEach(at => { + this.authTypes[at.value] = at; + }); + + this.subTypes.push({ id: '', name: 'Generic' }); + + // Collect all of the auth types for the sub-types + defn.subTypes.forEach(st => { + if (st.type !== 'config') { + this.subTypes.push({ id: st.type, name: st.labelShort }); + } + st.authTypes.forEach(at => { + this.authTypes[at.value] = at; + }); + }); + + // Sort the subtypes + this.subTypes = this.subTypes.sort((a, b) => a.name.localeCompare(b.name)); + } + } + + // Try and parse the authentication metadata + public parseAuth(cluster: KubeConfigFileCluster, user: KubeConfigFileUser): RowState { + + // Default subtype is generic Kubernetes + cluster._subType = ''; + + // Certificate authentication first + + // In-file certificate authentication + if (user.user['client-certificate-data'] && user.user['client-key-data']) { + // We are good to go - create the form data + + // Default is generic kubernetes + let subType = ''; + const authType = 'kube-cert-auth'; + if (cluster.cluster.server.indexOf('azmk8s.io') >= 0) { + // Probably Azure + subType = 'aks'; + cluster._subType = 'aks'; + } + + const authData = { + authType, + subType, + values: { + cert: user.user['client-certificate-data'], + certKey: user.user['client-key-data'] + } + }; + user._authData = authData; + return {}; + } + + if (user.user['client-certificate'] || user.user['client-key']) { + cluster._additionalUserInfo = true; + return { + message: 'This endpoint will be registered but not connected (additional information is required)', + info: true + }; + } + + const authProvider = user.user['auth-provider']; + + + if (authProvider && authProvider.config) { + if (authProvider.config['cmd-path'] && authProvider.config['cmd-path'].indexOf('gcloud') !== -1) { + // GKE + cluster._subType = 'gke'; + // Can not connect to GKE - user must do so manually + cluster._additionalUserInfo = true; + return { + message: 'This endpoint will be registered but not connected (additional information is required)', + info: true + }; + } + } + + if ( + cluster.cluster.server.indexOf('eks.amazonaws.com') >= 0 || + (user.user.exec && user.user.exec.command && user.user.exec.command === 'aws-iam-authenticator') + ) { + // Probably EKS + cluster._subType = 'eks'; + cluster._additionalUserInfo = true; + return { + message: 'This endpoint will be registered but not connected (additional information is required)', + info: true + }; + } + + return { message: 'Authentication mechanism is not supported', warning: true }; + } + + // Use the auto component to get the data in the correct format for connecting to the endpoint + public getAuthDataForConnect(resolver: ComponentFactoryResolver, injector: Injector, fb: FormBuilder, user: KubeConfigFileUser) + : ConnectEndpointData | null { + + let data = null; + + // Get the component to us + if (user && user._authData) { + const authType = this.authTypes[user._authData.authType]; + + const factory = resolver.resolveComponentFactory(authType.component); + + const ref = factory.create(injector); + + const form = fb.group({ + authType: authType.value, + systemShared: false, + authValues: fb.group(user._authData.values) + }); + + ref.instance.formGroup = form; + + // Allow the auth form to supply body content if it needs to + const endpointFormInstance = ref.instance as any; + if (endpointFormInstance.getBody && endpointFormInstance.getValues) { + data = { + authType: authType.value, + authVal: endpointFormInstance.getValues(user._authData.values), + systemShared: false, + bodyContent: endpointFormInstance.getBody() + }; + } + ref.destroy(); + } + return data; + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.html new file mode 100644 index 0000000000..b78c35d87c --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.scss new file mode 100644 index 0000000000..05510cdc8c --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.scss @@ -0,0 +1,8 @@ +:host { + display: flex; + flex: 1; +} + +.kubeconfig-import { + flex: 1; +} \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.spec.ts new file mode 100644 index 0000000000..a006e2debc --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../kubernetes.testing.module'; +import { KubeConfigImportComponent } from './kube-config-import.component'; + +describe('KubeConfigImportComponent', () => { + let component: KubeConfigImportComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigImportComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigImportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.ts new file mode 100644 index 0000000000..e5e95dffd5 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.ts @@ -0,0 +1,300 @@ +import { Component, ComponentFactoryResolver, Injector, OnDestroy } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, first, map, pairwise, startWith, withLatestFrom } from 'rxjs/operators'; + +import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog'; +import { safeUnsubscribe } from '../../../../core/utils.service'; +import { ITableColumn } from '../../../../shared/components/list/list-table/table.types'; +import { KUBERNETES_ENDPOINT_TYPE } from '../../kubernetes-entity-factory'; +import { KubeConfigAuthHelper } from '../kube-config-auth.helper'; +import { KubeConfigFileCluster, KubeConfigImportAction, KubeImportState } from '../kube-config.types'; +import { RegisterEndpoint } from './../../../../../../store/src/actions/endpoint.actions'; +import { AppState } from './../../../../../../store/src/app-state'; +import { EndpointsEffect } from './../../../../../../store/src/effects/endpoint.effects'; +import { endpointSchemaKey } from './../../../../../../store/src/helpers/entity-factory'; +import { ActionState } from './../../../../../../store/src/reducers/api-request-reducer/types'; +import { selectUpdateInfo } from './../../../../../../store/src/selectors/api.selectors'; +import { STRATOS_ENDPOINT_TYPE } from './../../../../base-entity-schemas'; +import { EndpointsService } from './../../../../core/endpoints.service'; +import { + ConnectEndpointConfig, + ConnectEndpointData, + ConnectEndpointService, +} from './../../../../features/endpoints/connect.service'; +import { + ITableListDataSource, + RowState, +} from './../../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { StepOnNextFunction } from './../../../../shared/components/stepper/step/step.component'; +import { + KubeConfigTableImportStatusComponent, +} from './kube-config-table-import-status/kube-config-table-import-status.component'; + +const REGISTER_ACTION = 'Register endpoint'; +const CONNECT_ACTION = 'Connect endpoint'; + +@Component({ + selector: 'app-kube-config-import', + templateUrl: './kube-config-import.component.html', + styleUrls: ['./kube-config-import.component.scss'] +}) +export class KubeConfigImportComponent implements OnDestroy { + + done = new BehaviorSubject(false); + done$ = this.done.asObservable(); + busy = new BehaviorSubject(false); + busy$ = this.busy.asObservable(); + data = new BehaviorSubject([]); + data$ = this.data.asObservable(); + + public dataSource: ITableListDataSource = { + connect: () => this.data$, + disconnect: () => { }, + // Ensure unique per entry to step (in case user went back step and updated) + trackBy: (index, item) => item.cluster.name + this.iteration, + isTableLoading$: this.data$.pipe(map(data => !(data && data.length > 0))), + getRowState: (row: KubeConfigImportAction): Observable => { + return row ? row.state.asObservable() : observableOf({}); + } + }; + public columns: ITableColumn[] = [ + { + columnId: 'action', headerCell: () => 'Action', + cellDefinition: { + valuePath: 'action' + }, + cellFlex: '1', + }, + { + columnId: 'description', headerCell: () => 'Description', + cellDefinition: { + valuePath: 'description' + }, + cellFlex: '4', + }, + // Right-hand column to show the action progress + { + columnId: 'monitorState', + cellComponent: KubeConfigTableImportStatusComponent, + cellConfig: (row) => row.actionState.asObservable(), + cellFlex: '0 0 24px' + } + ]; + + subs: Subscription[] = []; + applyStarted: boolean; + private iteration = 0; + + private endpointEntityKey = entityCatalog.getEntityKey(STRATOS_ENDPOINT_TYPE, endpointSchemaKey); + private connectService: ConnectEndpointService; + + constructor( + public store: Store, + public resolver: ComponentFactoryResolver, + private injector: Injector, + private fb: FormBuilder, + private endpointsService: EndpointsService, + ) { + } + + // Process the next action in the list + private processAction(actions: KubeConfigImportAction[]) { + if (actions.length === 0) { + // We are done + this.done.next(true); + this.busy.next(false); + return; + } + + // Get the next action + const i = actions.shift(); + if (i.action === REGISTER_ACTION) { + this.doRegister(i, actions); + } else if (i.action === CONNECT_ACTION) { + this.doConnect(i, actions); + } else { + // Do the next action + this.processAction(actions); + } + } + + private doRegister(reg: KubeConfigImportAction, next: KubeConfigImportAction[]) { + const obs$ = this.registerEndpoint(reg.cluster.name, reg.cluster.cluster.server, reg.cluster.cluster['insecure-skip-tls-verify']); + const mainObs$ = this.getUpdatingState(obs$).pipe( + startWith({ busy: true, error: false, completed: false }) + ); + + this.subs.push(mainObs$.subscribe(reg.actionState)); + + const sub = reg.actionState.subscribe(progress => { + // Not sure what the status is used for? + reg.status = progress; + if (progress.error && progress.message) { + // Mark all dependency jobs as skip + next.forEach(action => { + if (action.depends === reg) { + // Mark it as skipped by setting the action to null + action.action = null; + action.state.next({ message: 'Skipping action as endpoint could not be registered', warning: true }); + } + }); + reg.state.next({ message: progress.message, error: true }); + } + if (progress.completed) { + if (!progress.error) { + // If we created okay, then guid is in the message + reg.cluster._guid = progress.message; + } + sub.unsubscribe(); + // Do the next one + this.processAction(next); + } + }); + this.subs.push(sub); + } + + private doConnect(connect: KubeConfigImportAction, next: KubeConfigImportAction[]) { + if (!connect.user) { + return; + } + const helper = new KubeConfigAuthHelper(); + const data = helper.getAuthDataForConnect(this.resolver, this.injector, this.fb, connect.user); + if (data) { + const obs$ = this.connectEndpoint(connect, data); + + // Echo obs$ to the behaviour subject + this.subs.push(obs$.subscribe(connect.actionState)); + + this.subs.push(connect.actionState.pipe(filter(status => status.completed), first()).subscribe(status => { + if (status.error) { + connect.state.next({ message: status.errorMessage || status.message, error: true }); + } + this.processAction(next); + })); + } + } + + ngOnDestroy() { + safeUnsubscribe(...this.subs); + + if (this.connectService) { + this.connectService.destroy(); + } + } + + // Register the endpoint + private registerEndpoint(name: string, url: string, skipSslValidation: boolean) { + const action = new RegisterEndpoint(KUBERNETES_ENDPOINT_TYPE, null, name, url, skipSslValidation, '', '', false); + this.store.dispatch(action); + const update$ = this.store.select( + selectUpdateInfo(this.endpointEntityKey, action.guid(), EndpointsEffect.registeringKey) + ).pipe(filter(update => !!update)); + return update$; + } + + // Connect to an endpoint + private connectEndpoint(action: KubeConfigImportAction, pData: ConnectEndpointData) { + const config: ConnectEndpointConfig = { + name: action.cluster.name, + guid: action.depends.cluster._guid || action.cluster._guid, + type: null, + subType: action.user._authData.subType, + ssoAllowed: false + }; + + if (this.connectService) { + this.connectService.destroy(); + } + this.connectService = new ConnectEndpointService(this.store, this.endpointsService, config); + this.connectService.setData(pData); + this.connectService.submit(); + return this.connectService.getConnectingObservable(); + } + + // Enter the step - process the list of clusters to import + onEnter = (data: KubeConfigFileCluster[]) => { + this.applyStarted = false; + this.iteration += 1; + const imports: KubeConfigImportAction[] = []; + data.forEach(item => { + if (item._selected) { + const register = { + action: REGISTER_ACTION, + description: `Register "${item.name}" with the URL "${item.cluster.server}"`, + cluster: item, + state: new BehaviorSubject({}), + actionState: new BehaviorSubject({}), + }; + // Only include if the endpoint does not already exist + if (!item._guid) { + imports.push(register); + } + if (item._additionalUserInfo) { + return; + } + const user = item._users.find(u => u.name === item._user); + if (user) { + imports.push({ + action: CONNECT_ACTION, + description: `Connect "${item.name}" with the user "${user.name}"`, + cluster: item, + user, + state: new BehaviorSubject({}), + depends: register, + actionState: new BehaviorSubject({}), + }); + } + } + }); + this.data.next(imports); + } + + // Finish - go back to the endpoints view + onNext: StepOnNextFunction = () => { + if (this.applyStarted) { + // this.store.dispatch(new RouterNav({ path: ['endpoints'] })); + return observableOf({ success: true, redirect: true }); + + } else { + this.applyStarted = true; + this.busy.next(true); + this.data$.pipe( + filter((data => data && data.length > 0)), + first() + ).subscribe(imports => { + // Go through the imports and dispatch the actions to perform them in sequence + this.processAction([...imports]); + }) + return observableOf({ success: true, ignoreSuccess: true }); + } + } + + // These two should be somewhere else + private getUpdatingState(actionState$: Observable): Observable { + const completed$ = this.getHasCompletedObservable(actionState$.pipe(map(requestState => requestState.busy))); + return actionState$.pipe( + pairwise(), + withLatestFrom(completed$), + map(([[, requestState], completed]) => { + return { + busy: requestState.busy, + error: requestState.error, + completed, + message: requestState.message, + }; + }) + ); + } + + private getHasCompletedObservable(busy$: Observable) { + return busy$.pipe( + distinctUntilChanged(), + pairwise(), + map(([oldBusy, newBusy]) => oldBusy && !newBusy), + startWith(false), + ); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.html new file mode 100644 index 0000000000..e4b06461a1 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.html @@ -0,0 +1 @@ + diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.spec.ts new file mode 100644 index 0000000000..6d2263d935 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigTableImportStatusComponent } from './kube-config-table-import-status.component'; + +describe('KubeConfigTableImportStatusComponent', () => { + let component: KubeConfigTableImportStatusComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableImportStatusComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableImportStatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.ts new file mode 100644 index 0000000000..08bd785052 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.ts @@ -0,0 +1,26 @@ +import { Component, Input } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-import-status', + templateUrl: './kube-config-table-import-status.component.html', + styleUrls: ['./kube-config-table-import-status.component.scss'] +}) +export class KubeConfigTableImportStatusComponent extends TableCellCustom { + + public state: Observable; + + constructor() { + super(); + } + + @Input() + set config(element) { + if (!this.state) { + this.state = element(this.row); + } + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.html new file mode 100644 index 0000000000..6611b3a683 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.html @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.spec.ts new file mode 100644 index 0000000000..bfcf2b0024 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../kubernetes.testing.module'; +import { KubeConfigImportComponent } from './kube-config-import/kube-config-import.component'; +import { KubeConfigRegistrationComponent } from './kube-config-registration.component'; +import { KubeConfigSelectionComponent } from './kube-config-selection/kube-config-selection.component'; + +describe('KubeConfigRegistrationComponent', () => { + let component: KubeConfigRegistrationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [ + KubeConfigRegistrationComponent, + KubeConfigSelectionComponent, + KubeConfigImportComponent + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigRegistrationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.ts new file mode 100644 index 0000000000..1708ec7897 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-kube-config-registration', + templateUrl: './kube-config-registration.component.html', + styleUrls: ['./kube-config-registration.component.scss'] +}) +export class KubeConfigRegistrationComponent { } diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.html new file mode 100644 index 0000000000..77ce84fc8a --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.html @@ -0,0 +1,17 @@ +
+
+ insert_drive_file +

Select a Kube Config file to import clusters

+
+ + +
+
+
+ + +
+ +
+
\ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.scss new file mode 100644 index 0000000000..df0fad3cd0 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.scss @@ -0,0 +1,35 @@ +:host { + display: flex; + flex: 1; +} + +.kube-config-select { + &__panel { + align-items: center; + display: flex; + flex: 1; + justify-content: center; + } + &__upload { + text-align: center; + } + &__title { + font-size: 24px; + text-align: center; + } + &__icon { + font-size: 96px; + height: 96px; + opacity: .7; + width: 96px; + } + &__table { + flex: 1; + } + &__buttons { + padding-bottom: 12px; + button { + margin-right: 24px; + } + } +} \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.spec.ts new file mode 100644 index 0000000000..cc646004a4 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../kubernetes.testing.module'; +import { KubeConfigSelectionComponent } from './kube-config-selection.component'; + +describe('KubeConfigSelectionComponent', () => { + let component: KubeConfigSelectionComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigSelectionComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigSelectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.ts new file mode 100644 index 0000000000..4a89f59baf --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.ts @@ -0,0 +1,210 @@ +import { Component, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, combineLatest, Observable, of as observableOf, of } from 'rxjs'; +import { first, map, switchMap } from 'rxjs/operators'; + +import { HideSnackBar, ShowSnackBar } from '../../../../../../store/src/actions/snackBar.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +import { + TableHeaderSelectComponent, +} from '../../../../shared/components/list/list-table/table-header-select/table-header-select.component'; +import { KubeConfigHelper } from '../kube-config.helper'; +import { KubeConfigFileCluster } from '../kube-config.types'; +import { + ITableListDataSource, + RowState, +} from './../../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { ITableColumn } from './../../../../shared/components/list/list-table/table.types'; +import { KubeConfigTableCertComponent } from './kube-config-table-cert/kube-config-table-cert.component'; +import { KubeConfigTableName } from './kube-config-table-name/kube-config-table-name.component'; +import { KubeConfigTableSelectComponent } from './kube-config-table-select/kube-config-table-select.component'; +import { + KubeConfigTableSubTypeSelectComponent, +} from './kube-config-table-sub-type-select/kube-config-table-sub-type-select.component'; +import { KubeConfigTableUserSelectComponent } from './kube-config-table-user-select/kube-config-table-user-select.component'; + +export interface KubeConfigTableListDataSource extends ITableListDataSource { + editRowName: string; +} + +@Component({ + selector: 'app-kube-config-selection', + templateUrl: './kube-config-selection.component.html', + styleUrls: ['./kube-config-selection.component.scss'], + providers: [ + KubeConfigHelper + ], +}) +export class KubeConfigSelectionComponent { + + @Input() applyStarted: boolean; + public dataSource: KubeConfigTableListDataSource = { + connect: () => this.helper.clusters$, + disconnect: () => { }, + trackBy: (index, row) => row.name, + isTableLoading$: observableOf(false), + getRowState: (row: KubeConfigFileCluster, schemaKey: string): Observable => { + return row ? row._state.asObservable() : observableOf({}); + }, + selectAllIndeterminate: false, + selectAllChecked: false, + selectAllFilteredRows: () => { + // Should always go to true from indeterminate + this.dataSource.selectAllChecked = this.dataSource.selectAllIndeterminate ? true : !this.dataSource.selectAllChecked + this.dataSource.selectAllIndeterminate = false; // either all off or all on, cannot be indeterminate + + this.helper.clusters$.pipe( + first(), + switchMap(clusters => combineLatest(clusters.map(cluster => { + if (!cluster._invalid) { + cluster._selected = this.dataSource.selectAllChecked; + return this.helper.checkValidity(cluster).pipe(map(() => cluster)); + } + return of(cluster); + }))), + first(), + ).subscribe(clusters => { + this.checkCanGoNext(clusters); + }) + }, + editRow: null, + editRowName: null, + startEdit: (c: KubeConfigFileCluster) => { + this.dataSource.editRow = c; + }, + saveEdit: () => { + this.dataSource.editRow.name = this.dataSource.editRowName; + this.helper.update(this.dataSource.editRow); + delete this.dataSource.editRowName; + delete this.dataSource.editRow; + }, + cancelEdit: () => { + delete this.dataSource.editRowName; + delete this.dataSource.editRow; + }, + getRowUniqueId: (c: KubeConfigFileCluster) => c ? c._id : null + }; + + public columns: ITableColumn[] = [ + { + columnId: 'select', + headerCellComponent: TableHeaderSelectComponent, + cellComponent: KubeConfigTableSelectComponent, + class: 'table-column-select', + cellFlex: '0 0 48px' + }, + { + columnId: 'name', headerCell: () => 'Name', + cellComponent: KubeConfigTableName, + cellFlex: '3', + class: 'app-table__cell--table-no-v-padding' + }, + { + columnId: 'url', headerCell: () => 'URL', + cellDefinition: { + valuePath: 'cluster.server' + }, + cellFlex: '4', + }, + { + columnId: 'type', headerCell: () => 'Type', + cellFlex: '1', + cellComponent: KubeConfigTableSubTypeSelectComponent + }, + { + columnId: 'user', headerCell: () => 'User', + cellFlex: '4', + cellComponent: KubeConfigTableUserSelectComponent + }, + { + columnId: 'cert', headerCell: () => 'Skip SSL Validation', + cellFlex: '0 0 62px', + class: 'app-table__cell--table-centred', + cellComponent: KubeConfigTableCertComponent + } + ]; + + // Is the import data valid? + valid = new BehaviorSubject(false); + valid$ = this.valid.asObservable(); + + canSetIntermediate = false; + + constructor( + private store: Store, + public helper: KubeConfigHelper + ) { + this.helper.clustersChanged = () => this.clustersChanged() + } + + // Save data for the next step to know the list of clusters to import + onNext = () => this.helper.clusters$.pipe( + first(), + map(clusters => ({ + success: true, + data: clusters + })) + ) + + clustersParse(cluster: string) { + this.store.dispatch(new HideSnackBar()); + this.helper.parse(cluster).pipe(first()).subscribe(errorString => { + if (errorString) { + this.store.dispatch(new ShowSnackBar(`Failed to load Kube Config: ${errorString}`, 'Close')) + } + }) + } + + onEnter = () => { + if (!this.applyStarted) { + return; + } + // Handle back from review step (ensure newly registered endpoints are taken into account) + this.helper.updateAll().pipe(first()).subscribe(() => { }) + } + + // Row changed event - update the next button and selection state + clustersChanged() { + this.helper.clusters$.pipe( + first() + ).subscribe(clusters => { + this.checkCanGoNext(clusters); + + // Check the select all state + let selectedCount = 0; + let totalCount = 0; + clusters.forEach(i => { + if (!i._invalid) { + totalCount++; + selectedCount += i._selected ? 1 : 0; + } + }); + + if (selectedCount === 0 || totalCount === selectedCount) { + this.dataSource.selectAllIndeterminate = false; + this.dataSource.selectAllChecked = (selectedCount !== 0); + } else { + this.dataSource.selectAllIndeterminate = true; + } + }) + + } + + // Can we proceed? + checkCanGoNext(clusters: KubeConfigFileCluster[]) { + let selected = 0; + let okay = 0; + clusters.forEach(i => { + if (i._selected) { + selected++; + if (!i._invalid) { + okay++; + } + } + }); + + // Must be at least one selected and they all must be okay to import + this.valid.next(selected > 0 && selected === okay); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.html new file mode 100644 index 0000000000..5d0a8d6faf --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.spec.ts new file mode 100644 index 0000000000..0b1bdaf8b8 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.spec.ts @@ -0,0 +1,33 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigTableCertComponent } from './kube-config-table-cert.component'; + +describe('KubeConfigTableCertComponent', () => { + let component: KubeConfigTableCertComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableCertComponent], + providers: [ + KubeConfigHelper + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableCertComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.ts new file mode 100644 index 0000000000..c776a3d0bf --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.ts @@ -0,0 +1,73 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Component, Input } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { timeout } from 'rxjs/operators'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +type CertResponse = { + Status: number; + Required: boolean; + Error: boolean; + Message: string; +} + +@Component({ + selector: 'app-kube-config-table-cert', + templateUrl: './kube-config-table-cert.component.html', + styleUrls: ['./kube-config-table-cert.component.scss'] +}) +export class KubeConfigTableCertComponent extends TableCellCustom { + + initialValue = new BehaviorSubject<{ + checked: boolean + }>(null) + initialValue$ = this.initialValue.asObservable(); + + private pRow: KubeConfigFileCluster; + @Input() + set row(row: KubeConfigFileCluster) { + if (!this.pRow) { + this.pRow = row; + if (row.cluster['insecure-skip-tls-verify']) { + // User has manually specified default skip option + this.initialValue.next({ + checked: true + }); + } else { + // Manually check if a cert is required, if so tick by default + this.http.get(`/pp/v1/kube/cert?url=${row.cluster.server}`).pipe( + timeout(5000), + ).subscribe( + // Success, no cert required + (res: CertResponse) => this.update(res.Required), + // Failed, check for specific cert required error + (e: HttpErrorResponse) => this.update(false) + ) + } + } + } + get row(): KubeConfigFileCluster { + return this.pRow; + } + + constructor( + private helper: KubeConfigHelper, + private http: HttpClient + ) { + super() + } + + private update(checked: boolean) { + this.initialValue.next({ checked }); + this.valueChanged(checked); + } + + valueChanged(value) { + this.row.cluster['insecure-skip-tls-verify'] = value; + this.helper.update(this.row); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.html new file mode 100644 index 0000000000..39cdc6abec --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.html @@ -0,0 +1,9 @@ +
+ + + + {{row.name}} + +
\ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.scss new file mode 100644 index 0000000000..1d85cadb70 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.scss @@ -0,0 +1,12 @@ +.name { + align-items: center; + display: flex; + + .cell-edit-variable { + flex: 1; + } + + app-table-cell-edit { + margin-bottom: 3px; + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.spec.ts new file mode 100644 index 0000000000..c37211591f --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.spec.ts @@ -0,0 +1,34 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IListDataSource } from '../../../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigFileCluster } from '../../kube-config.types'; +import { KubeConfigTableName } from './kube-config-table-name.component'; + +describe('KubeConfigTableName', () => { + let component: KubeConfigTableName; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableName] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableName); + component = fixture.componentInstance; + component.dataSource = { + getRowUniqueId: (row) => "" + } as IListDataSource + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.ts new file mode 100644 index 0000000000..576f4c19f2 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-name', + templateUrl: './kube-config-table-name.component.html', + styleUrls: ['./kube-config-table-name.component.scss'] +}) +export class KubeConfigTableName extends TableCellCustom { } diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.html new file mode 100644 index 0000000000..944326f9e6 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.spec.ts new file mode 100644 index 0000000000..3bf72f8139 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; +import { KubeConfigTableSelectComponent } from './kube-config-table-select.component'; + +describe('KubeConfigTableSelectComponent', () => { + let component: KubeConfigTableSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableSelectComponent], + providers: [ + KubeConfigHelper + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableSelectComponent); + component = fixture.componentInstance; + component.row = {} as KubeConfigFileCluster; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.ts new file mode 100644 index 0000000000..b13745ed48 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-select', + templateUrl: './kube-config-table-select.component.html', + styleUrls: ['./kube-config-table-select.component.scss'] +}) +export class KubeConfigTableSelectComponent extends TableCellCustom { + + constructor(private helper: KubeConfigHelper) { + super(); + } + changed(v) { + this.row._selected = v.checked; + this.helper.update(this.row); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.html new file mode 100644 index 0000000000..92a8d0e90e --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.html @@ -0,0 +1,3 @@ + + {{ type.name }} + diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.spec.ts new file mode 100644 index 0000000000..c2ce574749 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; +import { KubeConfigTableSubTypeSelectComponent } from './kube-config-table-sub-type-select.component'; + +describe('KubeConfigTableSubTypeSelectComponent', () => { + let component: KubeConfigTableSubTypeSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableSubTypeSelectComponent], + providers: [ + KubeConfigHelper + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableSubTypeSelectComponent); + component = fixture.componentInstance; + component.row = {} as KubeConfigFileCluster; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.ts new file mode 100644 index 0000000000..8d487d1bc6 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit } from '@angular/core'; + +import { TableCellCustom } from '../../../../../../../core/src/shared/components/list/list.types'; +import { KubeConfigAuthHelper } from '../../kube-config-auth.helper'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-sub-type-select', + templateUrl: './kube-config-table-sub-type-select.component.html', + styleUrls: ['./kube-config-table-sub-type-select.component.scss'] +}) +export class KubeConfigTableSubTypeSelectComponent extends TableCellCustom implements OnInit { + + selected: string; + + subTypes: string[]; + + constructor(private helper: KubeConfigHelper) { + super(); + + this.subTypes = new KubeConfigAuthHelper().subTypes; + } + + ngOnInit() { + this.selected = this.row._subType || ''; + } + + valueChanged(value) { + this.row._subType = value; + this.helper.update(this.row); + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.html new file mode 100644 index 0000000000..1c1c629a8a --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.html @@ -0,0 +1,9 @@ +
+ + Register Only + {{ user.name }} + +
+
+ No user found, register only +
\ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.spec.ts new file mode 100644 index 0000000000..3ada12009f --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.spec.ts @@ -0,0 +1,39 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; +import { KubeConfigTableUserSelectComponent } from './kube-config-table-user-select.component'; + +describe('KubeConfigTableUserSelectComponent', () => { + let component: KubeConfigTableUserSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [ + KubeConfigTableUserSelectComponent, + ], + providers: [ + KubeConfigHelper + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableUserSelectComponent); + component = fixture.componentInstance; + component.row = { + _users: [] + } as KubeConfigFileCluster; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.ts new file mode 100644 index 0000000000..f39471a321 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-user-select', + templateUrl: './kube-config-table-user-select.component.html', + styleUrls: ['./kube-config-table-user-select.component.scss'] +}) +export class KubeConfigTableUserSelectComponent extends TableCellCustom implements OnInit { + + hasUser = false; + selected: string; + + constructor(private helper: KubeConfigHelper) { + super(); + } + + ngOnInit() { + this.selected = this.row._user || ''; + this.hasUser = this.row._users.length > 0; + } + + valueChanged(value) { + this.row._user = value; + this.helper.update(this.row); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.helper.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.helper.ts new file mode 100644 index 0000000000..645505ea91 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.helper.ts @@ -0,0 +1,201 @@ +import { Injectable } from '@angular/core'; +import * as yaml from 'js-yaml'; +import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; +import { filter, first, map, tap } from 'rxjs/operators'; + +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; +import { EndpointsService } from '../../../core/endpoints.service'; +import { createGuid } from '../../../core/utils.service'; +import { getFullEndpointApiUrl } from '../../../features/endpoints/endpoint-helpers'; +import { RowState } from '../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { KubeConfigAuthHelper } from './kube-config-auth.helper'; +import { KubeConfigFile, KubeConfigFileCluster } from './kube-config.types'; + +/** + * Helper to parse the kubeconfig and transform it into data + * that we can display in a table for selection + * + * Main issue is we only support one credential per endpoint, so need to format the data + * to offer the user ability to select which user to import + */ +@Injectable() +export class KubeConfigHelper { + + authHelper = new KubeConfigAuthHelper(); + + clusters = new BehaviorSubject(null) + clusters$ = this.clusters.asObservable().pipe( + filter(clusters => !!clusters) + ); + + constructor( + public endpointsService: EndpointsService, + ) { + } + + public clustersChanged: () => void; + public update = (cluster: KubeConfigFileCluster) => { + this.checkValidity(cluster).subscribe(() => this.clustersChanged()); + } + + public updateAll(): Observable { + return this.clusters$.pipe( + tap(clusters => clusters.forEach(cluster => this.update(cluster))), + ) + } + + public parse(config: string): Observable { + let doc: KubeConfigFile; + + const clusters: { [name: string]: KubeConfigFileCluster } = {}; + + try { + doc = yaml.safeLoad(config); + } catch (e) { + return of(`${e}`); + } + + // Need contexts, users and clusters + if (!doc || !doc.contexts || !doc.users || !doc.clusters) { + return of(`Configuration must have contexts, users and clusters`); + } + + // Go through all of the contexts and find the clusters + doc.contexts.forEach(ctx => { + const cluster = doc.clusters.find(item => item.name === ctx.context.cluster); + if (cluster) { + // Found the cluster + if (!clusters[cluster.name]) { + const clstr = { + ...cluster, + _users: [] + }; + clusters[cluster.name] = clstr; + clstr._state = new BehaviorSubject({}); + } + + // Get the user + const user = doc.users.find(item => item.name === ctx.context.user); + if (user) { + // Check we don't already have this user (remove duplicates) + const users = clusters[cluster.name]._users; + if (users.findIndex(usr => usr.name === user.name) === -1) { + clusters[cluster.name]._users.push(user); + if (ctx.name === doc['current-context']) { + // Auto-select this cluster/user if it is the current context + clusters[cluster.name]._user = user.name; + clusters[cluster.name]._selected = true; + } + } + } + } + }); + + // Go through all clusters, auto-select the user where this is only 1 and check validity + const clustersArray = Object.values(clusters); + clustersArray.forEach(cluster => { + if (cluster._users.length >= 1) { + cluster._user = cluster._users[0].name; + } + cluster._id = createGuid(); + }); + + // Check validity + return combineLatest( + clustersArray.map(cluster => this.checkValidity(cluster)) + ).pipe( + map(() => { + // Notify cluster changes + this.clustersChanged(); + this.clusters.next(Object.values(clusters)); + return ''; + }) + ); + } + + + // Check the validity of a cluster for import + public checkValidity(cluster: KubeConfigFileCluster): Observable { + // Check endpoint name + return combineLatest([ + this.endpointsService.endpoints$, + this.clusters.asObservable() // Might be called before we've loaded clusters, so used the non-filtered one + ]).pipe( + first(), + map(([eps, clusters]) => this.validate(Object.values(eps), cluster, clusters)) + ); + } + + private validate(endpoints: EndpointModel[], cluster: KubeConfigFileCluster, clusters: KubeConfigFileCluster[]) { + cluster._invalid = false; + let reset = true; + + const found = endpoints.find(item => item.name === cluster.name); + if (found) { + // If the URL is the same, then we will just connect to the existing endpoint + if (getFullEndpointApiUrl(found) === cluster.cluster.server && !!cluster._user) { + cluster._guid = found.guid; + cluster._state.next({ + message: 'This endpoint will be connected and not registered (endpoint is already registered)', + info: true + }); + reset = false; + } else { + // An endpoint with the same name (but different URL) already exists + cluster._invalid = true; + cluster._state.next({ message: 'An endpoint with this name already exists', warning: true }); + } + } else { + // Check endpoint url is not registered with a different name + if (endpoints.find(item => getFullEndpointApiUrl(item) === cluster.cluster.server)) { + cluster._invalid = true; + cluster._state.next({ message: 'An endpoint with this URL already exists', warning: true }); + } + } + + // Check the connection details + if (!cluster._invalid && cluster._user) { + const user = cluster._users.find(item => item.name === cluster._user); + if (user) { + const newState = this.authHelper.parseAuth(cluster, user); + if (!!newState && !!newState.message) { + reset = false; + cluster._invalid = newState.error || newState.warning + cluster._state.next(newState); + } + } + } + + // Register only (_additionalUserInfo.. specific to text warning) is true + // Connect only (endpoint exists) is true + // Show special warning + if (cluster._additionalUserInfo && cluster._guid) { + cluster._invalid = true; + reset = true; + cluster._state.next({ + message: 'This endpoint will not be registered or connected (endpoint is already registered, additional information required to connect)', + warning: true + }); + } + + if (clusters && !!clusters.find(candidate => candidate.name === cluster.name && candidate._id !== cluster._id)) { + cluster._invalid = true; + cluster._state.next({ message: 'An endpoint with this name already exists in the config file', warning: true }); + } + + if (!cluster.name) { + cluster._invalid = true; + cluster._state.next({ message: 'Cluster must have name', warning: true }); + } + + // Cluster is valid, so clear any warning or error message + if (!cluster._invalid && reset) { + cluster._state.next({}); + } + + // Ensure invalid rows aren't selected (user cannot unselect invalid rows) + if (cluster._invalid) { + cluster._selected = false; + } + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.types.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.types.ts new file mode 100644 index 0000000000..f2d8b7abde --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.types.ts @@ -0,0 +1,99 @@ +import { Observable, Subject } from 'rxjs'; + +import { EndpointAuthTypeConfig } from '../../../core/extension/extension-types'; +import { RowState } from '../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { ActionStatus } from './../../../../../store/src/reducers/api-request-reducer/types'; + +// Types for a Kubernetes Configuration file + +export interface KubeConfigFileCluster { + name: string; + cluster: { + 'certificate-authority': string; + 'certificate-authority-data': string; + 'insecure-skip-tls-verify': boolean; + server: string; + }; + // Selected user to import + _user: string; + _users: KubeConfigFileUser[]; + // _onUpdate: (row) => {}; + // Is the cluster selected for import? + _selected: boolean; + // Is this cluster invalid? i.e. requires more information + _invalid: boolean; + // row state + _state: Subject; + // status of import + _status: string; + // guid of the existing endpoint for this cluster + _guid: string; + // subtype + _subType?: string; + // additional info is required in order to connect, hints at register only, though is specific due to warning message + _additionalUserInfo: boolean; + // unique identifier + _id: string; +} + +export interface KubeConfigFileUser { + name: string; + user: KubeConfigFileUserDetail; + _authData: KubeConfigImportAuthConfig; +} + +export interface KubeConfigFileUserDetail { + 'client-certificate'?: string; + 'client-key'?: string; + 'client-certificate-data'?: string; + 'client-key-data'?: string; + token?: string; + exec?: any +} + +export interface KubeConfigFileContext { + name: string; + context: { + cluster: string; + user: string; + }; +} + +export interface KubeConfigFile { + apiVersion: string; + clusters: KubeConfigFileCluster[]; + contexts: KubeConfigFileContext[]; + 'current-context': string; + kind: string; + users: KubeConfigFileUser[]; +} + +export interface KubeConfigImportAction { + action: string; + description: string; + cluster: KubeConfigFileCluster; + user?: KubeConfigFileUser; + status?: ActionStatus; + state: Subject; + actionState$?: Observable; + actionState: Subject; + depends?: KubeConfigImportAction; +} + +export interface KubeImportState { + busy: boolean; + error: boolean; + completed: boolean; + message: string; +} + +export interface EndpointConfig { + type: string; + authTypes: EndpointAuthTypeConfig[]; +} + +export interface KubeConfigImportAuthConfig { + subType: string; + authType: string; + values: { [key: string]: string }; +} diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts index eac9f993c0..15e0ea101b 100644 --- a/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts +++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts @@ -20,6 +20,7 @@ import { KubernetesConfigAuthFormComponent, } from './auth-forms/kubernetes-config-auth-form/kubernetes-config-auth-form.component'; import { KubernetesGKEAuthFormComponent } from './auth-forms/kubernetes-gke-auth-form/kubernetes-gke-auth-form.component'; +import { KubeConfigRegistrationComponent } from './kube-config-registration/kube-config-registration.component'; import { kubeEntityCatalog } from './kubernetes-entity-catalog'; import { KUBERNETES_ENDPOINT_TYPE, @@ -129,31 +130,44 @@ export function generateKubernetesEntities(): StratosBaseCatalogEntity[] { urlValidation: undefined, authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CERT_AUTH], kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG]], renderPriority: 4, - subTypes: [{ - type: 'caasp', - label: 'SUSE CaaS Platform', - authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG]], - logoUrl: '/core/assets/custom/caasp.png', - renderPriority: 5 - }, { - type: 'aks', - label: 'Azure AKS', - authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG_AZ]], - logoUrl: '/core/assets/custom/aks.svg', - renderPriority: 6 - }, { - type: 'eks', - label: 'Amazon EKS', - authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.AWS_IAM]], - logoUrl: '/core/assets/custom/eks.svg', - renderPriority: 6 - }, { - type: 'gke', - label: 'Google Kubernetes Engine', - authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.GKE]], - logoUrl: '/core/assets/custom/gke.svg', - renderPriority: 6 - }], + subTypes: [ + { + type: 'config', + label: 'Import Kubeconfig', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG]], + logoUrl: '/core/assets/custom/kube_import.png', + renderPriority: 3, + registrationComponent: KubeConfigRegistrationComponent, + }, + { + type: 'caasp', + label: 'SUSE CaaS Platform', + labelShort: 'CaaSP', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG]], + logoUrl: '/core/assets/custom/caasp.png', + renderPriority: 5, + }, { + type: 'aks', + label: 'Azure AKS', + labelShort: 'AKS', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG_AZ]], + logoUrl: '/core/assets/custom/aks.svg', + renderPriority: 6 + }, { + type: 'eks', + label: 'Amazon EKS', + labelShort: 'EKS', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.AWS_IAM]], + logoUrl: '/core/assets/custom/eks.svg', + renderPriority: 6 + }, { + type: 'gke', + label: 'Google Kubernetes Engine', + labelShort: 'GKE', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.GKE]], + logoUrl: '/core/assets/custom/gke.svg', + renderPriority: 6 + }], }; return [ generateEndpointEntity(endpointDefinition), diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts index ed05e9af79..0c2111ed22 100644 --- a/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts +++ b/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts @@ -1,4 +1,3 @@ -/* tslint:disable:max-line-length */ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { NgxChartsModule } from '@swimlane/ngx-charts'; @@ -93,6 +92,7 @@ import { KubernetesNodesTabComponent } from './tabs/kubernetes-nodes-tab/kuberne import { KubernetesPodsTabComponent } from './tabs/kubernetes-pods-tab/kubernetes-pods-tab.component'; import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kubernetes-summary.component'; +/* tslint:disable:max-line-length */ /* tslint:enable */ @@ -146,7 +146,7 @@ import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kub KubernetesResourceViewerComponent, KubeServiceCardComponent, KubedashConfigurationComponent, - KubernetesPodContainersComponent + KubernetesPodContainersComponent, ], providers: [ KubernetesService, @@ -170,7 +170,7 @@ import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kub KubernetesPodStatusComponent, KubeServiceCardComponent, KubernetesResourceViewerComponent, - KubernetesPodContainersComponent + KubernetesPodContainersComponent, ], exports: [ KubernetesResourceViewerComponent diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes.setup.module.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes.setup.module.ts index 45f33e0292..03989028b4 100644 --- a/custom-src/frontend/app/custom/kubernetes/kubernetes.setup.module.ts +++ b/custom-src/frontend/app/custom/kubernetes/kubernetes.setup.module.ts @@ -14,6 +14,29 @@ import { KubernetesConfigAuthFormComponent, } from './auth-forms/kubernetes-config-auth-form/kubernetes-config-auth-form.component'; import { KubernetesGKEAuthFormComponent } from './auth-forms/kubernetes-gke-auth-form/kubernetes-gke-auth-form.component'; +import { KubeConfigImportComponent } from './kube-config-registration/kube-config-import/kube-config-import.component'; +import { + KubeConfigTableImportStatusComponent, +} from './kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component'; +import { KubeConfigRegistrationComponent } from './kube-config-registration/kube-config-registration.component'; +import { + KubeConfigSelectionComponent, +} from './kube-config-registration/kube-config-selection/kube-config-selection.component'; +import { + KubeConfigTableCertComponent, +} from './kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component'; +import { + KubeConfigTableName, +} from './kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component'; +import { + KubeConfigTableSelectComponent, +} from './kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component'; +import { + KubeConfigTableSubTypeSelectComponent, +} from './kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component'; +import { + KubeConfigTableUserSelectComponent, +} from './kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component'; import { kubeEntityCatalog } from './kubernetes-entity-catalog'; import { KUBERNETES_ENDPOINT_TYPE } from './kubernetes-entity-factory'; import { generateKubernetesEntities } from './kubernetes-entity-generator'; @@ -21,7 +44,6 @@ import { BaseKubeGuid } from './kubernetes-page.types'; import { KubernetesStoreModule } from './kubernetes.store.module'; import { KubernetesEndpointService } from './services/kubernetes-endpoint.service'; - @NgModule({ imports: [ EntityCatalogModule.forFeature(generateKubernetesEntities), @@ -35,6 +57,15 @@ import { KubernetesEndpointService } from './services/kubernetes-endpoint.servic KubernetesAWSAuthFormComponent, KubernetesConfigAuthFormComponent, KubernetesGKEAuthFormComponent, + KubeConfigRegistrationComponent, + KubeConfigSelectionComponent, + KubeConfigImportComponent, + KubeConfigTableSelectComponent, + KubeConfigTableUserSelectComponent, + KubeConfigTableImportStatusComponent, + KubeConfigTableSubTypeSelectComponent, + KubeConfigTableName, + KubeConfigTableCertComponent ], providers: [ BaseKubeGuid, @@ -45,6 +76,13 @@ import { KubernetesEndpointService } from './services/kubernetes-endpoint.servic KubernetesAWSAuthFormComponent, KubernetesConfigAuthFormComponent, KubernetesGKEAuthFormComponent, + KubeConfigRegistrationComponent, + KubeConfigTableSelectComponent, + KubeConfigTableUserSelectComponent, + KubeConfigTableImportStatusComponent, + KubeConfigTableSubTypeSelectComponent, + KubeConfigTableName, + KubeConfigTableCertComponent ] }) export class KubernetesSetupModule { diff --git a/custom-src/frontend/assets/custom/kube_import.png b/custom-src/frontend/assets/custom/kube_import.png new file mode 100644 index 0000000000000000000000000000000000000000..62d9f0bdd2e7037bb9f33580a7829e14dd93d0a1 GIT binary patch literal 13298 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 literal 0 HcmV?d00001 diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.html b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.html index 9b9fcb9301..3f634c71e4 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.html +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.html @@ -2,4 +2,5 @@ -{{row.value}} \ No newline at end of file +{{row.value}} \ No newline at end of file diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.scss b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.scss index eb9c8d43a8..6cd7d23bf0 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.scss +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.scss @@ -7,3 +7,7 @@ mat-form-field { .cell-edit-variable { width: 100%; } + +.cell-value-variable { + cursor: pointer; +} diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-quotas/cf-quotas-data-source.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-quotas/cf-quotas-data-source.service.ts index f2bf84af31..d5018ccb88 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-quotas/cf-quotas-data-source.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-quotas/cf-quotas-data-source.service.ts @@ -50,7 +50,7 @@ export class CfQuotasDataSourceService extends ListDataSource { map(requestInfo => ({ deleting: requestInfo.deleting.busy, error: requestInfo.deleting.error, - message: requestInfo.deleting.error ? `Failed to delete quota: ${requestInfo.message}` : null + message: requestInfo.deleting.error ? `Failed to delete quota: ${requestInfo.deleting.message}` : null })) ); }; diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-space-quotas/cf-space-quotas-data-source.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-space-quotas/cf-space-quotas-data-source.service.ts index 4a880b804b..ccfb93879a 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-space-quotas/cf-space-quotas-data-source.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-space-quotas/cf-space-quotas-data-source.service.ts @@ -50,7 +50,7 @@ export class CfOrgSpaceQuotasDataSourceService extends ListDataSource ({ deleting: requestInfo.deleting.busy, error: requestInfo.deleting.error, - message: requestInfo.deleting.error ? `Failed to delete space quota: ${requestInfo.message}` : null + message: requestInfo.deleting.error ? `Failed to delete space quota: ${requestInfo.deleting.message}` : null })) ); }; diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss index 4f657dbc93..9fc21b86f9 100644 --- a/src/frontend/packages/core/sass/_all-theme.scss +++ b/src/frontend/packages/core/sass/_all-theme.scss @@ -181,6 +181,6 @@ $side-nav-light-active: #484848; $warn: map-get($theme, warn); $primary: map-get($theme, primary); $white: #fff; // Use default palette for status - @return (success: map-get($mat-green, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, ); + @return (success: map-get($mat-green, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, info: map-get($mat-blue, 500)); } } diff --git a/src/frontend/packages/core/sass/components/mat-table.scss b/src/frontend/packages/core/sass/components/mat-table.scss index c623288894..6c0d902e78 100644 --- a/src/frontend/packages/core/sass/components/mat-table.scss +++ b/src/frontend/packages/core/sass/components/mat-table.scss @@ -12,7 +12,7 @@ $mat-table-header-paginator-font-size: 13px; .stratos { // Only put right padding between cells - .mat-cell, .mat-header-cell { + .mat-cell, mat-footer-cell, mat-header-cell { padding: 10px 10px 10px 0; } diff --git a/src/frontend/packages/core/src/core/utils.service.ts b/src/frontend/packages/core/src/core/utils.service.ts index 415a8f89c6..83de981544 100644 --- a/src/frontend/packages/core/src/core/utils.service.ts +++ b/src/frontend/packages/core/src/core/utils.service.ts @@ -349,3 +349,11 @@ export const arraysEqual = (a: any[], b: any[]): boolean => { // Falsy/Truthy return false; }; + +export const createGuid = (): string => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + var r = Math.random() * 16 | 0, + v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} diff --git a/src/frontend/packages/core/src/features/endpoints/connect.service.ts b/src/frontend/packages/core/src/features/endpoints/connect.service.ts index 015ff5b14d..d4e9dac73d 100644 --- a/src/frontend/packages/core/src/features/endpoints/connect.service.ts +++ b/src/frontend/packages/core/src/features/endpoints/connect.service.ts @@ -201,6 +201,25 @@ export class ConnectEndpointService { ); } + public getConnectingObservable() { + return this.isBusy$.pipe( + pairwise(), + filter(([oldBusy, newBusy]) => { + return !(oldBusy === true && newBusy === false); + }), + withLatestFrom(this.update$), + map(([, updateSection]) => ({ + ...updateSection, + completed: !updateSection.busy, + })), + startWith({ + busy: true, + completed: false, + error: false + }) + ); + } + public destroy() { safeUnsubscribe(...this.subs); } diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.html index 0aa360488a..5a3116dce9 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.html @@ -1,5 +1,5 @@ -

Register a new Endpoint

+

Register Endpoint

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 85fedb80c1..e354112437 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,16 +1,16 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { RouterNav } from '../../../../../../store/src/actions/router.actions'; import { GeneralEntityAppState } from '../../../../../../store/src/app-state'; -import { selectSessionData } from '../../../../../../store/src/reducers/auth.reducer'; import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog'; +import { IStratosEndpointDefinition } from '../../../../../../store/src/entity-catalog/entity-catalog.types'; +import { selectSessionData } from '../../../../../../store/src/reducers/auth.reducer'; 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'; -import { Observable } from 'rxjs'; -import { IStratosEndpointDefinition } from '../../../../../../store/src/entity-catalog/entity-catalog.types'; interface ICreateEndpointTilesData extends ITileData { type: string; @@ -103,7 +103,8 @@ export class CreateEndpointBaseStepComponent { }, { type: endpoint.type, - parentType: endpoint.parentType + parentType: endpoint.parentType, + component: endpoint.registrationComponent, } ); }); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.html index d4a8beb761..076abff82d 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.html @@ -1,8 +1,8 @@ -

Register a new Endpoint

+

Register Endpoint

- + @@ -11,4 +11,6 @@

Register a new Endpoint

[finishButtonText]="connect.doConnect ? 'Connect' : 'Finish'">
-
\ No newline at end of file + + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.ts index 1bd0f9c07c..d44c7af725 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild, ViewContainerRef, ComponentRef, OnInit, OnDestroy, ComponentFactory, ComponentFactoryResolver } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; @@ -10,16 +10,40 @@ import { getIdFromRoute } from '../../../core/utils.service'; templateUrl: './create-endpoint.component.html', styleUrls: ['./create-endpoint.component.scss'] }) -export class CreateEndpointComponent { +export class CreateEndpointComponent implements OnInit, OnDestroy { showConnectStep: boolean; - constructor(activatedRoute: ActivatedRoute) { + component: any; + @ViewChild('customComponent', { read: ViewContainerRef, static: true }) customComponentContainer; + componentRef: ComponentRef; + + constructor(activatedRoute: ActivatedRoute, private resolver: ComponentFactoryResolver) { const epType = getIdFromRoute(activatedRoute, 'type'); const epSubType = getIdFromRoute(activatedRoute, 'subtype'); const endpoint = entityCatalog.getEndpoint(epType, epSubType); + + this.component = endpoint.definition.registrationComponent; this.showConnectStep = !endpoint.definition.unConnectable ? endpoint.definition.authTypes && !!endpoint.definition.authTypes.length : false; } + + ngOnInit() { + this.customComponentContainer.clear(); + if (this.componentRef) { + this.componentRef.destroy(); + } + if (this.component) { + const factory: ComponentFactory = this.resolver.resolveComponentFactory(this.component); + this.componentRef = this.customComponentContainer.createComponent(factory); + } + } + + ngOnDestroy() { + if (this.componentRef) { + this.componentRef.destroy(); + } + } + } diff --git a/src/frontend/packages/core/src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.ts b/src/frontend/packages/core/src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.ts index 6637ca8748..fc0663320a 100644 --- a/src/frontend/packages/core/src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.ts +++ b/src/frontend/packages/core/src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.ts @@ -36,7 +36,8 @@ export class ActionMonitorComponentState { this.currentState = this.getStateObservable(entityMonitor, monitorState); } - private getStateObservable(entityMonitor: EntityMonitor, monitorState: AppMonitorComponentTypes) { + private getStateObservable(entityMonitor: EntityMonitor, monitorState: AppMonitorComponentTypes) + : Observable { switch (monitorState) { case AppMonitorComponentTypes.DELETE: return this.getDeletingState(entityMonitor); @@ -122,6 +123,10 @@ export class ActionMonitorComponentState { }) export class AppActionMonitorIconComponent implements OnInit { + // State observable - use this instead of creating one + @Input() + public state: Observable; + @Input() public entityKey: string; @@ -143,6 +148,9 @@ export class AppActionMonitorIconComponent implements OnInit { constructor(private entityMonitorFactory: EntityMonitorFactory) { } ngOnInit() { + if (this.state) { + this.currentState = this.state; + } else { const state: ActionMonitorComponentState = new ActionMonitorComponentState( this.entityMonitorFactory, this.id, @@ -151,5 +159,6 @@ export class AppActionMonitorIconComponent implements OnInit { this.updateKey ); this.currentState = state.currentState; + } } } diff --git a/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.html b/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.html index 6e9a288393..1c9713853c 100644 --- a/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.html +++ b/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.html @@ -1 +1,4 @@ - +
+ + +
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.ts b/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.ts index 2895bce18d..345af4a5c6 100644 --- a/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.ts +++ b/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.ts @@ -1,8 +1,7 @@ -import { DataSource } from '@angular/cdk/table'; import { Component, Input, OnInit } from '@angular/core'; import { schema } from 'normalizr'; import { never as observableNever, Observable, of as observableOf } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, publishReplay, refCount } from 'rxjs/operators'; import { EntitySchema } from '../../../../../store/src/helpers/entity-schema'; import { EntityMonitorFactory } from '../../../../../store/src/monitors/entity-monitor.factory.service'; @@ -26,7 +25,7 @@ import { ITableColumn } from '../list/list-table/table.types'; export class AppActionMonitorComponent implements OnInit { @Input() - private data$: Observable> = observableNever(); + public data$: Observable> = observableNever(); @Input() public entityKey: string; @@ -58,7 +57,7 @@ export class AppActionMonitorComponent implements OnInit { @Input() public columns: ITableColumn[] = []; - public dataSource: DataSource; + public dataSource: ITableListDataSource; public allColumns: ITableColumn[] = []; @@ -82,9 +81,16 @@ export class AppActionMonitorComponent implements OnInit { cellFlex: '0 0 24px' }; + // Some obs will only ever emit once, once consumed in template this meant table never received emitted data + // so wrap in publish replay + const replayData = this.data$.pipe( + publishReplay(1), + refCount() + ) + this.allColumns = [...this.columns, monitorColumn]; this.dataSource = { - connect: () => this.data$, + connect: () => replayData, disconnect: () => { }, trackBy: (index, item) => { const fn = monitorColumn.cellConfig(item).getId; @@ -117,7 +123,7 @@ export class AppActionMonitorComponent implements OnInit { }) ); } - } as ITableListDataSource; + }; } diff --git a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.html b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.html index 195ae17f56..edef8e7b79 100644 --- a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.html +++ b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.html @@ -1,6 +1,6 @@
- +
No file selected
{{ name }}
@@ -10,4 +10,5 @@
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.ts b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.ts index a08b287010..b99327bd0a 100644 --- a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.ts +++ b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.ts @@ -28,8 +28,12 @@ export class FileInputComponent implements OnInit, OnDestroy { @Input() accept: string; @Output() onFileSelect: EventEmitter = new EventEmitter(); + @Output() onFileData: EventEmitter = new EventEmitter(); + @Input() fileFormControlName; + @Input() buttonLabel = ''; + private files: File[]; public name = ''; @@ -65,7 +69,9 @@ export class FileInputComponent implements OnInit, OnDestroy { this.onFileSelect.emit(this.files[0]); if (!!this.formGroupControl) { - this.handleFormControl(this.files[0]); + this.handleFileData(this.files[0], (value) => this.updateFileState(value)); + } else { + this.handleFileData(this.files[0], (value) => this.onFileData.emit(value)); } if (this.files.length > 0) { this.name = this.files[0].name; @@ -79,17 +85,18 @@ export class FileInputComponent implements OnInit, OnDestroy { return false; } - handleFormControl(file) { + handleFileData(file, done) { const reader = new FileReader(); reader.onload = () => { - this.updateFileState(reader.result); + done(reader.result); }; reader.onerror = () => { // Clear the form and thus make it invalid on error - this.updateFileState(null); + done(null); }; reader.readAsText(file); } + private updateFileState(value: string | ArrayBuffer) { this.formGroupControl.control.controls[this.fileFormControlName].setValue(value); } diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts index 4b1af3bd34..96c4b0443e 100644 --- a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts @@ -64,11 +64,29 @@ interface ICoreListDataSource extends DataSource { trackBy(index: number, item: T); } -export interface ITableListDataSource extends ICoreListDataSource { +interface ICoreTableListDataSource extends ICoreListDataSource { + isTableLoading$?: Observable; + + selectAllChecked?: boolean; // Select items - remove once ng-content can exist in md-table + selectAllIndeterminate?: boolean; // Select all checkbox as indeterminate + selectedRows?: Map; // Select items - remove once ng-content can exist in md-table + selectedRows$?: ReplaySubject>; // Select items - remove once ng-content can exist in md-table + selectAllFilteredRows?: () => void; // Select items - remove once ng-content can exist in md-table + selectedRowToggle?: (row: T, multiMode?: boolean) => void; // Select items - remove once ng-content can exist in md-table + selectClear?: () => void; + + editRow?: T; // Edit items - remove once ng-content can exist in md-table + startEdit?: (row: T) => void; // Edit items - remove once ng-content can exist in md-table + saveEdit?: () => void; // Edit items - remove once ng-content can exist in md-table + cancelEdit?: () => void; // Edit items - remove once ng-content can exist in md-table + getRowUniqueId?: getRowUniqueId; +} + +export interface ITableListDataSource extends ICoreTableListDataSource { isTableLoading$: Observable; } -export interface IListDataSource extends ICoreListDataSource, EntityCatalogEntityConfig { +export interface IListDataSource extends ICoreListDataSource, ICoreTableListDataSource, EntityCatalogEntityConfig { pagination$: Observable; isLocal?: boolean; localDataFunctions?: (( @@ -94,20 +112,11 @@ export interface IListDataSource extends ICoreListDataSource, EntityCatalo filter$: Observable; sort$: Observable; - editRow: T; // Edit items - remove once ng-content can exist in md-table - selectAllChecked: boolean; // Select items - remove once ng-content can exist in md-table - selectedRows: Map; // Select items - remove once ng-content can exist in md-table - selectedRows$: ReplaySubject>; // Select items - remove once ng-content can exist in md-table + getRowUniqueId: getRowUniqueId; entitySelectConfig?: EntitySelectConfig; // For multi action lists, this is used to configure the entity select. - selectAllFilteredRows(); // Select items - remove once ng-content can exist in md-table - selectedRowToggle(row: T, multiMode?: boolean); // Select items - remove once ng-content can exist in md-table - selectClear(); - startEdit(row: T); // Edit items - remove once ng-content can exist in md-table - saveEdit(); // Edit items - remove once ng-content can exist in md-table - cancelEdit(); // Edit items - remove once ng-content can exist in md-table destroy(); /** * Set's data source specific text filter param diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.html index 125c4a88fc..7762b74c15 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.html @@ -1,11 +1,14 @@ -
- - - -
+ + + + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.scss b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.scss index 9681b2e526..e964f6220f 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.scss @@ -1,4 +1,14 @@ -div { +.edit { display: flex; justify-content: flex-end; + &--subtle { + .mat-icon-button { + font-size: 18px; + height: 25px; + line-height: 25px; + opacity: 60%; + padding-left: 5px; + width: 35px; + } + } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.ts index 5f38b44101..1aa4769a4f 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.ts @@ -1,5 +1,6 @@ -/* tslint:disable:no-access-missing-member https://github.com/mgechev/codelyzer/issues/191*/ -import { Component, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; + +import { IListDataSource } from '../../data-sources-controllers/list-data-source-types'; import { TableCellCustom } from '../../list.types'; @Component({ @@ -7,4 +8,20 @@ import { TableCellCustom } from '../../list.types'; templateUrl: './table-cell-edit.component.html', styleUrls: ['./table-cell-edit.component.scss'] }) -export class TableCellEditComponent extends TableCellCustom { } +export class TableCellEditComponent extends TableCellCustom { + + @Input() + row: T; + + @Input() + dataSource: IListDataSource; + + @Input() + subtle: boolean; + + isEditing(): boolean { + return this.dataSource.editRow ? + this.dataSource.getRowUniqueId(this.row) === this.dataSource.getRowUniqueId(this.dataSource.editRow) : + false + } +} diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-header-select/table-header-select.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table-header-select/table-header-select.component.html index c2f860861e..51c00db460 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-header-select/table-header-select.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-header-select/table-header-select.component.html @@ -1,2 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.html index be2ec31af4..b715c964f2 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.html @@ -1,5 +1,5 @@
+ [ngClass]="{'table-row-wrapper__blocked': isBlocked$ | async, 'table-row-wrapper__info': inInfoState$ | async, 'table-row-wrapper__warning': inWarningState$ | async,'table-row-wrapper__errored': inErrorState$ | async}">
Deleting
@@ -7,7 +7,7 @@
+ [ngClass]="{'in-expanded-row': !!inExpandedRow, 'has-expanded-row': expandComponent, 'has-error-row': errorMessage$ | async}">
- warning -
+
+
+ warning + info +
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.scss b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.scss index 06621f3b97..535edff498 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.scss @@ -16,7 +16,8 @@ } } &__errored, - &__warning { + &__warning, + &__info { .table-row__error { display: flex; } @@ -64,14 +65,23 @@ &__error { align-items: center; display: none; + &-message { flex: 1; line-height: 20px; - margin: 15px 36px; + margin-left: 10px; text-align: left; } - &-icon { - padding-left: 24px; + &-spacer { + align-self: stretch; + flex: 0 0 20px; + &__prominentErrorBar { + flex: 0 0 68px; + } + } + &-content { + flex: 1; + padding: 0 10px 10px 0; } } &__blocker { @@ -93,6 +103,7 @@ } .table-row__inner__expansion.mat-expansion-panel { + border-radius: unset; width: 100%; .table-row__inner__expansion--header { @@ -108,4 +119,9 @@ &.in-expanded-row { border-left-width: 1px; } + + &.has-error-row { + // Remove the bottom border if there's an error underneath it + border-bottom-width: 0; + } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.theme.scss b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.theme.scss index 0ac4538dad..07be004cb3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.theme.scss @@ -2,6 +2,7 @@ $status-colors: map-get($app-theme, status); $error-color: map-get($status-colors, danger); $warn-color: map-get($status-colors, warning); + $info-color: map-get($status-colors, info); $text-color: map-get($status-colors, text); $primary: map-get($theme, primary); $primary-color: mat-color($primary); @@ -22,26 +23,24 @@ } .table-row-wrapper { &__errored { - .table-row { - background-color: transparentize($error-color, .9); - } - .table-row__error { - background-color: $error-color; - color: $text-color; - } - .table-row__error-message { - a { - color: $text-color; + .table-row__error-content { + mat-icon { + color: $error-color; } } } &__warning { - .table-row { - background-color: transparentize($warn-color, .9); + .table-row__error-content { + mat-icon { + color: $warn-color; + } } - .table-row__error { - background-color: $warn-color; - color: $text-color; + } + &__info { + .table-row__error-content { + mat-icon { + color: $info-color; + } } } &__highlighted { diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.ts index 1c3c08d120..2c8325d488 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.ts @@ -38,13 +38,16 @@ export class TableRowComponent extends CdkRow implements OnInit { @Input() minRowHeight: string; @Input() inExpandedRow: boolean; @Input() rowId: string; + @Input() prominentErrorBar: boolean; public inErrorState$: Observable; public inWarningState$: Observable; + public inInfoState$: Observable; public errorMessage$: Observable; public isBlocked$: Observable; public isHighlighted$: Observable; public isDeleting$: Observable; + public isWarningIcon$: Observable; public defaultMinRowHeight = '50px'; private expandedComponentRef: ComponentRef; @@ -64,6 +67,9 @@ export class TableRowComponent extends CdkRow implements OnInit { this.inWarningState$ = this.rowState.pipe( map(state => state.warning) ); + this.inInfoState$ = this.rowState.pipe( + map(state => state.info) + ); this.errorMessage$ = this.rowState.pipe( map(state => state.message) ); @@ -76,6 +82,9 @@ export class TableRowComponent extends CdkRow implements OnInit { this.isDeleting$ = this.rowState.pipe( map(state => state.deleting) ); + this.isWarningIcon$ = this.rowState.pipe( + map(state => state.error || state.warning) + ); } // Ensure we 'register' with the expander service. This also helps with page changes diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html index 4bd2ce8004..fe4fa52887 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html @@ -37,7 +37,7 @@ + [rowId]="dataSource.trackBy(null, row)" [prominentErrorBar]="prominentErrorBar"> diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.scss b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.scss index 8c2db26e14..c2dc1fe36a 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.scss @@ -5,7 +5,6 @@ mat-cell, mat-header-cell { flex: 1 1 200px; - padding: 10px; app-table-cell { width: 100%; @@ -88,6 +87,19 @@ mat-header-cell { } } + &--table-centred { + app-table-cell { + align-items: center; + display: flex; + justify-content: center; + text-align: center; + } + } + + &--table-no-v-padding { + padding: 0 10px 0 0; + } + &--table-column-additional-padding { app-table-cell { padding-left: 15px; diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.ts index 10f0637f6a..ab1af731f3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.ts @@ -64,6 +64,7 @@ export class TableComponent implements OnInit, OnDestroy { public columnNames: string[]; @Input() minRowHeight: string; + @Input() prominentErrorBar: boolean = true; ngOnInit() { if (this.addSelect || this.expandComponent || this.addActions) { diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts index d5ab5af42e..b7cc5252d8 100644 --- a/src/frontend/packages/core/src/shared/shared.module.ts +++ b/src/frontend/packages/core/src/shared/shared.module.ts @@ -47,6 +47,7 @@ import { MetaCardItemComponent } from './components/list/list-cards/meta-card/me import { MetaCardKeyComponent } from './components/list/list-cards/meta-card/meta-card-key/meta-card-key.component'; import { MetaCardTitleComponent } from './components/list/list-cards/meta-card/meta-card-title/meta-card-title.component'; import { MetaCardValueComponent } from './components/list/list-cards/meta-card/meta-card-value/meta-card-value.component'; +import { TableCellEditComponent } from './components/list/list-table/table-cell-edit/table-cell-edit.component'; import { TableCellRequestMonitorIconComponent, } from './components/list/list-table/table-cell-request-monitor-icon/table-cell-request-monitor-icon.component'; @@ -310,7 +311,8 @@ import { UserPermissionDirective } from './user-permission.directive'; SidepanelPreviewComponent, TableCellEndpointNameComponent, CardProgressOverlayComponent, - MaxListMessageComponent + MaxListMessageComponent, + TableCellEditComponent ], entryComponents: [ DialogConfirmComponent, 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 7205f8ff23..7f29f7a188 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 @@ -67,6 +67,7 @@ export interface IStratosBaseEntityDefinition[]; readonly paginationConfig?: PaginationPageIteratorConfig; readonly tableConfig?: EntityTableConfig; + readonly registrationComponent?: any; /** * Hook that will fire before an entity is emitted by an entity service. This could be used, for example, entity validation */ diff --git a/src/frontend/packages/store/src/reducers/api-request-reducer/fail-request.ts b/src/frontend/packages/store/src/reducers/api-request-reducer/fail-request.ts index b62fee8a48..1a3826a515 100644 --- a/src/frontend/packages/store/src/reducers/api-request-reducer/fail-request.ts +++ b/src/frontend/packages/store/src/reducers/api-request-reducer/fail-request.ts @@ -26,8 +26,8 @@ export function failRequest(state, action: IFailedRequestAction) { busy: false, deleted: false, error: true, + message: action.message }; - requestFailedState.message = action.message; } else { requestFailedState.fetching = false; requestFailedState.error = true; diff --git a/src/frontend/packages/store/src/reducers/api-request-reducer/types.ts b/src/frontend/packages/store/src/reducers/api-request-reducer/types.ts index 8b514a0c4a..0f1b724a3e 100644 --- a/src/frontend/packages/store/src/reducers/api-request-reducer/types.ts +++ b/src/frontend/packages/store/src/reducers/api-request-reducer/types.ts @@ -14,6 +14,14 @@ export interface ActionState { message: string; } +// Status of an action +export interface ActionStatus { + busy: boolean; + error: boolean; + message?: string; + completed: boolean; +} + /** * Multi action lists can have different entity types per page * We use schemaKey to track this type diff --git a/src/frontend/packages/store/src/selectors/api.selectors.ts b/src/frontend/packages/store/src/selectors/api.selectors.ts index 4aecb698d7..b1eaa34938 100644 --- a/src/frontend/packages/store/src/selectors/api.selectors.ts +++ b/src/frontend/packages/store/src/selectors/api.selectors.ts @@ -1,8 +1,8 @@ import { compose } from '@ngrx/store'; +import { GeneralEntityAppState, IRequestEntityTypeState as IRequestEntityKeyState, IRequestTypeState } from '../app-state'; import { EntityCatalogHelpers } from '../entity-catalog/entity-catalog.helper'; import { EntityCatalogEntityConfig } from '../entity-catalog/entity-catalog.types'; -import { GeneralEntityAppState, IRequestEntityTypeState as IRequestEntityKeyState, IRequestTypeState } from '../app-state'; import { ActionState, RequestInfoState, UpdatingSection } from '../reducers/api-request-reducer/types'; import { APIResource } from '../types/api.types'; @@ -31,7 +31,7 @@ export const getEntityUpdateSections = ( export const getUpdateSectionById = (guid: string) => ( updating ): ActionState => { - return updating[guid]; + return updating ? updating[guid] : null; }; export function selectUpdateInfo( diff --git a/src/jetstream/default.config.properties b/src/jetstream/default.config.properties index e45f461d9d..2a430adb51 100644 --- a/src/jetstream/default.config.properties +++ b/src/jetstream/default.config.properties @@ -47,8 +47,8 @@ INVITE_USER_CLIENT_SECRET= # Use local admin user rather than UAA users # AUTH_ENDPOINT_TYPE=local -# LOCAL_USER=localuser -# LOCAL_USER_PASSWORD=localuserpass +# LOCAL_USER=admin +# LOCAL_USER_PASSWORD=admin # LOCAL_USER_SCOPE=stratos.admin # MariaDB database for local dev diff --git a/src/jetstream/plugins/kubernetes/main.go b/src/jetstream/plugins/kubernetes/main.go index 0ed9278022..4556391650 100644 --- a/src/jetstream/plugins/kubernetes/main.go +++ b/src/jetstream/plugins/kubernetes/main.go @@ -4,8 +4,10 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/http" "net/url" "strconv" + "strings" "errors" @@ -135,7 +137,7 @@ func (c *KubernetesSpecification) Init() error { } func (c *KubernetesSpecification) AddAdminGroupRoutes(echoGroup *echo.Group) { - // no-op + echoGroup.GET("/kube/cert", c.RequiresCert) } func (c *KubernetesSpecification) AddSessionGroupRoutes(echoGroup *echo.Group) { @@ -242,3 +244,31 @@ func parseErrorResponse(body []byte) error { func (c *KubernetesSpecification) UpdateMetadata(info *interfaces.Info, userGUID string, echoContext echo.Context) { } + +func (c *KubernetesSpecification) RequiresCert(ec echo.Context) error { + url := ec.QueryParam("url") + + log.Debug("Request Kube API Versions") + var httpClient = c.portalProxy.GetHttpClient(false) + _, err := httpClient.Get(url + "/api") + var response struct { + Status int + Required bool + Error bool + Message string + } + if err != nil { + if strings.Contains(err.Error(), "x509: certificate") { + response.Status = http.StatusOK + response.Required = true + } else { + response.Status = http.StatusInternalServerError + response.Error = true + response.Message = fmt.Sprintf("Failed to validate Kube certificate requirement: %+v", err) + } + } else { + response.Status = http.StatusOK + response.Required = false + } + return ec.JSON(response.Status, response) +} diff --git a/src/test-e2e/endpoints/register-dialog.po.ts b/src/test-e2e/endpoints/register-dialog.po.ts index 30e52790e7..44423a0d97 100644 --- a/src/test-e2e/endpoints/register-dialog.po.ts +++ b/src/test-e2e/endpoints/register-dialog.po.ts @@ -18,7 +18,7 @@ export class RegisterStepper extends Page { } isRegisterDialog(): promise.Promise { - return this.header.getTitleText().then(title => title === 'Register a new Endpoint'); + return this.header.getTitleText().then(title => title === 'Register Endpoint'); } getName = () => this.form.getFormField('name');