From 4e6a37b37f26ab5e1ef4c39d90e1fe384021caf1 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 11 May 2020 15:51:49 +0100 Subject: [PATCH] Merge upstream (#374) * Add initial UI for backup/restore * Convert connection details checkbox to drop down * Add docs for max list feature * Add db version check * Tidy up #1 * Tidying & bug fixes. Handle client_secret * Add minimum password length * First pass encrypt/decrypt * Tidy up * Fix unit tests * Re-add tests that now work * Changes following review * Move backup into it's own module * Ensure we filter out maxed results for local lists - When apps collection is fetched and exceeds maxed allowed certain monitor observables were still firing with the partial collection of apps - This led to app based stats (like count and memory) to be shown on the space cards & summary - This was caused by a correction to the pagination monitor to use 'local' monitor observables instead of non-local - The now correct observable to use did not have a filter for maxed results, so emited the bad data * CF Push: Ensure we refresh token * Fix uaa docs. Make UAA endpoint config simpler * Fix compile issues * Fixes following merge * Fix row highlight (see app github tab commit table) - wasn't showing after list in list changes - fixes #4243 * Improve metrics view * Fix code climate issues * Fix 2 more code climate issues * Fix merge issue * Fix dark mode box shadow - dark mode styling overode disabling of box shadow * Helm Chart: Fix Helm 3 compatibility with Release.Time * Fix multiple metrics requests due to recreated list cards * Fix most additional /stratos requests on load of endpoints page with metrics * Add nodeSelector implementation and tests to kubernetes helm deployment. (#4252) * Add nodeSelector template and tests Add nodeSelector template to the helm deployment: - deployment.yaml - database.yaml - config-init.yaml Add tests to validate nodeSelector template to - deployment_test.yaml Create tests to validate nodeSelector template to - database_test.yaml - config_init_test.yaml * Add c&p fix and another nodeSelector test Add fix for c&p bug to - database.yaml - deployment.yaml Add test to validate nodeSelector template to - config_init_test.yaml Add test config to - database-test.yaml - deployment-test.yaml * Remove test Remove test to validate nodeSelector template to - config_init_test.yaml * Autoscaler improvements * Fix unit tests * Fix disabling of polling (#4260) - fixes #4244 * Add Helm 3 lint check to the Travis Helm Chart job (#4263) * Helm Chart: Fix Helm 3 compatibility with Release.Time * Add helm 3 lint * Fix EndpointCardComponent test * Increase users table page size before searching for `e2e` user * Helm node selector docs and values (#4264) * Add nodeSelector template and tests Add nodeSelector template to the helm deployment: - deployment.yaml - database.yaml - config-init.yaml Add tests to validate nodeSelector template to - deployment_test.yaml Create tests to validate nodeSelector template to - database_test.yaml - config_init_test.yaml * Add c&p fix and another nodeSelector test Add fix for c&p bug to - database.yaml - deployment.yaml Add test to validate nodeSelector template to - config_init_test.yaml Add test config to - database-test.yaml - deployment-test.yaml * Remove test Remove test to validate nodeSelector template to - config_init_test.yaml * More gates on node selector. Added to values.yaml and README Co-authored-by: Johannes Jungkunst * Fix root list colour in metrics summary page dark mode * Fix first row alignment in users table * Fix invite user e2e test (see #4272) * Disable random route override setting when deploying apps in e2e tests - pushes route over max 63 chars * Disable random route override setting when deploying apps in e2e tests - pushes route over max 63 chars * Override name but do not set a random route * EE Test improvements (#4274) * Speed up form fill * Manage users fix? * Wait slightly longer * Improve error output in tests * Fix invite users error msg check * Fix manage users e2e * Need checked in form to check box correctly * Fix row highligh & Improve metrics summary view (#4270) * Fix row highlight (see app github tab commit table) - wasn't showing after list in list changes - fixes #4243 * Improve metrics view * Fix code climate issues * Fix 2 more code climate issues * Fix merge issue * Fix dark mode box shadow - dark mode styling overode disabling of box shadow * Fix multiple metrics requests due to recreated list cards * Fix most additional /stratos requests on load of endpoints page with metrics * Fix unit tests * Fix EndpointCardComponent test * Fix root list colour in metrics summary page dark mode * Fix first row alignment in users table * Fix invite user e2e test (see #4272) * Disable random route override setting when deploying apps in e2e tests - pushes route over max 63 chars Co-authored-by: Neil MacDougall * Improve view that shows details for a metrics endpoint (#4258) * Improve metrics view * Fix code climate issues * Fix 2 more code climate issues * Fix merge issue * Fix multiple metrics requests due to recreated list cards * Fix most additional /stratos requests on load of endpoints page with metrics * Fix unit tests * Fix EndpointCardComponent test Co-authored-by: Richard Cox * Fix row highlight (see app github tab commit table) (#4257) * Fix row highlight (see app github tab commit table) - wasn't showing after list in list changes - fixes #4243 * Fix dark mode box shadow - dark mode styling overode disabling of box shadow Co-authored-by: Neil MacDougall * E2E Cleanup script: clean other users (#4269) * E2E Cleanup script: clean other users and reduce time to 1 hour * Remove superfluous blank lines * Revert back to 2 hours * Version bump and change log for 3.2.0 * E2E Test improvements (#4275) * Speed up form fill * Fix page set setting * Remove whitespace * Add ability to provided a wildcard in path when matching urls (#4277) * Update releas notes Co-authored-by: Neil MacDougall Co-authored-by: Neil MacDougall Co-authored-by: macevil <30493519+macevil@users.noreply.github.com> Co-authored-by: Neil MacDougall Co-authored-by: Johannes Jungkunst --- CHANGELOG.md | 23 ++ build/tools/changelog.sh | 4 +- deploy/ci/automation/e2e-clean-remnants.sh | 1 + deploy/ci/travis/helm-chart-unit-tests.sh | 12 + deploy/kubernetes/console/README.md | 21 +- .../console/templates/config-init.yaml | 10 + .../console/templates/database.yaml | 6 + .../console/templates/deployment.yaml | 18 +- .../console/tests/config_init_test.yaml | 12 + .../console/tests/database-test.yaml | 12 + .../console/tests/deployment_test.yaml | 8 + deploy/kubernetes/console/values.yaml | 23 +- docs/cf-entity-scaling.md | 43 +++ docs/sso.md | 6 +- package-lock.json | 2 +- package.json | 2 +- .../autoscaler-metric-page.component.spec.ts | 10 +- ...caler-scale-history-page.component.spec.ts | 9 +- ...plication-instance-chart.component.spec.ts | 3 +- .../metrics-tab/metrics-tab.component.spec.ts | 3 +- .../cloud-foundry-cell-apps.component.spec.ts | 7 +- .../cloud-foundry-cell-base.component.spec.ts | 11 +- ...loud-foundry-cell-charts.component.spec.ts | 7 +- ...oud-foundry-cell-summary.component.spec.ts | 9 +- .../cf-endpoint-details.component.scss | 2 +- .../shared/components/components.module.ts | 6 +- .../packages/core/sass/_all-theme.scss | 6 +- .../packages/core/src/base-entity-types.ts | 10 +- .../backup-checkbox-cell.component.html | 2 + .../backup-checkbox-cell.component.scss | 0 .../backup-checkbox-cell.component.spec.ts | 45 +++ .../backup-checkbox-cell.component.ts | 25 ++ .../backup-connection-cell.component.html | 10 + .../backup-connection-cell.component.scss | 0 .../backup-connection-cell.component.spec.ts | 43 +++ .../backup-connection-cell.component.ts | 36 ++ .../backup-endpoints.service.ts | 182 ++++++++++ .../backup-endpoints.component.html | 43 +++ .../backup-endpoints.component.scss | 15 + .../backup-endpoints.component.spec.ts | 35 ++ .../backup-endpoints.component.ts | 187 ++++++++++ .../backup-restore-endpoints.component.html | 12 + .../backup-restore-endpoints.component.scss | 3 + ...backup-restore-endpoints.component.spec.ts | 35 ++ .../backup-restore-endpoints.component.ts | 46 +++ .../backup-restore/backup-restore.types.ts | 25 ++ .../restore-endpoints.service.ts | 141 ++++++++ .../restore-endpoints.component.html | 59 +++ .../restore-endpoints.component.scss | 32 ++ .../restore-endpoints.component.spec.ts | 35 ++ .../restore-endpoints.component.theme.scss | 11 + .../restore-endpoints.component.ts | 105 ++++++ .../connect-endpoint-dialog.component.spec.ts | 9 +- .../connect-endpoint.component.spec.ts | 5 +- ...reate-endpoint-cf-step-1.component.spec.ts | 5 +- .../create-endpoint.component.spec.ts | 12 +- .../endpoints-page.component.html | 7 +- .../features/endpoints/endpoints.module.ts | 20 +- .../features/endpoints/endpoints.routing.ts | 19 +- .../metrics-endpoint-details.component.html | 9 + .../metrics-endpoint-details.component.scss | 16 + ...metrics-endpoint-details.component.spec.ts | 37 ++ .../metrics-endpoint-details.component.ts | 88 +++++ .../src/features/metrics/metrics.helpers.ts | 74 ++++ .../src/features/metrics/metrics.module.ts | 15 +- .../metrics/metrics/metrics.component.html | 90 ++--- .../metrics/metrics/metrics.component.scss | 80 ++++- .../metrics/metrics/metrics.component.spec.ts | 5 +- .../metrics/metrics.component.theme.scss | 21 ++ .../metrics/metrics/metrics.component.ts | 61 ++-- .../profile-info/profile-info.component.html | 2 +- .../packages/core/src/jetstream.helpers.ts | 17 + .../list/list-cards/card/card.component.ts | 14 +- .../table-row/table-row.component.html | 4 +- .../table-row/table-row.component.scss | 7 +- .../table-row/table-row.component.theme.scss | 8 + .../endpoint-card.component.spec.ts | 16 +- .../endpoint/endpoints-list-config.service.ts | 7 +- .../metrics-chart.component.spec.ts | 4 +- .../store/src/actions/endpoint.actions.ts | 2 +- .../store/src/actions/metrics-api.actions.ts | 18 +- .../store/src/effects/metrics.effects.ts | 4 +- .../store/src/monitors/pagination-monitor.ts | 1 + .../store/src/reducers/auth.reducer.ts | 2 +- .../src/reducers/system-endpoints.reducer.ts | 13 + .../store/src/types/endpoint.types.ts | 5 +- src/jetstream/authcnsi.go | 2 +- src/jetstream/cnsi.go | 4 +- src/jetstream/default.config.properties | 1 + src/jetstream/load_plugins.go | 2 + .../plugins/backup/backup_restore.go | 336 ++++++++++++++++++ src/jetstream/plugins/backup/main.go | 111 ++++++ src/jetstream/plugins/cfapppush/deploy.go | 14 +- src/jetstream/plugins/monocular/sync.go | 6 +- src/jetstream/repository/cnsis/cnsis.go | 1 + src/jetstream/repository/cnsis/pgsql_cnsis.go | 22 ++ .../repository/interfaces/portal_proxy.go | 2 +- .../repository/interfaces/structs.go | 22 +- .../repository/tokens/pgsql_tokens.go | 95 +++++ src/jetstream/repository/tokens/tokens.go | 4 +- src/jetstream/stringutils/utils.go | 7 +- src/jetstream/version_info.go | 2 +- .../application-autoscaler-e2e.spec.ts | 39 +- .../application/application-deploy-helper.ts | 3 +- .../cf-level/manage-quota-e2e.spec.ts | 17 +- .../cloud-foundry/invite-users-e2e.helper.ts | 8 +- .../manage-users-stepper-e2e.spec.ts | 2 + .../cloud-foundry/users-removal-e2e.helper.ts | 25 +- src/test-e2e/helpers/e2e-helpers.ts | 8 + src/test-e2e/po/component.po.ts | 14 +- src/test-e2e/po/form.po.ts | 42 ++- src/test-e2e/po/list.po.ts | 18 +- 112 files changed, 2645 insertions(+), 272 deletions(-) create mode 100644 deploy/kubernetes/console/tests/config_init_test.yaml create mode 100644 deploy/kubernetes/console/tests/database-test.yaml create mode 100644 docs/cf-entity-scaling.md create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore.types.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts create mode 100644 src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.html create mode 100644 src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.scss create mode 100644 src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.ts create mode 100644 src/frontend/packages/core/src/features/metrics/metrics.helpers.ts create mode 100644 src/frontend/packages/core/src/features/metrics/metrics/metrics.component.theme.scss create mode 100644 src/jetstream/plugins/backup/backup_restore.go create mode 100644 src/jetstream/plugins/backup/main.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 351a1f2096..ace65b5a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Change Log +## 3.2.0 + +[Full Changelog](https://github.com/cloudfoundry/stratos/compare/3.1.0...3.2.0) + +This release contains a number of fixes and improvements: + +**Improvements:** + +- SSO_WHITELIST should be able to ignore path [\#4273](https://github.com/cloudfoundry/stratos/issues/4273) +- Improve view that shows details for a metrics endpoint [\#4271](https://github.com/cloudfoundry/stratos/issues/4271) +- Helm Chart: Add support for node selectors [\#4265](https://github.com/cloudfoundry/stratos/issues/4265) +- Add documentation for list's 'max' feature, include info on 'fetch all' button [\#4259](https://github.com/cloudfoundry/stratos/issues/4259) +- Backup Endpoints & Tokens [\#4228](https://github.com/cloudfoundry/stratos/issues/4228) + +**Fixes:** + +- Deployment time does not show correctly in diagnostics when deployed with Helm 3 [\#4261](https://github.com/cloudfoundry/stratos/issues/4261) +- Pushing app from Stratos can sometimes fail due to expired token [\#4253](https://github.com/cloudfoundry/stratos/issues/4253) +- Helm: Chart fails to render if `uaa` section is missing, docs misleading [\#4248](https://github.com/cloudfoundry/stratos/issues/4248) +- Profile: Disabling polling fails to disable polling [\#4244](https://github.com/cloudfoundry/stratos/issues/4244) +- App Summary: Github tab: Row highlight of deployed commit is obscured [\#4243](https://github.com/cloudfoundry/stratos/issues/4243) +- Data Inaccuracies in PCF [\#4237](https://github.com/cloudfoundry/stratos/issues/4237) + ## 3.1.0 [Full Changelog](https://github.com/SUSE/stratos/compare/3.0.0...3.1.0) diff --git a/build/tools/changelog.sh b/build/tools/changelog.sh index 61affff09f..c1fd74daae 100755 --- a/build/tools/changelog.sh +++ b/build/tools/changelog.sh @@ -68,9 +68,9 @@ function log() { } COMPARE_REPO=${REPO} -QUERY="repo:${REPO}+milestone:3.1.0+state:closed" +QUERY="repo:${REPO}+milestone:${MILESTONE}+state:closed" if [ -n "${FORK}" ]; then - FORK_QUERY="repo:${FORK}+milestone:3.1.0+state:closed" + FORK_QUERY="repo:${FORK}+milestone:${MILESTONE}+state:closed" COMPARE_REPO=${FORK} fi diff --git a/deploy/ci/automation/e2e-clean-remnants.sh b/deploy/ci/automation/e2e-clean-remnants.sh index 7285afb277..9059f5ff5c 100755 --- a/deploy/ci/automation/e2e-clean-remnants.sh +++ b/deploy/ci/automation/e2e-clean-remnants.sh @@ -120,6 +120,7 @@ USERS=$(cf curl "/v2/users?results-per-page=100" | jq -r .resources[].entity.use clean "$USERS" "-" "delete-user" "^(acceptance\.e2e\.travisci)(-remove-users)\.(20[0-9]*)[Tt]([0-9]*)[zZ].*" clean "$USERS" "-" "delete-user" "^(acceptance\.e2e\.travis)(-remove-users)\.(20[0-9]*)[Tt]([0-9]*)[zZ].*" clean "$USERS" "-" "delete-user" "^(acceptancee2etravis)(invite[0-9])(20[0-9]*)[Tt]([0-9]*)[zZ].*" +clean "$USERS" "-" "delete-user" "^(acceptance\.e2e\.travisci)(-manage-by-username)\.(20[0-9]*)[Tt]([0-9]*)[zZ].*" # Routes echo "Cleaning routes" diff --git a/deploy/ci/travis/helm-chart-unit-tests.sh b/deploy/ci/travis/helm-chart-unit-tests.sh index aa1d106565..a98d0d694e 100755 --- a/deploy/ci/travis/helm-chart-unit-tests.sh +++ b/deploy/ci/travis/helm-chart-unit-tests.sh @@ -24,3 +24,15 @@ helm unittest console # Run lint helm lint console + +# Run helm3 lint as well +echo "Installing Helm 3" +export BINARY_NAME=helm3 +curl -fsSL -o get_helm3.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 +chmod 700 get_helm3.sh +./get_helm3.sh + +# RUn Helm 3 lint +helm3 lint console + +echo "All done" diff --git a/deploy/kubernetes/console/README.md b/deploy/kubernetes/console/README.md index 5a03fedeba..dee1153dd4 100644 --- a/deploy/kubernetes/console/README.md +++ b/deploy/kubernetes/console/README.md @@ -22,9 +22,11 @@ Check the repository was successfully added by searching for the `console`, for ``` helm search console NAME CHART VERSION APP VERSION DESCRIPTION -stratos/console 3.0.0 3.0.0 A Helm chart for deploying Stratos UI Console +stratos/console 3.1.0 3.1.0 A Helm chart for deploying Stratos UI Console ``` +> Note: Version numbers will depend on the version of Stratos available from the Helm repository + > Note: Commands shown in this document are for Helm version 3. For Helm version 2, when installing, instead of supplying the name via the `--name` flag, it is supplied as the first argument, before the chart name. To install Stratos: @@ -85,13 +87,11 @@ The following table lists the configurable parameters of the Stratos Helm chart |console.mariadb.host|Hostname of the database when using an external db|| |console.mariadb.port|Port of the database when using an external db|3306| |console.mariadb.tls|TLS mode when connecting to database (true, false, skip-verify, preferred)|false| -|console.uaa.protocol|Protocol to use when authenticating with the UAA|https://| -|console.uaa.host|Host of the UAA to authenticate with || -|console.uaa.port|Port of the UAA to authenticate with || -|console.uaa.consoleClient|Client to use when authenticating with the UAA|cf| -|console.uaa.consoleClientSecret|Client secret to use when authenticating with the UAA|| -|console.uaa.consoleAdminIdentifier|Scope that identifies an admin user of Stratos (e.g. cloud_controller.admin|| -|console.uaa.skipSSLValidation|Skip SSL validation when when authenticating with the UAA|false| +|uaa.endpoint|URL of the UAA endpoint to authenticate with || +|uaa.consoleClient|Client to use when authenticating with the UAA|cf| +|uaa.consoleClientSecret|Client secret to use when authenticating with the UAA|| +|uaa.consoleAdminIdentifier|Scope that identifies an admin user of Stratos (e.g. cloud_controller.admin|| +|uaa.skipSSLValidation|Skip SSL validation when when authenticating with the UAA|false| |env.SMTP_AUTH|Authenticate against the SMTP server using AUTH command when Sending User Invite emails|false| |env.SMTP_FROM_ADDRESS|From email address to use when Sending User Invite emails|| |env.SMTP_USER|User name to use for authentication when Sending User Invite emails|| @@ -115,6 +115,9 @@ The following table lists the configurable parameters of the Stratos Helm chart |console.service.extraLabels|Additional labels to be added to all service resources|| |console.service.ingress.annotations|Annotations to be added to the ingress resource|| |console.service.ingress.extraLabels|Additional labels to be added to the ingress resource|| +|console.nodeSelector|Node selectors to use for the console Pod|| +|mariadb.nodeSelector|Node selectors to use for the database Pod|| +|configInit.nodeSelector|Node selectors to use for the configuration Pod|| ## Accessing the Console @@ -280,6 +283,7 @@ UAA configuration can be specified by providing the following configuration. Create a yaml file with the content below and and update according to your environment and save to a file called `uaa-config.yaml`. ``` uaa: + url: https://uaa.cf-dev.io:2793 protocol: https:// port: 2793 host: uaa.cf-dev.io @@ -291,7 +295,6 @@ uaa: To install Stratos with the above specified configuration: - ``` kubectl create namespace console helm install my-console stratos/console --namespace=console -f uaa-config.yaml diff --git a/deploy/kubernetes/console/templates/config-init.yaml b/deploy/kubernetes/console/templates/config-init.yaml index 6313d3ab8c..e6d2f3582a 100644 --- a/deploy/kubernetes/console/templates/config-init.yaml +++ b/deploy/kubernetes/console/templates/config-init.yaml @@ -94,6 +94,12 @@ spec: {{ toYaml .Values.console.podExtraLabels | nindent 8 }} {{- end }} spec: +{{- if .Values.configInit }} +{{- if .Values.configInit.nodeSelector }} + nodeSelector: +{{ toYaml .Values.configInit.nodeSelector | trim | indent 8 }} +{{- end }} +{{- end }} containers: - env: - name: "STRATOS_VOLUME_MIGRATION" @@ -170,6 +176,10 @@ spec: app.kubernetes.io/version: "{{ .Chart.AppVersion }}" helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" spec: +{{- if .Values.configInit }} + nodeSelector: +{{ toYaml .Values.configInit.nodeSelector | indent 8 }} +{{- end }} containers: - env: - name: "NAMESPACE" diff --git a/deploy/kubernetes/console/templates/database.yaml b/deploy/kubernetes/console/templates/database.yaml index 45af491bca..2f23173f49 100644 --- a/deploy/kubernetes/console/templates/database.yaml +++ b/deploy/kubernetes/console/templates/database.yaml @@ -49,6 +49,12 @@ spec: {{ toYaml .Values.console.podExtraLabels | nindent 8 }} {{- end}} spec: +{{- if .Values.mariadb }} +{{- if .Values.mariadb.nodeSelector }} + nodeSelector: +{{ toYaml .Values.mariadb.nodeSelector | trim | indent 8 }} +{{- end }} +{{- end }} containers: - name: mariadb image: {{.Values.kube.registry.hostname}}/{{.Values.kube.organization}}/{{.Values.images.mariadb}}:{{.Values.consoleVersion}} diff --git a/deploy/kubernetes/console/templates/deployment.yaml b/deploy/kubernetes/console/templates/deployment.yaml index c4123af521..b0aec9dc1b 100644 --- a/deploy/kubernetes/console/templates/deployment.yaml +++ b/deploy/kubernetes/console/templates/deployment.yaml @@ -47,6 +47,12 @@ spec: {{ toYaml .Values.console.podExtraLabels | indent 8 }} {{- end }} spec: +{{- if .Values.console }} +{{- if .Values.console.nodeSelector }} + nodeSelector: +{{ toYaml .Values.console.nodeSelector | trim | indent 8 }} +{{- end }} +{{- end }} containers: - image: {{.Values.kube.registry.hostname}}/{{.Values.kube.organization}}/{{.Values.images.console}}:{{.Values.consoleVersion}} imagePullPolicy: {{.Values.imagePullPolicy}} @@ -184,7 +190,15 @@ spec: value: stratos.admin {{- else }} # UAA - {{- if or .Values.env.UAA_HOST .Values.env.DOMAIN }} + # Highest priority setting to use is uaa.endpoint + {{- if .Values.uaa.endpoint }} + - name: UAA_ENDPOINT + value: {{ .Values.uaa.endpoint | quote }} + - name: CONSOLE_ADMIN_SCOPE + value: {{ default "cloud_controller.admin" .Values.uaa.consoleAdminIdentifier }} + - name: SKIP_SSL_VALIDATION + value: {{default "true" .Values.uaa.skipSSLValidation | quote}} + {{- else if or .Values.env.UAA_HOST .Values.env.DOMAIN }} - name: UAA_ENDPOINT value: {{ template "scfUaaEndpoint" . }} {{- if and .Values.env.DOMAIN (not .Values.console.autoRegisterCF) }} @@ -219,7 +233,7 @@ spec: - name: HELM_CHART_VERSION value: "{{ .Chart.Version }}" - name: HELM_LAST_MODIFIED - value: "{{ .Release.Time }}" + value: "seconds:{{ now | unixEpoch }} nanos:0 " - name: SSO_LOGIN value: {{default "false" .Values.console.ssoLogin | quote}} - name: SSO_OPTIONS diff --git a/deploy/kubernetes/console/tests/config_init_test.yaml b/deploy/kubernetes/console/tests/config_init_test.yaml new file mode 100644 index 0000000000..85778e7267 --- /dev/null +++ b/deploy/kubernetes/console/tests/config_init_test.yaml @@ -0,0 +1,12 @@ +suite: test stratos configInit +templates: + - config-init.yaml +tests: + - it: should set kubernetes.io/arch if configInit.nodeSelector.kubernetes.io/arch is set + set: + configInit.nodeSelector.kubernetes.io/arch: amd64 + asserts: + - equal: + path: spec.template.spec.nodeSelector.kubernetes.io/arch + value: amd64 + documentIndex: 3 \ No newline at end of file diff --git a/deploy/kubernetes/console/tests/database-test.yaml b/deploy/kubernetes/console/tests/database-test.yaml new file mode 100644 index 0000000000..be52ea6bb9 --- /dev/null +++ b/deploy/kubernetes/console/tests/database-test.yaml @@ -0,0 +1,12 @@ +suite: test stratos database +templates: + - database.yaml +tests: + - it: should set kubernetes.io/arch if mariadb.nodeSelector.kubernetes.io/arch is set + set: + mariadb.nodeSelector.kubernetes.io/arch: amd64 + asserts: + - equal: + path: spec.template.spec.nodeSelector.kubernetes.io/arch + value: amd64 + documentIndex: 0 \ No newline at end of file diff --git a/deploy/kubernetes/console/tests/deployment_test.yaml b/deploy/kubernetes/console/tests/deployment_test.yaml index b451ba5a3a..9aa6137c08 100644 --- a/deploy/kubernetes/console/tests/deployment_test.yaml +++ b/deploy/kubernetes/console/tests/deployment_test.yaml @@ -67,3 +67,11 @@ tests: content: name: AUTO_REG_CF_URL value: https://autoreg.test.com + - it: should set kubernetes.io/arch if console.nodeSelector.kubernetes.io/arch is set + set: + console.nodeSelector.kubernetes.io/arch: amd64 + asserts: + - equal: + path: spec.template.spec.nodeSelector.kubernetes.io/arch + value: amd64 + documentIndex: 0 \ No newline at end of file diff --git a/deploy/kubernetes/console/values.yaml b/deploy/kubernetes/console/values.yaml index 975a8073de..8c7a7cf614 100644 --- a/deploy/kubernetes/console/values.yaml +++ b/deploy/kubernetes/console/values.yaml @@ -79,6 +79,7 @@ console: tlsSecretName: # URL of a Cloud Foundry to use for authentication and to auto-register on login + # Deprecated autoRegisterCF: ~ # Custom annotations to apply to Stateful sets @@ -104,6 +105,9 @@ console: # Extra labels to apply to Pods podExtraLabels: {} + + # Node Selector for console Pod + nodeSelector: {} images: console: stratos-console @@ -145,13 +149,24 @@ mariadb: accessMode: ReadWriteOnce size: 1Gi storageClass: + # Node selector for the database pod + nodeSelector: {} + +configInit: + # Node selector for the config init pod + nodeSelector: {} + +# UAA configuration uaa: - protocol: https:// - port: - host: + # UAA endpoint (e.g. https://uaa.domain:2793) + endpoint: ~ + # Client to use when authenticating (default is 'cf') consoleClient: + # Client Secret to use when authenticating (default is '') consoleClientSecret: - consoleAdminIdentifier: + # Scope that determines if a user is a Stratos admin + consoleAdminIdentifier: + # Skip SSL validation when communicating with the UAA skipSSLValidation: false # SCF values compatability diff --git a/docs/cf-entity-scaling.md b/docs/cf-entity-scaling.md new file mode 100644 index 0000000000..6055137d25 --- /dev/null +++ b/docs/cf-entity-scaling.md @@ -0,0 +1,43 @@ +# Cloud Foundry Scaling - How Stratos Handles Large Lists + +Stratos presents collections of entities via the Stratos list component. The list component presents the collection in a paginated, sortable and searchable way in either a set of cards or a table. +In order to achieve this, due to the limitations of the APIs used, the list may fetch all entities (as opposed to fetching entities for the visible page only) and paginate, sort and search locally. + +## Protecting Stratos from Large Collections +In some cases the number of entities in a collection can be incredibly high. Stratos can decide to not fetch them all in order to protect the CF from +a substantial number of requests. To do this the first page is fetched and the total number of entities is checked against a limit. If under +the limit the remaining pages are asynchronously fetched. If over then the remaining pages are ignored and the user is informed that the list could not fetch all entities. + +Depending on the list, the user can then try to filter the collection such that the number of entities is below the limit. Depending on configuration +the user also has the option to fetch all entities regardless of the limit. + +## Applicable Lists +Currently, in 3.1.0, this large collection protection is only applicable to the following lists + +- Application Wall +- Marketplace (Services) +- Services (Service & User Provided Service Instances) +- CF, Organisation and Space Users +- CF Routes + +In the future we hope to expand this to all lists. + +## Determining the List Limit +The limit at which we won't fetch all entities is determined, in least important to most important order, by + +1) The global CF default - 600 +2) The Jetstream override for all lists - by default not set + +In the future we hope to allow each end user of Stratos to determine their own limit (if they have the correct permissions). + +## Fetch All feature +If the list hits the limit the user will be presented with a button to `Fetch All` entities. Clicking this button ensures the list reverts +back as if there were no limit and thus fetching all entities. This feature is disabled by default and can be enabled by a Jetstream override. + +## Configuration +The Jetstream overrides can be set via environment variable, or if in helm, as values. + +Environment Variable|Helm Value|Description|Default| +|---|---|---|---| +|`UI_LIST_MAX_SIZE`|`console.ui.listMaxSize`|Override the default maximum number of entities that a configured list can fetch. When a list meets this amount additional pages are not fetched|| +|`UI_LIST_ALLOW_LOAD_MAXED`|`console.ui.listAllowLoadMaxed`|If the maximum list size is met give the user the option to fetch all results|false| \ No newline at end of file diff --git a/docs/sso.md b/docs/sso.md index e1ae6d9b60..8f9f747c67 100644 --- a/docs/sso.md +++ b/docs/sso.md @@ -55,11 +55,13 @@ uaac client update cf --authorized_grant_types authorization_code When SSO has been configured Stratos's log in request will contain a URL that tells SSO where to return to. When using a browser this is automatically populated. To avoid situations where this can be hijacked or called separately an SSO `state` whitelist can be provided via the environment variable `SSO_WHITELIST`. This is a comma separated list. For example... ``` -SSO_WHITELIST=https://your.domain +SSO_WHITELIST=https://your.domain/* ``` ``` -SSO_WHITELIST=https://your.domain,https://your.other.domain +SSO_WHITELIST=https://your.domain/*,https://your.other.domain/* ``` When set, any requests to log in with a different `state` will be denied. + +In order for the SSO `state` to match an entry from the whitelist the schema, hostname, port and path must match exactly. A wildcard `*` can be provided for the path to match anything. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e43b5be2d5..6a1d60bb3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "stratos", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6bd5660b6f..558a6f881c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stratos", - "version": "3.1.0", + "version": "3.2.0", "description": "Stratos Console", "main": "index.js", "scripts": { diff --git a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts index 0c897ce7f2..e59ab50816 100644 --- a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts +++ b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts @@ -2,18 +2,17 @@ import { DatePipe } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { createEmptyStoreModule } from '@stratos/store/testing'; import { ApplicationService } from '../../../../cloud-foundry/src/features/applications/application.service'; import { CoreModule } from '../../../../core/src/core/core.module'; import { SharedModule } from '../../../../core/src/shared/shared.module'; import { TabNavService } from '../../../../core/tab-nav.service'; import { ApplicationServiceMock } from '../../../../core/test-framework/application-service-helper'; -import { createEmptyStoreModule } from '@stratos/store/testing'; import { CfAutoscalerTestingModule } from '../../cf-autoscaler-testing.module'; import { AutoscalerMetricPageComponent } from './autoscaler-metric-page.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('AutoscalerMetricPageComponent', () => { +describe('AutoscalerMetricPageComponent', () => { let component: AutoscalerMetricPageComponent; let fixture: ComponentFixture; @@ -47,10 +46,5 @@ xdescribe('AutoscalerMetricPageComponent', () => { expect(component).toBeTruthy(); }); - // TODO: Fix after metrics has been sorted - STRAT-152 (cause of `Cannot read property 'getEntityMonitor' of undefined` test failure) - it('Blocked', () => { - fail('Blocked: Requires metrics to be working (specifically metrics entities)'); - }); - afterAll(() => { }); }); diff --git a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts index 55eead24ce..043a399200 100644 --- a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts +++ b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts @@ -2,18 +2,17 @@ import { DatePipe } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { createEmptyStoreModule } from '@stratos/store/testing'; import { ApplicationService } from '../../../../cloud-foundry/src/features/applications/application.service'; import { CoreModule } from '../../../../core/src/core/core.module'; import { SharedModule } from '../../../../core/src/shared/shared.module'; import { TabNavService } from '../../../../core/tab-nav.service'; import { ApplicationServiceMock } from '../../../../core/test-framework/application-service-helper'; -import { createEmptyStoreModule } from '@stratos/store/testing'; import { CfAutoscalerTestingModule } from '../../cf-autoscaler-testing.module'; import { AutoscalerScaleHistoryPageComponent } from './autoscaler-scale-history-page.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('AutoscalerScaleHistoryPageComponent', () => { +describe('AutoscalerScaleHistoryPageComponent', () => { let component: AutoscalerScaleHistoryPageComponent; let fixture: ComponentFixture; @@ -47,9 +46,5 @@ xdescribe('AutoscalerScaleHistoryPageComponent', () => { expect(component).toBeTruthy(); }); - // TODO: Fix after metrics has been sorted - STRAT-152 (cause of `Cannot read property 'getEntityMonitor' of undefined` test failure) - it('Blocked', () => { - fail('Blocked: Requires metrics to be working (specifically metrics entities)'); - }); }); diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-instance-chart/application-instance-chart.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-instance-chart/application-instance-chart.component.spec.ts index 4680ae8b5c..ebdde598a6 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-instance-chart/application-instance-chart.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-instance-chart/application-instance-chart.component.spec.ts @@ -7,8 +7,7 @@ import { SharedModule } from '../../../../../../core/src/shared/shared.module'; import { generateCfStoreModules } from '../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { ApplicationInstanceChartComponent } from './application-instance-chart.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('ApplicationInstanceChartComponent', () => { +describe('ApplicationInstanceChartComponent', () => { let component: ApplicationInstanceChartComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/metrics-tab/metrics-tab.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/metrics-tab/metrics-tab.component.spec.ts index bb8752a65f..fdc73fff41 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/metrics-tab/metrics-tab.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/metrics-tab/metrics-tab.component.spec.ts @@ -17,8 +17,7 @@ import { applicationEntityType } from '../../../../../../cf-entity-types'; import { ApplicationEnvVarsHelper } from '../build-tab/application-env-vars.service'; import { MetricsTabComponent } from './metrics-tab.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('MetricsTabComponent', () => { +describe('MetricsTabComponent', () => { let component: MetricsTabComponent; let fixture: ComponentFixture; const appId = '1'; diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-apps/cloud-foundry-cell-apps.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-apps/cloud-foundry-cell-apps.component.spec.ts index ba1472e058..af4480e9dc 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-apps/cloud-foundry-cell-apps.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-apps/cloud-foundry-cell-apps.component.spec.ts @@ -1,13 +1,10 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { - generateCfBaseTestModules, -} from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { generateCfBaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { ActiveRouteCfCell } from '../../../../cf-page.types'; import { CloudFoundryCellAppsComponent } from './cloud-foundry-cell-apps.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CloudFoundryCellAppsComponent', () => { +describe('CloudFoundryCellAppsComponent', () => { let component: CloudFoundryCellAppsComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-base/cloud-foundry-cell-base.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-base/cloud-foundry-cell-base.component.spec.ts index 93d7b9a131..205c8a732e 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-base/cloud-foundry-cell-base.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-base/cloud-foundry-cell-base.component.spec.ts @@ -1,16 +1,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TabNavService } from '../../../../../../../../core/tab-nav.service'; -import { - generateCfBaseTestModules, -} from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { generateCfBaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { CfUserService } from '../../../../../../shared/data-services/cf-user.service'; import { ActiveRouteCfOrgSpace } from '../../../../cf-page.types'; import { CloudFoundryEndpointService } from '../../../../services/cloud-foundry-endpoint.service'; import { CloudFoundryCellService } from '../cloud-foundry-cell.service'; import { CloudFoundryCellBaseComponent } from './cloud-foundry-cell-base.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CloudFoundryCellBaseComponent', () => { +describe('CloudFoundryCellBaseComponent', () => { let component: CloudFoundryCellBaseComponent; let fixture: ComponentFixture; @@ -22,7 +20,8 @@ xdescribe('CloudFoundryCellBaseComponent', () => { CloudFoundryEndpointService, CloudFoundryCellService, ActiveRouteCfOrgSpace, - TabNavService + TabNavService, + CfUserService ] }) .compileComponents(); diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-charts/cloud-foundry-cell-charts.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-charts/cloud-foundry-cell-charts.component.spec.ts index bf10e45c66..047523224d 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-charts/cloud-foundry-cell-charts.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-charts/cloud-foundry-cell-charts.component.spec.ts @@ -1,14 +1,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { - generateCfBaseTestModules, -} from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { generateCfBaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { ActiveRouteCfCell } from '../../../../cf-page.types'; import { CloudFoundryCellService } from '../cloud-foundry-cell.service'; import { CloudFoundryCellChartsComponent } from './cloud-foundry-cell-charts.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CloudFoundryCellChartsComponent', () => { +describe('CloudFoundryCellChartsComponent', () => { let component: CloudFoundryCellChartsComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-summary/cloud-foundry-cell-summary.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-summary/cloud-foundry-cell-summary.component.spec.ts index 8bf289dfb4..e8c160cbcf 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-summary/cloud-foundry-cell-summary.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-summary/cloud-foundry-cell-summary.component.spec.ts @@ -11,14 +11,12 @@ import { MetricsChartHelpers, } from '../../../../../../../../core/src/shared/components/metrics-chart/metrics.component.helpers'; import { MetricQueryType } from '../../../../../../../../core/src/shared/services/metrics-range-selector.types'; -import { - generateCfBaseTestModules, -} from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { MetricQueryConfig } from '../../../../../../../../store/src/actions/metrics.actions'; +import { generateCfBaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { FetchCFCellMetricsAction } from '../../../../../../actions/cf-metrics.actions'; import { ActiveRouteCfCell } from '../../../../cf-page.types'; import { CloudFoundryCellService } from '../cloud-foundry-cell.service'; import { CloudFoundryCellSummaryComponent } from './cloud-foundry-cell-summary.component'; -import { FetchCFCellMetricsAction } from '../../../../../../actions/cf-metrics.actions'; class MockCloudFoundryCellService { cfGuid = 'cfGuid'; @@ -60,8 +58,7 @@ class MockCloudFoundryCellService { } -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CloudFoundryCellSummaryComponent', () => { +describe('CloudFoundryCellSummaryComponent', () => { let component: CloudFoundryCellSummaryComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/cf-endpoint-details/cf-endpoint-details.component.scss b/src/frontend/packages/cloud-foundry/src/shared/components/cf-endpoint-details/cf-endpoint-details.component.scss index 5b7707aa0e..4a125b2a60 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/cf-endpoint-details/cf-endpoint-details.component.scss +++ b/src/frontend/packages/cloud-foundry/src/shared/components/cf-endpoint-details/cf-endpoint-details.component.scss @@ -11,6 +11,6 @@ align-items: center; display: flex; justify-content: center; - width: 32px; + margin-right: 8px; } } diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/components.module.ts b/src/frontend/packages/cloud-foundry/src/shared/components/components.module.ts index 197aca9173..0eab5d7bd5 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/components.module.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/components.module.ts @@ -100,6 +100,9 @@ import { import { TableCellEventTypeComponent, } from './list/list-types/cf-events/table-cell-event-type/table-cell-event-type.component'; +import { + TableCellFeatureFlagDescriptionComponent, +} from './list/list-types/cf-feature-flags/table-cell-feature-flag-description/table-cell-feature-flag-description.component'; import { TableCellFeatureFlagStateComponent, } from './list/list-types/cf-feature-flags/table-cell-feature-flag-state/table-cell-feature-flag-state.component'; @@ -175,7 +178,6 @@ import { SelectServiceComponent } from './select-service/select-service.componen import { ServiceIconComponent } from './service-icon/service-icon.component'; import { ServicePlanPriceComponent } from './service-plan-price/service-plan-price.component'; import { ServicePlanPublicComponent } from './service-plan-public/service-plan-public.component'; -import { TableCellFeatureFlagDescriptionComponent } from './list/list-types/cf-feature-flags/table-cell-feature-flag-description/table-cell-feature-flag-description.component'; // tslint:disable:max-line-length // tslint:enable:max-line-length @@ -219,7 +221,7 @@ const cfListTableCells: Type>[] = [ TableCellServiceBindableComponent, TableCellServiceActiveComponent, TableCellServiceReferencesComponent, - TableCellServiceInstanceTagsComponent + TableCellServiceInstanceTagsComponent, ]; const cfListCards: Type>[] = [ diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss index 3daaa41180..4f657dbc93 100644 --- a/src/frontend/packages/core/sass/_all-theme.scss +++ b/src/frontend/packages/core/sass/_all-theme.scss @@ -65,7 +65,9 @@ @import '../../cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-firehose/cloud-foundry-firehose.component.theme'; @import '../../cloud-foundry/src/features/service-catalog/service-catalog-page/service-catalog-page.component.theme'; @import '../../cloud-foundry/src/features/applications/application-wall/application-wall.component.theme'; -@import '../../core/src/features/error-page/error-page/error-page.component.theme.scss'; +@import '../../core/src/features/error-page/error-page/error-page.component.theme'; +@import '../../core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme'; +@import '../../core/src/features/metrics/metrics/metrics.component.theme'; // Defaults $side-nav-light-text: #fff; @@ -159,6 +161,8 @@ $side-nav-light-active: #484848; @include code-block-theme($theme, $app-theme); @include copy-to-clipboard-theme($theme, $app-theme); @include app-user-avatar-theme($theme, $app-theme); + @include restore-endpoints-theme($theme, $app-theme); + @include metrics-component-theme($theme, $app-theme); } @function app-generate-nav-theme($theme, $nav-theme: null) { diff --git a/src/frontend/packages/core/src/base-entity-types.ts b/src/frontend/packages/core/src/base-entity-types.ts index 33ba893917..37f2c840ac 100644 --- a/src/frontend/packages/core/src/base-entity-types.ts +++ b/src/frontend/packages/core/src/base-entity-types.ts @@ -1,8 +1,9 @@ -import { systemEndpointsReducer } from '../../store/src/reducers/system-endpoints.reducer'; +import { StratosCatalogEndpointEntity, StratosCatalogEntity } from '../../store/src/entity-catalog/entity-catalog-entity'; import { addOrUpdateUserFavoriteMetadataReducer, deleteUserFavoriteMetadataReducer, } from '../../store/src/reducers/favorite.reducer'; +import { systemEndpointsReducer } from '../../store/src/reducers/system-endpoints.reducer'; import { endpointEntitySchema, STRATOS_ENDPOINT_TYPE, @@ -10,8 +11,10 @@ import { userFavoritesEntitySchema, userProfileEntitySchema, } from './base-entity-schemas'; -import { StratosCatalogEndpointEntity, StratosCatalogEntity } from '../../store/src/entity-catalog/entity-catalog-entity'; import { BaseEndpointAuth } from './features/endpoints/endpoint-auth'; +import { + MetricsEndpointDetailsComponent, +} from './features/metrics/metrics-endpoint-details/metrics-endpoint-details.component'; // // These types are used to represent the base stratos types. @@ -94,7 +97,8 @@ export function generateStratosEntities() { tokenSharing: true, logoUrl: '/core/assets/endpoint-icons/metrics.svg', authTypes: [BaseEndpointAuth.UsernamePassword, BaseEndpointAuth.None], - renderPriority: 1 + renderPriority: 1, + listDetailsComponent: MetricsEndpointDetailsComponent, }, metadata => `/endpoints/metrics/${metadata.guid}` ) diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.html new file mode 100644 index 0000000000..3f240bbf9e --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.spec.ts new file mode 100644 index 0000000000..448a1eac17 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.spec.ts @@ -0,0 +1,45 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { BackupEndpointsService } from '../backup-endpoints.service'; +import { BackupEndpointTypes } from '../backup-restore.types'; +import { BackupCheckboxCellComponent } from './backup-checkbox-cell.component'; + +describe('BackupCheckboxCellComponent', () => { + let component: BackupCheckboxCellComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [BackupCheckboxCellComponent], + imports: [ + ...BaseTestModulesNoShared + ], + providers: [ + BackupEndpointsService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackupCheckboxCellComponent); + component = fixture.componentInstance; + component.config = { + type: BackupEndpointTypes.ENDPOINT + }; + component.row = { + guid: 'test', + cnsi_type: 'metrics', + } as EndpointModel; + component.service.initialize([{ + guid: 'test' + } as EndpointModel]); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.ts new file mode 100644 index 0000000000..0098d10c9b --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { TableCellCustom } from '../../../../shared/components/list/list.types'; +import { BackupEndpointsService } from '../backup-endpoints.service'; + +@Component({ + selector: 'app-backup-checkbox-cell', + templateUrl: './backup-checkbox-cell.component.html', + styleUrls: ['./backup-checkbox-cell.component.scss'] +}) +export class BackupCheckboxCellComponent extends TableCellCustom { + + constructor(public service: BackupEndpointsService) { + super(); + } + + validate() { + this.service.stateUpdated(); + } + + disabled(): boolean { + return !this.service.canBackupEndpoint(this.row, this.config.type); + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html new file mode 100644 index 0000000000..43e7eee490 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html @@ -0,0 +1,10 @@ + + + None + Current User + All Users + + +N/A \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts new file mode 100644 index 0000000000..09028f8d35 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts @@ -0,0 +1,43 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { BackupEndpointsService } from '../backup-endpoints.service'; +import { BackupConnectionCellComponent } from './backup-connection-cell.component'; + +describe('BackupConnectionCellComponent', () => { + let component: BackupConnectionCellComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + BackupConnectionCellComponent, + ], + imports: [ + ...BaseTestModulesNoShared + ], + providers: [ + BackupEndpointsService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackupConnectionCellComponent); + component = fixture.componentInstance; + component.row = { + guid: 'test', + cnsi_type: 'metrics', + } as EndpointModel; + component.service.initialize([{ + guid: 'test' + } as EndpointModel]); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts new file mode 100644 index 0000000000..978f098196 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts @@ -0,0 +1,36 @@ +import { Component, OnInit } from '@angular/core'; + +import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog.service'; +import { EndpointModel, SystemSharedUserGuid } from '../../../../../../store/src/types/endpoint.types'; +import { TableCellCustom } from '../../../../shared/components/list/list.types'; +import { BackupEndpointsService } from '../backup-endpoints.service'; +import { BackupEndpointConnectionTypes, BackupEndpointTypes } from '../backup-restore.types'; + +@Component({ + selector: 'app-backup-connection-cell', + templateUrl: './backup-connection-cell.component.html', + styleUrls: ['./backup-connection-cell.component.scss'] +}) +export class BackupConnectionCellComponent extends TableCellCustom implements OnInit { + + connectable = false; + backupType = BackupEndpointTypes; + connectionTypes = BackupEndpointConnectionTypes; + selected: BackupEndpointConnectionTypes; + userConnectionWarning: string; + + constructor(public service: BackupEndpointsService) { + super(); + } + + ngOnInit() { + const epType = entityCatalog.getEndpoint(this.row.cnsi_type, this.row.sub_type); + const epEntity = epType.definition; + this.connectable = !epEntity.unConnectable; + if (!this.row.user) { + this.userConnectionWarning = 'User not connected'; + } else if (this.row.user.guid === SystemSharedUserGuid) { + this.userConnectionWarning = 'User has shared connection'; + } + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts new file mode 100644 index 0000000000..d10c9144e8 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts @@ -0,0 +1,182 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; + +import { GeneralEntityAppState } from '../../../../../store/src/app-state'; +import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog.service'; +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; +import { BrowserStandardEncoder } from '../../../helper'; +import { + BackupEndpointConfigUI, + BackupEndpointConnectionTypes, + BackupEndpointsConfig, + BackupEndpointTypes, + BaseEndpointConfig, +} from './backup-restore.types'; + + +interface BackupRequest { + state: BackupEndpointsConfig; + password: string; +} + +@Injectable() +export class BackupEndpointsService { + + hasChanges = new BehaviorSubject(false); + hasChanges$ = this.hasChanges.asObservable(); + allChanged = new BehaviorSubject(false); + allChanged$ = this.allChanged.asObservable(); + + state: BackupEndpointsConfig = {}; + password: string; + + constructor( + private store: Store, + private http: HttpClient + ) { + } + + // State Related + initialize(endpoints: EndpointModel[]) { + endpoints.forEach(entity => { + this.state[entity.guid] = { + [BackupEndpointTypes.ENDPOINT]: false, + [BackupEndpointTypes.CONNECT]: BackupEndpointConnectionTypes.NONE, + entity + }; + }); + this.stateUpdated(); + } + + stateUpdated() { + const endpoints = Object.values(this.state); + endpoints.forEach(endpoint => { + if (!endpoint[BackupEndpointTypes.ENDPOINT]) { + endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.NONE; + } + }); + + const hasChanges = !!endpoints.find(endpoint => + endpoint[BackupEndpointTypes.ENDPOINT] || + endpoint[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE + ); + this.hasChanges.next(hasChanges); + const allChanged = endpoints.every(endpoint => { + const e = !this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.ENDPOINT) || endpoint[BackupEndpointTypes.ENDPOINT]; + const c = !this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.CONNECT) || + endpoint[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE; + return e && c; + } + + ); + this.allChanged.next(allChanged); + } + + canBackupEndpoint(endpoint: EndpointModel, type: BackupEndpointTypes): boolean { + // Can always back up endpoint + if (type === BackupEndpointTypes.ENDPOINT) { + return true; + } + + // All other settings require endpoint to be backed up + if (!this.state[endpoint.guid] || !this.state[endpoint.guid][BackupEndpointTypes.ENDPOINT]) { + return false; + } + + const epType = entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type).definition; + // The endpoint type supports connection details + if (epType.unConnectable) { + return false; + } + + return true; + } + + canBackup(): boolean { + return !!Object.values(this.state).length; + } + + selectAll() { + Object.values(this.state).forEach(endpoint => { + if (this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.ENDPOINT)) { + endpoint[BackupEndpointTypes.ENDPOINT] = true; + } + if (this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.CONNECT)) { + endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.ALL; + } + }); + this.stateUpdated(); + } + + selectNone() { + Object.values(this.state).forEach(endpoint => { + endpoint[BackupEndpointTypes.ENDPOINT] = false; + endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.NONE; + }); + this.stateUpdated(); + } + + hasConnectionDetails(): boolean { + return !!Object.values(this.state).find(e => e[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE); + } + + // Request Related + + createBackup(): Observable { + const url = '/pp/v1/endpoints/backup'; + const fromObject = {}; + const params: HttpParams = new HttpParams({ + fromObject, + encoder: new BrowserStandardEncoder() + }); + + // return this.getSessionData().pipe( + // switchMap(ses => this.http.post(url, this.createBodyToSend(ses), { params })), + // map(res => new Blob([JSON.stringify(res)])), + // first(), + // ); + return this.http.post(url, this.createBodyToSend(), { params }).pipe( + map(res => new Blob([JSON.stringify(res)])), + first(), + ); + } + + private createBodyToSend(): BackupRequest { + const state: BackupEndpointsConfig = Object.entries(this.state).reduce((res, [endpointId, endpoint]) => { + if (endpoint[BackupEndpointTypes.ENDPOINT]) { + const { entity, ...rest } = endpoint; + const requestConfig: BaseEndpointConfig = { + ...rest, + }; + res[endpointId] = requestConfig; + } + return res; + }, {}); + return { + state, + // userId: this.getUserIdFromSessionData(sd), + password: this.password, + }; + } + + // private getUserIdFromSessionData(sd: SessionData): string { + // if (sd && sd.user) { + // return sd.user.guid; + // } + // return null; + // } + + // private getSessionData(): Observable { + // return this.store.select(s => s.auth).pipe( + // filter(auth => !!(auth && auth.sessionData)), + // map((auth: AuthState) => auth.sessionData), + // first() + // ); + // } + + + +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html new file mode 100644 index 0000000000..f11198e59d --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html @@ -0,0 +1,43 @@ + +

Backup Endpoints

+
+ + + +
+

Select the endpoints and connection details that you would like to backup

+
+ + +
+ +
+ +

There are no endpoints to backup

+
+
+ +
+

Protect the backup by providing a password. You will need this password when restoring from this backup

+
+ + Password + + + + Password is required + + Password must be at least {{passwordForm.controls.password.errors.minlength.requiredLength}} characters + + +
+
+
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.scss new file mode 100644 index 0000000000..de139e1b04 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.scss @@ -0,0 +1,15 @@ +:host { + flex: 1; +} +.select-step { + display: flex; + flex: 1; + flex-direction: column; + + &__buttons { + padding-bottom: 12px; + button { + margin-right: 24px; + } + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts new file mode 100644 index 0000000000..3ccb5308ed --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabNavService } from '../../../../../tab-nav.service'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { SharedModule } from '../../../../shared/shared.module'; +import { BackupEndpointsComponent } from './backup-endpoints.component'; + +describe('BackupEndpointsComponent', () => { + let component: BackupEndpointsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [BackupEndpointsComponent], + imports: [ + ...BaseTestModulesNoShared, + SharedModule + ], + providers: [ + TabNavService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackupEndpointsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts new file mode 100644 index 0000000000..1d24656803 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts @@ -0,0 +1,187 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import * as moment from 'moment'; +import { Observable, of, Subject } from 'rxjs'; +import { filter, first, map } from 'rxjs/operators'; + +import { GetAllEndpoints } from '../../../../../../store/src/actions/endpoint.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog.service'; +import { PaginationMonitorFactory } from '../../../../../../store/src/monitors/pagination-monitor.factory'; +import { getPaginationObservables } from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; +import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { httpErrorResponseToSafeString } from '../../../../jetstream.helpers'; +import { ConfirmationDialogConfig } from '../../../../shared/components/confirmation-dialog.config'; +import { ConfirmationDialogService } from '../../../../shared/components/confirmation-dialog.service'; +import { ITableListDataSource } from '../../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { ITableColumn } from '../../../../shared/components/list/list-table/table.types'; +import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/components/stepper/step/step.component'; +import { BackupCheckboxCellComponent } from '../backup-checkbox-cell/backup-checkbox-cell.component'; +import { BackupConnectionCellComponent } from '../backup-connection-cell/backup-connection-cell.component'; +import { BackupEndpointsService } from '../backup-endpoints.service'; +import { BackupEndpointTypes } from '../backup-restore.types'; + +@Component({ + selector: 'app-backup-endpoints', + templateUrl: './backup-endpoints.component.html', + styleUrls: ['./backup-endpoints.component.scss'], + providers: [ + BackupEndpointsService + ] +}) +export class BackupEndpointsComponent { + + // Step 1 + columns: ITableColumn[] = [ + { + columnId: 'name', + headerCell: () => 'Name', + cellDefinition: { + valuePath: 'name' + } + }, + { + columnId: 'type', + headerCell: () => 'Type', + cellDefinition: { + getValue: this.getEndpointTypeString + }, + }, + { + columnId: 'endpoint', + headerCell: () => 'Backup', + cellComponent: BackupCheckboxCellComponent, + cellConfig: { + type: BackupEndpointTypes.ENDPOINT + } + }, + { + columnId: 'connect', + headerCell: () => 'Connection Details', + cellComponent: BackupConnectionCellComponent, + }, + ]; + endpointDataSource: ITableListDataSource; + disableSelectAll$: Observable; + disableSelectNone$: Observable; + selectValid$: Observable; + + // Step 2 + passwordValid$: Observable; + passwordForm: FormGroup; + show = false; + + constructor( + public service: BackupEndpointsService, + private store: Store, + private paginationMonitorFactory: PaginationMonitorFactory, + private confirmDialog: ConfirmationDialogService, + ) { + this.setupSelectStep(); + this.setupPasswordStep(); + } + + + setupSelectStep() { + const action = new GetAllEndpoints(); + const endpointObs = getPaginationObservables({ + store: this.store, + action, + paginationMonitor: this.paginationMonitorFactory.create( + action.paginationKey, + action, + true + ) + }, true); + + + const endpoints$ = endpointObs.entities$.pipe( + filter(entities => !!entities), + map(endpoints => endpoints.sort((a, b) => a.name.localeCompare(b.name))) + ); + + endpoints$.pipe(first()).subscribe(entities => this.service.initialize(entities)); + + this.endpointDataSource = { + isTableLoading$: endpointObs.fetchingEntities$, + connect: () => endpoints$, + disconnect: () => { }, + trackBy: (index, row) => row.guid + }; + + this.disableSelectAll$ = this.service.allChanged$; + this.disableSelectNone$ = this.service.hasChanges$.pipe( + map(hasChanges => !hasChanges) + ); + + this.selectValid$ = this.service.hasChanges$; + } + + setupPasswordStep() { + this.passwordForm = new FormGroup({ + password: new FormControl('', [Validators.required, Validators.minLength(6)]), + }); + this.passwordValid$ = this.passwordForm.statusChanges.pipe( + map(() => { + this.service.password = this.passwordForm.controls.password.value; + return this.passwordForm.valid; + }) + ); + } + + onNext: StepOnNextFunction = () => { + const confirmation = new ConfirmationDialogConfig( + 'Backup', + 'The backup that is about to be created may contain credentials, tokens and other sensitive information. Although it is encrypted, you should take the appropriate steps to secure it. ', + 'Continue', + true + ); + const result = new Subject(); + + const userCancelledDialog = () => { + result.next({ + success: false + }); + }; + + const backupSuccess = data => { + const downloadURL = window.URL.createObjectURL(data); + const link = document.createElement('a'); + link.href = downloadURL; + // Time of client, not server + const dateTime = moment().format('YYYYMMDD-HHmmss'); + link.download = `stratos_backup_${dateTime}.bk`; + link.click(); + + result.next({ + success: true, + redirect: true, + }); + }; + + const backupFailure = err => { + const errorMessage = httpErrorResponseToSafeString(err); + result.next({ + success: false, + message: `Failed to create backup` + (errorMessage ? `: ${errorMessage}` : '') + }); + return of(false); + }; + + const createBackup = () => this.service.createBackup().pipe(first()).subscribe(backupSuccess, backupFailure); + + if (this.service.hasConnectionDetails()) { + this.confirmDialog.openWithCancel(confirmation, createBackup, userCancelledDialog); + } else { + createBackup(); + } + + return result.asObservable(); + } + + + private getEndpointTypeString(endpoint: EndpointModel): string { + return entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type).definition.label; + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html new file mode 100644 index 0000000000..089175d985 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html @@ -0,0 +1,12 @@ + +

Backup/Restore Endpoints

+
+ + + +
+

Create a backup of endpoints and their connection details or restore from an existing backup.

+ +
+
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.scss new file mode 100644 index 0000000000..712926fb2c --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.scss @@ -0,0 +1,3 @@ +.tiles { + flex: 1; +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts new file mode 100644 index 0000000000..528a5ee7a9 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabNavService } from '../../../../../tab-nav.service'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { SharedModule } from '../../../../shared/shared.module'; +import { BackupRestoreEndpointsComponent } from './backup-restore-endpoints.component'; + +describe('BackupRestoreEndpointsComponent', () => { + let component: BackupRestoreEndpointsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [BackupRestoreEndpointsComponent], + imports: [ + ...BaseTestModulesNoShared, + SharedModule + ], + providers: [ + TabNavService + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackupRestoreEndpointsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts new file mode 100644 index 0000000000..615c1d2021 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { RouterNav } from '../../../../../../store/src/actions/router.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +import { ITileConfig, ITileData } from '../../../../shared/components/tile/tile-selector.types'; + +interface IAppTileData extends ITileData { + type: string; +} + + +@Component({ + selector: 'app-backup-restore-endpoints', + templateUrl: './backup-restore-endpoints.component.html', + styleUrls: ['./backup-restore-endpoints.component.scss'], +}) +export class BackupRestoreEndpointsComponent { + + public serviceType: string; + public tileSelectorConfig: ITileConfig[]; + + set selectedTile(tile: ITileConfig) { + if (tile) { + const url = 'endpoints/backup-restore/' + tile.data.type; + this.store.dispatch(new RouterNav({ path: url })); + } + } + + constructor( + private store: Store) { + this.tileSelectorConfig = [ + new ITileConfig( + 'Backup', + { matIcon: 'cloud_download' }, + { type: 'backup' } + ), + new ITileConfig( + 'Restore', + { matIcon: 'cloud_upload' }, + { type: 'restore' } + ) + ]; + } + +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore.types.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore.types.ts new file mode 100644 index 0000000000..edda11d612 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore.types.ts @@ -0,0 +1,25 @@ +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; + +export enum BackupEndpointTypes { + ENDPOINT = 'endpoint', + CONNECT = 'connect', +} + +export enum BackupEndpointConnectionTypes { + NONE = 'NONE', + CURRENT = 'CURRENT', + ALL = 'ALL' +} + +export interface BackupEndpointsConfig { + [endpointId: string]: T; +} + +export interface BaseEndpointConfig { + [BackupEndpointTypes.ENDPOINT]: boolean; + [BackupEndpointTypes.CONNECT]: BackupEndpointConnectionTypes; +} + +export interface BackupEndpointConfigUI extends BaseEndpointConfig { + entity: EndpointModel; +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts new file mode 100644 index 0000000000..8fa35048c1 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts @@ -0,0 +1,141 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { filter, map, switchMap } from 'rxjs/operators'; + +import { GeneralEntityAppState } from '../../../../../store/src/app-state'; +import { selectSessionData } from '../../../../../store/src/reducers/auth.reducer'; +import { SessionData } from '../../../../../store/src/types/auth.types'; +import { LoggerService } from '../../../core/logger.service'; +import { BrowserStandardEncoder } from '../../../helper'; + +interface BackupContent { + payload: string; + dbVersion: number; +} + +interface RestoreEndpointsData { + data: string; + password: string; + ignoreDbVersion: boolean; +} + +@Injectable() +export class RestoreEndpointsService { + + // Step 1 + validFileContent = new BehaviorSubject(false); + validFileContent$: Observable = this.validFileContent.asObservable(); + + file = new BehaviorSubject<{ + name: string, + content: BackupContent + }>(null); + file$ = this.file.asObservable(); + + validDb = new BehaviorSubject(false); + validDb$: Observable; + unparsableFileContent: string = null; + currentDbVersion$: Observable; + ignoreDbVersion = new BehaviorSubject(false); + ignoreDbVersion$ = this.ignoreDbVersion.asObservable(); + + // Step 2 + private password: string; + + constructor( + private store: Store, + private http: HttpClient, + private logger: LoggerService + ) { + this.setupStep1(); + } + + private setupStep1() { + this.currentDbVersion$ = this.store.select(selectSessionData()).pipe( + filter(sd => !!sd), + map((sd: SessionData) => sd.version.database_version) + ); + + this.validDb$ = combineLatest([ + this.file$, + this.currentDbVersion$ + ]).pipe( + filter(([file,]) => !!file && !!file.content), + map(([file, currentDbVersion]) => { + return file && file.content && file.content.dbVersion === currentDbVersion; + }) + ); + + this.validFileContent$ = combineLatest([ + this.file$, + this.validDb$, + this.ignoreDbVersion$ + ]).pipe( + map(([file, validDb, ignoreDb]) => !!file && (ignoreDb || validDb)) + ); + } + + setFile(file): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const res = reader.result as string; + this.setFileResult(res, file.name); + resolve(res); + }; + reader.onerror = () => this.setFileResult(null, null); + reader.onabort = () => this.setFileResult(null, null); + reader.readAsText(file); + }); + } + + private setFileResult(content: string, fileName: string) { + let parsedContent: BackupContent; + try { + parsedContent = JSON.parse(content); + this.unparsableFileContent = null; + } catch (err) { + this.logger.warn('Failed to parse file contents: ', err); + parsedContent = null; + this.unparsableFileContent = `${err instanceof Error ? err.message : String(err)}`; + } + + this.file.next({ + name: fileName, + content: parsedContent + }); + } + + setIgnoreDbVersion(ignore: boolean) { + this.ignoreDbVersion.next(ignore); + } + + setPassword(password: string) { + this.password = password; + } + + restoreBackup(): Observable { + const url = '/pp/v1/endpoints/restore'; + const fromObject = {}; + const params: HttpParams = new HttpParams({ + fromObject, + encoder: new BrowserStandardEncoder() + }); + + return combineLatest([ + this.file$, + this.ignoreDbVersion$ + ]).pipe( + switchMap(([file, ignoreDb]) => { + const body: RestoreEndpointsData = { + data: JSON.stringify(file.content), + password: this.password, + ignoreDbVersion: ignoreDb + }; + return this.http.post(url, body, { params }); + }) + ); + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html new file mode 100644 index 0000000000..1682c9b572 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html @@ -0,0 +1,59 @@ + +

Restore Endpoints

+
+ + + +
+
+

Provide the backup file to restore from.

+
+ Choose + + {{file.name}} +
+
+ +
+
+

+ warning The database version of Stratos + ({{service.currentDbVersion$ | async}}) and the backup + ({{file.content.dbVersion}}) are different. Restoring this file may have adverse affects. +

+ + Ignore different database versions + +
+
+

+ warningUnable to parse file contents. Reason:  + {{service.unparsableFileContent}} +

+
+
+
+
+
+ +
+

Provide the password that was given at the time the backup was created

+
+ + Password + + + + Password is required + + Password must be at least {{passwordForm.controls.password.errors.minlength.requiredLength}} characters + + +
+
+
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss new file mode 100644 index 0000000000..7fb53ed32c --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss @@ -0,0 +1,32 @@ +:host { + flex: 1; +} +.file-step { + + &, + &__chunk { + display: flex; + flex-direction: column; + } + + &__input { + &--input { + height: 0; + visibility: hidden; + width: 0; + } + button { + margin-right: 5px; + } + } + + &__error { + p { + align-items: center; + display: flex; + } + mat-icon { + margin-right: 10px; + } + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts new file mode 100644 index 0000000000..ff141f7953 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabNavService } from '../../../../../tab-nav.service'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { SharedModule } from '../../../../shared/shared.module'; +import { RestoreEndpointsComponent } from './restore-endpoints.component'; + +describe('RestoreEndpointsComponent', () => { + let component: RestoreEndpointsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [RestoreEndpointsComponent], + imports: [ + ...BaseTestModulesNoShared, + SharedModule + ], + providers: [ + TabNavService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RestoreEndpointsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme.scss new file mode 100644 index 0000000000..335545c7ad --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme.scss @@ -0,0 +1,11 @@ +@mixin restore-endpoints-theme($theme, $app-theme) { + $status-colors: map-get($app-theme, status); + $warn-color: map-get($status-colors, warning); + .file-step { + &__error { + mat-icon { + color: $warn-color; + } + } + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts new file mode 100644 index 0000000000..2904f6024d --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts @@ -0,0 +1,105 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatCheckboxChange } from '@angular/material'; +import { Store } from '@ngrx/store'; +import { Observable, of, Subject } from 'rxjs'; +import { first, map } from 'rxjs/operators'; + +import { GetAllEndpoints } from '../../../../../../store/src/actions/endpoint.actions'; +import { GeneralEntityAppState } from '../../../../../../store/src/app-state'; +import { getEventFiles } from '../../../../core/browser-helper'; +import { httpErrorResponseToSafeString } from '../../../../jetstream.helpers'; +import { ConfirmationDialogConfig } from '../../../../shared/components/confirmation-dialog.config'; +import { ConfirmationDialogService } from '../../../../shared/components/confirmation-dialog.service'; +import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/components/stepper/step/step.component'; +import { RestoreEndpointsService } from '../restore-endpoints.service'; + + +@Component({ + selector: 'app-restore-endpoints', + templateUrl: './restore-endpoints.component.html', + styleUrls: ['./restore-endpoints.component.scss'], + providers: [ + RestoreEndpointsService + ] +}) +export class RestoreEndpointsComponent { + + // Step 2 + passwordValid$: Observable; + passwordForm: FormGroup; + show = false; + + constructor( + private store: Store, + public service: RestoreEndpointsService, + private confirmDialog: ConfirmationDialogService, + ) { + this.setupPasswordStep(); + } + + setupPasswordStep() { + this.passwordForm = new FormGroup({ + password: new FormControl('', [Validators.required, Validators.minLength(6)]), + }); + this.passwordValid$ = this.passwordForm.statusChanges.pipe( + map(() => { + this.service.setPassword(this.passwordForm.controls.password.value); + return this.passwordForm.valid; + }) + ); + } + + onFileChange(event) { + const files = getEventFiles(event); + if (!files.length) { + return; + } + const file = files[0]; + this.service.setFile(file); + } + + onIgnoreDbChange(event: MatCheckboxChange) { + this.service.setIgnoreDbVersion(event.checked); + } + + restore: StepOnNextFunction = () => { + const confirmation = new ConfirmationDialogConfig( + 'Restore', + 'This will overwrite any matching endpoints and connection details.', + 'Continue', + true + ); + const result = new Subject(); + + const userCancelledDialog = () => { + result.next({ + success: false + }); + }; + + const restoreSuccess = () => { + this.store.dispatch(new GetAllEndpoints()); + result.next({ + success: true, + redirect: true, + }); + }; + + const backupFailure = err => { + const errorMessage = httpErrorResponseToSafeString(err); + result.next({ + success: false, + message: `Failed to restore backup` + (errorMessage ? `: ${errorMessage}` : '') + }); + return of(false); + }; + + const restoreBackup = () => this.service.restoreBackup().pipe(first()).subscribe(restoreSuccess, backupFailure); + + this.confirmDialog.openWithCancel(confirmation, restoreBackup, userCancelledDialog); + + return result.asObservable(); + } + +} diff --git a/src/frontend/packages/core/src/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.spec.ts index 5f93a9992b..93034184a4 100644 --- a/src/frontend/packages/core/src/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.spec.ts @@ -4,10 +4,11 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../core/core.module'; +import { SidePanelService } from '../../../shared/services/side-panel.service'; import { SharedModule } from '../../../shared/shared.module'; import { ConnectEndpointComponent } from '../connect-endpoint/connect-endpoint.component'; import { ConnectEndpointConfig } from '../connect.service'; @@ -25,8 +26,7 @@ class MatDialogDataMock implements ConnectEndpointConfig { ssoAllowed = false; } -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('ConnectEndpointDialogComponent', () => { +describe('ConnectEndpointDialogComponent', () => { let component: ConnectEndpointDialogComponent; let fixture: ComponentFixture; @@ -34,7 +34,8 @@ xdescribe('ConnectEndpointDialogComponent', () => { const testingModule = TestBed.configureTestingModule({ providers: [ { provide: MatDialogRef, useClass: MatDialogRefMock }, - { provide: MAT_DIALOG_DATA, useClass: MatDialogDataMock } + { provide: MAT_DIALOG_DATA, useClass: MatDialogDataMock }, + SidePanelService ], declarations: [ ConnectEndpointDialogComponent, diff --git a/src/frontend/packages/core/src/features/endpoints/connect-endpoint/connect-endpoint.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/connect-endpoint/connect-endpoint.component.spec.ts index 3097c898b3..d265f5b75b 100644 --- a/src/frontend/packages/core/src/features/endpoints/connect-endpoint/connect-endpoint.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/connect-endpoint/connect-endpoint.component.spec.ts @@ -1,14 +1,13 @@ import { CommonModule } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../core/core.module'; import { SharedModule } from '../../../shared/shared.module'; import { ConnectEndpointComponent } from './connect-endpoint.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('ConnectEndpointComponent', () => { +describe('ConnectEndpointComponent', () => { let component: ConnectEndpointComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts index 0c9396218b..0d1c1e7a2c 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts @@ -1,15 +1,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreTestingModule } from '../../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../../core/core.module'; import { SharedModule } from '../../../../shared/shared.module'; import { CreateEndpointCfStep1Component } from './create-endpoint-cf-step-1.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CreateEndpointCfStep1Component', () => { +describe('CreateEndpointCfStep1Component', () => { let component: CreateEndpointCfStep1Component; let fixture: ComponentFixture; diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts index 36294cefcf..dda3a78ace 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts @@ -1,21 +1,21 @@ +import { HttpClientModule } from '@angular/common/http'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { TabNavService } from '../../../../tab-nav.service'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../core/core.module'; +import { SidePanelService } from '../../../shared/services/side-panel.service'; import { SharedModule } from '../../../shared/shared.module'; import { ConnectEndpointComponent } from '../connect-endpoint/connect-endpoint.component'; import { CreateEndpointCfStep1Component } from './create-endpoint-cf-step-1/create-endpoint-cf-step-1.component'; import { CreateEndpointConnectComponent } from './create-endpoint-connect/create-endpoint-connect.component'; import { CreateEndpointComponent } from './create-endpoint.component'; -import { HttpClientModule } from '@angular/common/http'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CreateEndpointComponent', () => { +describe('CreateEndpointComponent', () => { let component: CreateEndpointComponent; let fixture: ComponentFixture; @@ -47,7 +47,9 @@ xdescribe('CreateEndpointComponent', () => { } } } - }, TabNavService], + }, + TabNavService, + SidePanelService], }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html index ee90c52f27..c8e3f991c3 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html @@ -1,9 +1,14 @@

Endpoints

- +
diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts b/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts index b68dec67b8..44a05e8e2a 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts @@ -1,16 +1,23 @@ -import { EditEndpointStepComponent } from './edit-endpoint/edit-endpoint-step/edit-endpoint-step.component'; import { NgModule } from '@angular/core'; import { CoreModule } from '../../core/core.module'; import { SharedModule } from '../../shared/shared.module'; +import { BackupCheckboxCellComponent } from './backup-restore/backup-checkbox-cell/backup-checkbox-cell.component'; +import { BackupConnectionCellComponent } from './backup-restore/backup-connection-cell/backup-connection-cell.component'; +import { BackupEndpointsComponent } from './backup-restore/backup-endpoints/backup-endpoints.component'; +import { + BackupRestoreEndpointsComponent, +} from './backup-restore/backup-restore-endpoints/backup-restore-endpoints.component'; +import { RestoreEndpointsComponent } from './backup-restore/restore-endpoints/restore-endpoints.component'; import { CredentialsAuthFormComponent } from './connect-endpoint-dialog/auth-forms/credentials-auth-form.component'; import { NoneAuthFormComponent } from './connect-endpoint-dialog/auth-forms/none-auth-form.component'; import { SSOAuthFormComponent } from './connect-endpoint-dialog/auth-forms/sso-auth-form.component'; import { ConnectEndpointDialogComponent } from './connect-endpoint-dialog/connect-endpoint-dialog.component'; import { CreateEndpointModule } from './create-endpoint/create-endpoint.module'; +import { EditEndpointStepComponent } from './edit-endpoint/edit-endpoint-step/edit-endpoint-step.component'; +import { EditEndpointComponent } from './edit-endpoint/edit-endpoint.component'; import { EndpointsPageComponent } from './endpoints-page/endpoints-page.component'; import { EndpointsRoutingModule } from './endpoints.routing'; -import { EditEndpointComponent } from './edit-endpoint/edit-endpoint.component'; @NgModule({ imports: [ @@ -27,12 +34,19 @@ import { EditEndpointComponent } from './edit-endpoint/edit-endpoint.component'; NoneAuthFormComponent, EditEndpointComponent, EditEndpointStepComponent, + BackupRestoreEndpointsComponent, + BackupEndpointsComponent, + RestoreEndpointsComponent, + BackupCheckboxCellComponent, + BackupConnectionCellComponent, ], entryComponents: [ ConnectEndpointDialogComponent, CredentialsAuthFormComponent, SSOAuthFormComponent, - NoneAuthFormComponent + NoneAuthFormComponent, + BackupCheckboxCellComponent, + BackupConnectionCellComponent ] }) export class EndpointsModule { } diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints.routing.ts b/src/frontend/packages/core/src/features/endpoints/endpoints.routing.ts index 2b9fcb25c4..194357b894 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints.routing.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints.routing.ts @@ -4,12 +4,17 @@ import { RouterModule, Routes } from '@angular/router'; import { DynamicExtensionRoutes } from '../../core/extension/dynamic-extension-routes'; import { StratosActionType } from '../../core/extension/extension-service'; import { PageNotFoundComponentComponent } from '../../core/page-not-found-component/page-not-found-component.component'; +import { BackupEndpointsComponent } from './backup-restore/backup-endpoints/backup-endpoints.component'; +import { + BackupRestoreEndpointsComponent, +} from './backup-restore/backup-restore-endpoints/backup-restore-endpoints.component'; +import { RestoreEndpointsComponent } from './backup-restore/restore-endpoints/restore-endpoints.component'; import { CreateEndpointBaseStepComponent, } from './create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component'; import { CreateEndpointComponent } from './create-endpoint/create-endpoint.component'; -import { EndpointsPageComponent } from './endpoints-page/endpoints-page.component'; import { EditEndpointComponent } from './edit-endpoint/edit-endpoint.component'; +import { EndpointsPageComponent } from './endpoints-page/endpoints-page.component'; const endpointsRoutes: Routes = [ { @@ -34,6 +39,18 @@ const endpointsRoutes: Routes = [ path: 'edit/:id', component: EditEndpointComponent }, + { + path: 'backup-restore', + component: BackupRestoreEndpointsComponent + }, + { + path: 'backup-restore/backup', + component: BackupEndpointsComponent + }, + { + path: 'backup-restore/restore', + component: RestoreEndpointsComponent + }, { path: '**', component: PageNotFoundComponentComponent, diff --git a/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.html b/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.html new file mode 100644 index 0000000000..d15144178e --- /dev/null +++ b/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.html @@ -0,0 +1,9 @@ +
+
+ warning + {{ info.ok }} + / {{info.total}} + sources + source +
+
diff --git a/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.scss b/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.scss new file mode 100644 index 0000000000..c18a9f0f79 --- /dev/null +++ b/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.scss @@ -0,0 +1,16 @@ +.metrics-details { + display: flex; + flex-direction: column; + + &__line { + align-items: center; + display: flex; + } + + &__icon { + align-items: center; + display: flex; + justify-content: center; + margin-right: 8px; + } +} diff --git a/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.spec.ts b/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.spec.ts new file mode 100644 index 0000000000..b825e86948 --- /dev/null +++ b/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.spec.ts @@ -0,0 +1,37 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { createBasicStoreModule } from '../../../../../store/testing/src/store-test-helper'; +import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; +import { SharedModule } from '../../../shared/shared.module'; +import { MetricsService } from '../services/metrics-service'; +import { CoreModule } from './../../../core/core.module'; +import { MetricsEndpointDetailsComponent } from './metrics-endpoint-details.component'; + +describe('MetricsEndpointDetailsComponent', () => { + let component: MetricsEndpointDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CoreModule, + SharedModule, + CoreTestingModule, + createBasicStoreModule() + ], + declarations: [ MetricsEndpointDetailsComponent ], + providers: [ MetricsService ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MetricsEndpointDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.ts b/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.ts new file mode 100644 index 0000000000..0b47762037 --- /dev/null +++ b/src/frontend/packages/core/src/features/metrics/metrics-endpoint-details/metrics-endpoint-details.component.ts @@ -0,0 +1,88 @@ +import { Component, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, filter, map, publishReplay, refCount, tap } from 'rxjs/operators'; + +import { MetricsStratosAction } from '../../../../../store/src/actions/metrics-api.actions'; +import { AppState } from '../../../../../store/src/app-state'; +import { EndpointListDetailsComponent } from '../../../shared/components/list/list-types/endpoint/endpoint-list.helpers'; +import { mapMetricsData } from '../metrics.helpers'; +import { MetricsEndpointProvider, MetricsService } from '../services/metrics-service'; +import { EndpointModel } from './../../../../../store/src/types/endpoint.types'; + + +interface MetricsDetailsInfo { + ok: number; + total: number; + warning: boolean; + plural: boolean; +} + +@Component({ + selector: 'app-metrics-endpoint-details', + templateUrl: './metrics-endpoint-details.component.html', + styleUrls: ['./metrics-endpoint-details.component.scss'] +}) +export class MetricsEndpointDetailsComponent extends EndpointListDetailsComponent { + + data$: Observable; + + // The guid of the metrics endpoint that this row shows + guid$ = new BehaviorSubject(null); + + constructor( + public store: Store, + private metricsService: MetricsService + ) { + super(); + + const endpoints$ = this.metricsService.metricsEndpoints$.pipe( + filter(endpoints => !!endpoints), + distinctUntilChanged() + ); + + const guid$ = this.guid$.asObservable().pipe( + filter(guid => !!guid), + distinctUntilChanged() + ); + + // Raw endpoint data for this metrics endpoint + this.data$ = combineLatest( + endpoints$, + guid$ + ).pipe( + map(([endpoints, guid]) => endpoints.find((item) => item.provider.guid === guid)), + filter(provider => !!provider), + tap(data => { + if (!this.hasStratosData(data)) { + this.store.dispatch(new MetricsStratosAction(data.provider.guid)); + } + }), + map((provider) => this.processProvider(provider)), + publishReplay(1), + refCount() + ); + } + + private hasStratosData(provider: MetricsEndpointProvider): boolean { + const data = provider.provider; + return !!data && !!data.metadata && !!data.metadata.metrics_stratos; + } + + private processProvider(provider: MetricsEndpointProvider): MetricsDetailsInfo { + const hasStratosData = this.hasStratosData(provider); + const parsed = mapMetricsData(provider); + const known = parsed.filter(item => item.known).length; + return { + ok: known, + total: hasStratosData ? parsed.length : -1, + warning: known === 0, + plural: hasStratosData ? parsed.length !== 1 : known !== 1, + }; + } + + @Input() + set row(data: EndpointModel) { + this.guid$.next(data.guid); + } +} diff --git a/src/frontend/packages/core/src/features/metrics/metrics.helpers.ts b/src/frontend/packages/core/src/features/metrics/metrics.helpers.ts new file mode 100644 index 0000000000..f3bb9d0763 --- /dev/null +++ b/src/frontend/packages/core/src/features/metrics/metrics.helpers.ts @@ -0,0 +1,74 @@ +import { Observable, of as observableOf } from 'rxjs'; + +import { StratosStatus } from '../../shared/shared.types'; +import { EndpointIcon, getFullEndpointApiUrl } from '../endpoints/endpoint-helpers'; +import { entityCatalog } from './../../../../store/src/entity-catalog/entity-catalog.service'; +import { MetricsEndpointProvider } from './services/metrics-service'; + +// Info for an endpoint that a metrics endpoint provides for +export interface MetricsEndpointInfo { + name: string; + icon: EndpointIcon; + type: string; + known: boolean; + url: string; + metadata: { + metrics_job?: string; + metrics_environment?: string; + }; + status: Observable; +} + +// Process the endpoint and Stratos marker file data to give a single list of endpoitns +// linked to this metrics endpoint, comprising those that are known in Stratos and those that are not +export function mapMetricsData(ep: MetricsEndpointProvider): MetricsEndpointInfo[] { + const data: MetricsEndpointInfo[] = []; + + // Add all of the known endpoints first + ep.endpoints.forEach(endpoint => { + const catalogEndpoint = entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type); + + data.push({ + known: true, + name: endpoint.name, + url: getFullEndpointApiUrl(endpoint), + type: catalogEndpoint.definition.label, + icon: { + name: catalogEndpoint.definition.icon, + font: 'stratos-icons' + }, + metadata: { + metrics_job: endpoint.metadata ? endpoint.metadata.metrics_job : null, + metrics_environment: endpoint.metadata ? endpoint.metadata.metrics_environment : null + }, + status: observableOf(StratosStatus.OK) + }); + }); + + // Add all of the potentially unknown endpoints + if (ep.provider && ep.provider.metadata && ep.provider.metadata && ep.provider.metadata.metrics_stratos + && Array.isArray(ep.provider.metadata.metrics_stratos)) { + ep.provider.metadata.metrics_stratos.forEach(endp => { + // See if we already know about this endpoint + const hasEndpoint = data.findIndex(i => i.url === endp.url || i.url === endp.cfEndpoint) !== -1; + if (!hasEndpoint) { + const catalogEndpoint = entityCatalog.getEndpoint(endp.type, ''); + data.push({ + known: false, + name: '', + url: endp.cfEndpoint || endp.url, + type: catalogEndpoint.definition.label, + icon: { + name: catalogEndpoint.definition.icon, + font: 'stratos-icons' + }, + metadata: { + metrics_job: endp.job + }, + status: observableOf(StratosStatus.WARNING) + }); + } + }); + } + return data; + } diff --git a/src/frontend/packages/core/src/features/metrics/metrics.module.ts b/src/frontend/packages/core/src/features/metrics/metrics.module.ts index 7aa2a94888..9c14db9211 100644 --- a/src/frontend/packages/core/src/features/metrics/metrics.module.ts +++ b/src/frontend/packages/core/src/features/metrics/metrics.module.ts @@ -1,10 +1,12 @@ -import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { MetricsComponent } from './metrics/metrics.component'; -import { MetricsRoutingModule } from './metrics.routing'; -import { MetricsService } from './services/metrics-service'; +import { NgModule } from '@angular/core'; + import { CoreModule } from '../../core/core.module'; import { SharedModule } from '../../shared/shared.module'; +import { MetricsEndpointDetailsComponent } from './metrics-endpoint-details/metrics-endpoint-details.component'; +import { MetricsRoutingModule } from './metrics.routing'; +import { MetricsComponent } from './metrics/metrics.component'; +import { MetricsService } from './services/metrics-service'; @NgModule({ imports: [ @@ -13,9 +15,12 @@ import { SharedModule } from '../../shared/shared.module'; SharedModule, MetricsRoutingModule, ], - declarations: [MetricsComponent], + declarations: [MetricsComponent, MetricsEndpointDetailsComponent], providers: [ MetricsService, + ], + entryComponents: [ + MetricsEndpointDetailsComponent, ] }) export class MetricsModule { } diff --git a/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.html b/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.html index 3d5b4dfaf9..6407fa952b 100644 --- a/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.html +++ b/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.html @@ -1,4 +1,4 @@ -{{ (metricsEndpoint$ | async)?.entity.provider.name }} +{{ (metricsEndpoint$ | async)?.provider.name }}
@@ -6,57 +6,67 @@
equalizer
-
{{ ep.entity.provider.name }}
+
{{ ep.provider.name }}

- {{ ep.entity.provider.token_endpoint }} + {{ ep.provider.token_endpoint }}

+
+

This metrics endpoint does not provide a Stratos metadata file

+
-
-

Provides metrics for the following endpoints:

-
- -
-

Does not provide metrics for any endpoints

-
- -
-
- - -
- {{ ep.metadata[svc.guid].icon.name }} -