diff --git a/.gitignore b/.gitignore index 409ad5df8e..ce1c94fc35 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,9 @@ deploy/stratos-ui-release/dev_releases/ output/fissile deploy/ci/travis/temp/ +# Helm Chart Image List +deploy/kubernetes/console/imagelist.txt + .dist/ deploy/uaa/tmp/ src/backend/*/vendor/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b73fe024d9..d669ead0ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## 2.6.1 + +[Full Changelog](https://github.com/cloudfoundry/stratos/compare/2.6.0...2.6.1) + +This release contains a few fixes: + +**Fixes:** + +- Helm Chart does not work with Kubernetes 1.16 [\#4022](https://github.com/cloudfoundry/stratos/issues/4022) +- Generated Ingress certificates during Kubernetes deployment are empty [\#4006](https://github.com/cloudfoundry/stratos/issues/4006) +- Kubernetes Ingress certificate is incorrectly set [\#4005](https://github.com/cloudfoundry/stratos/issues/4005) +- Update metric used for cells [\#4009](https://github.com/cloudfoundry/stratos/pull/4009) +- Fix incorrect SSO behaviour following 2.4.0 --> 2.6.0 upgrade [\#4015](https://github.com/cloudfoundry/stratos/pull/4015) + ## 2.6.0 [Full Changelog](https://github.com/cloudfoundry/stratos/compare/2.5.0...2.6.0) diff --git a/README.md b/README.md index ad7817d1d1..d98bde0082 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,4 @@ Tested with Browserstack ## License The work done has been licensed under Apache License 2.0. The license file can be found [here](LICENSE). + diff --git a/deploy/Dockerfile.all-in-one b/deploy/Dockerfile.all-in-one index ed3391cea7..ce046c3325 100644 --- a/deploy/Dockerfile.all-in-one +++ b/deploy/Dockerfile.all-in-one @@ -1,5 +1,5 @@ # Docker build for all-in-one Stratos -FROM splatform/stratos-aio-base:opensuse as builder +FROM splatform/stratos-aio-base:leap15_1 as builder # Ensure that we copy the custom-src folder COPY --chown=stratos:users . ./ @@ -17,7 +17,7 @@ RUN CERTS_PATH=/home/stratos/dev-certs ./generate_cert.sh \ && chmod +x jetstream # use --target=aio to build All-in-one image -FROM splatform/stratos-bk-base:opensuse +FROM splatform/stratos-bk-base:leap15_1 ARG CANARY_BUILD COPY --from=builder /home/stratos/deploy/db /src/deploy/db COPY --from=builder /home/stratos/dev-certs /srv/dev-certs diff --git a/deploy/Dockerfile.bk b/deploy/Dockerfile.bk index 41ec20f1fb..8317187079 100644 --- a/deploy/Dockerfile.bk +++ b/deploy/Dockerfile.bk @@ -1,4 +1,4 @@ -FROM splatform/stratos-bk-build-base:opensuse as builder +FROM splatform/stratos-bk-build-base:leap15_1 as builder ARG stratos_version RUN mkdir -p /home/stratos WORKDIR /home/stratos @@ -7,12 +7,12 @@ RUN go version RUN npm install RUN npm run build-backend -FROM splatform/stratos-bk-base:opensuse as common-build +FROM splatform/stratos-bk-base:leap15_1 as common-build COPY --from=builder /home/stratos/src/jetstream/jetstream /srv/ RUN chmod +x /srv/jetstream # use --target=db-migrator to build db-migrator image -FROM splatform/stratos-bk-base:opensuse as db-migrator +FROM splatform/stratos-bk-base:leap15_1 as db-migrator WORKDIR /src COPY deploy/db/dbconf.yml db/dbconf.yml COPY deploy/db/scripts/development.sh . @@ -23,7 +23,7 @@ RUN chmod +x development.sh CMD bash /src/wait-for-it.sh -t 90 mariadb:3306 && bash /src/development.sh # use --target=postflight-job to build prod postflight-job -FROM splatform/stratos-bk-base:opensuse as postflight-job +FROM splatform/stratos-bk-base:leap15_1 as postflight-job RUN zypper -n in mariadb-client COPY --from=common-build /srv/jetstream /usr/local/bin/jetstream COPY deploy/db/dbconf.yml db/dbconf.yml @@ -31,7 +31,7 @@ COPY deploy/db/scripts/run-postflight-job.k8s.sh /run-postflight-job.sh CMD ["/run-postflight-job.sh"] # use --target=prod-build to build a backend image for Kubernetes -FROM splatform/stratos-bk-base:opensuse as prod-build +FROM splatform/stratos-bk-base:leap15_1 as prod-build RUN zypper in -y curl COPY deploy/containers/proxy/entrypoint.sh /entrypoint.sh COPY /deploy/db/scripts/run-preflight-job.sh /run-preflight-job.sh diff --git a/deploy/Dockerfile.ui b/deploy/Dockerfile.ui index 66856bb28c..19ee495543 100644 --- a/deploy/Dockerfile.ui +++ b/deploy/Dockerfile.ui @@ -1,4 +1,4 @@ -FROM splatform/stratos-ui-build-base:opensuse as base-build +FROM splatform/stratos-ui-build-base:leap15_1 as base-build ARG project ARG branch ARG commit @@ -13,7 +13,7 @@ RUN npm install && \ mkdir -p /usr/dist && \ cp -R dist/* /usr/dist -FROM splatform/stratos-nginx-base:opensuse as prod-build +FROM splatform/stratos-nginx-base:leap15_1 as prod-build RUN mkdir -p /usr/share/doc/suse COPY deploy/containers/nginx/LICENSE.txt /usr/share/doc/suse/LICENSE.txt COPY deploy/containers/nginx/conf/nginx.k8s.conf /etc/nginx/nginx.conf @@ -22,7 +22,7 @@ COPY deploy/containers/nginx/run-nginx.sh/ /run-nginx.sh EXPOSE 80 443 CMD [ "/run-nginx.sh" ] -FROM splatform/stratos-nginx-base:opensuse as dev-build +FROM splatform/stratos-nginx-base:leap15_1 as dev-build RUN mkdir -p /etc/secrets/ && \ openssl req -batch -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /etc/secrets/server.key -out /etc/secrets/server.crt && \ chmod 0600 /etc/secrets && \ diff --git a/deploy/ci/scripts/Dockerfile.stratos-ci b/deploy/ci/scripts/Dockerfile.stratos-ci index a9c9dcbc1d..b2e9ec19b7 100644 --- a/deploy/ci/scripts/Dockerfile.stratos-ci +++ b/deploy/ci/scripts/Dockerfile.stratos-ci @@ -6,7 +6,7 @@ # Default Image used to run tasks - contains Helm # Builder for the github release tool -FROM splatform/stratos-go-build-base:opensuse as go-base +FROM splatform/stratos-go-build-base:leap15_1 as go-base RUN export GOPATH=/home/stratos/go && \ mkdir -p ${GOPATH} && \ go get github.com/aktau/github-release diff --git a/deploy/ci/tasks/dev-releases/github-helper.sh b/deploy/ci/tasks/dev-releases/github-helper.sh index 632d4353b0..a4b0765286 100644 --- a/deploy/ci/tasks/dev-releases/github-helper.sh +++ b/deploy/ci/tasks/dev-releases/github-helper.sh @@ -39,7 +39,7 @@ downloadReleaseFile() { local parser=". | map(select(.tag_name == \"$VERSION\"))[0].assets | map(select(.name == \"$FILE\"))[0].id" # Get release information from GitHub - curl -H "Authorization: token $TOKEN" -H "Accept: application/vnd.github.v3.raw" -s $GITHUB/repos/$REPO/releases > releases.json + curl -L -H "Authorization: token $TOKEN" -H "Accept: application/vnd.github.v3.raw" -s $GITHUB/repos/$REPO/releases > releases.json if [ $? -ne 0 ]; then echo "Could not download release information for ${REPO}" exit 1 diff --git a/deploy/common-build.sh b/deploy/common-build.sh index ee5c771c20..2e8906adf4 100644 --- a/deploy/common-build.sh +++ b/deploy/common-build.sh @@ -43,7 +43,7 @@ function buildAndPublishImage { # Proxy support # Remove intermediate containers after a successful build -BUILD_ARGS="--rm=true" +BUILD_ARGS="--rm=true --squash" RUN_ARGS="" if [ -n "${http_proxy:-}" -o -n "${HTTP_PROXY:-}" ]; then BUILD_ARGS="${BUILD_ARGS} --build-arg http_proxy=${http_proxy:-${HTTP_PROXY}}" diff --git a/deploy/containers/nginx/Dockerfile.dc b/deploy/containers/nginx/Dockerfile.dc index 7af4708169..049c6c9a5a 100644 --- a/deploy/containers/nginx/Dockerfile.dc +++ b/deploy/containers/nginx/Dockerfile.dc @@ -1,4 +1,4 @@ -FROM splatform/stratos-nginx-base:opensuse +FROM splatform/stratos-nginx-base:leap15_1 RUN mkdir -p /etc/secrets/ && \ openssl req -batch -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /etc/secrets/server.key -out /etc/secrets/server.crt && \ diff --git a/deploy/db/Dockerfile.mariadb b/deploy/db/Dockerfile.mariadb index 9bcb558457..885f8d3cc3 100644 --- a/deploy/db/Dockerfile.mariadb +++ b/deploy/db/Dockerfile.mariadb @@ -1,6 +1,13 @@ -FROM splatform/stratos-db-base:opensuse +FROM splatform/stratos-db-base:leap15_1 + +# See: https://github.com/docker-library/mariadb/blob/master/10.2/Dockerfile +RUN \ + find /etc/ -name 'my*.cnf' -print0 \ + | xargs -0 grep -lZE '^(bind-address|log)' \ + | xargs -rt -0 sed -Ei 's/^(bind-address|log)/#&/'; COPY mariadb-entrypoint.sh /docker-entrypoint.sh +COPY mariadb-ping.sh /dbping.sh # ENTRYPOINT ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/deploy/db/mariadb-entrypoint.sh b/deploy/db/mariadb-entrypoint.sh index 45e0471437..48e04e3142 100755 --- a/deploy/db/mariadb-entrypoint.sh +++ b/deploy/db/mariadb-entrypoint.sh @@ -1,49 +1,214 @@ #!/bin/bash -set -e - -MYSQL_DATADIR="/var/lib/mysql" - -if [ ! -d "$MYSQL_DATADIR/mysql" ]; then - # if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then - # echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set' - # echo >&2 ' Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?' - # exit 1 - # fi - - echo 'Running mysql_install_db ...' - mysql_install_db --user=mysql --datadir="$MYSQL_DATADIR" - echo 'Finished mysql_install_db' - - # These statements _must_ be on individual lines, and _must_ end with - # semicolons (no line breaks or comments are permitted). - # TODO proper SQL escaping on ALL the things D: - - tempSqlFile='/tmp/mysql-first-time.sql' - cat > "$tempSqlFile" <<-EOSQL -DELETE FROM mysql.user ; -CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; -GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ; -DROP DATABASE IF EXISTS test ; -EOSQL - - if [ "$MYSQL_DATABASE" ]; then - echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" >> "$tempSqlFile" - fi - - if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then - echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" >> "$tempSqlFile" - - if [ "$MYSQL_DATABASE" ]; then - echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" >> "$tempSqlFile" - fi - fi - - echo 'FLUSH PRIVILEGES ;' >> "$tempSqlFile" - set -- "$@" --init-file="$tempSqlFile" + +########################################################################################################### +# +# This is the entrypoint script taken from here: https://github.com/docker-library/mariadb/tree/master/10.2 +# +# There is one change - which is clearly marked below +# +########################################################################################################### + +set -eo pipefail +shopt -s nullglob + +# if command starts with an option, prepend mysqld +if [ "${1:0:1}" = '-' ]; then + set -- mysqld "$@" fi -chown -R mysql:mysql "$MYSQL_DATADIR" -mkdir /var/run/mysql -chown -R mysql:mysql /var/run/mysql +# skip setup if they want an option that stops mysqld +wantHelp= +for arg; do + case "$arg" in + -'?'|--help|--print-defaults|-V|--version) + wantHelp=1 + break + ;; + esac +done + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + export "$var"="$val" + unset "$fileVar" +} + +_check_config() { + toRun=( "$@" --verbose --help --log-bin-index="$(mktemp -u)" ) + if ! errors="$("${toRun[@]}" 2>&1 >/dev/null)"; then + cat >&2 <<-EOM + + ERROR: mysqld failed while attempting to check config + command was: "${toRun[*]}" + + $errors + EOM + exit 1 + fi +} + +# Fetch value from server config +# We use mysqld --verbose --help instead of my_print_defaults because the +# latter only show values present in config files, and not server defaults +_get_config() { + local conf="$1"; shift + "$@" --verbose --help --log-bin-index="$(mktemp -u)" 2>/dev/null \ + | awk '$1 == "'"$conf"'" && /^[^ \t]/ { sub(/^[^ \t]+[ \t]+/, ""); print; exit }' + # match "datadir /some/path with/spaces in/it here" but not "--xyz=abc\n datadir (xyz)" +} + +# allow the container to be started with `--user` +if [ "$1" = 'mysqld' -a -z "$wantHelp" -a "$(id -u)" = '0' ]; then + _check_config "$@" + DATADIR="$(_get_config 'datadir' "$@")" + mkdir -p "$DATADIR" + find "$DATADIR" \! -user mysql -exec chown mysql '{}' + + exec gosu mysql "$BASH_SOURCE" "$@" +fi + +if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then + # still need to check config, container may have started with --user + _check_config "$@" + # Get config + DATADIR="$(_get_config 'datadir' "$@")" + + echo "Data dir is: ${DATADIR}" + + if [ ! -d "$DATADIR/mysql" ]; then + file_env 'MYSQL_ROOT_PASSWORD' + if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then + echo >&2 'error: database is uninitialized and password option is not specified ' + echo >&2 ' You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD' + exit 1 + fi + + mkdir -p "$DATADIR" + # ======================================== + # Stratos changes + chown -R mysql:mysql "$DATADIR" + # ======================================== + + echo 'Initializing database' + installArgs=( --datadir="$DATADIR" --rpm ) + if { mysql_install_db --help || :; } | grep -q -- '--auth-root-authentication-method'; then + # beginning in 10.4.3, install_db uses "socket" which only allows system user root to connect, switch back to "normal" to allow mysql root without a password + # see https://github.com/MariaDB/server/commit/b9f3f06857ac6f9105dc65caae19782f09b47fb3 + # (this flag doesn't exist in 10.0 and below) + installArgs+=( --auth-root-authentication-method=normal ) + fi + # "Other options are passed to mysqld." (so we pass all "mysqld" arguments directly here) + mysql_install_db "${installArgs[@]}" "${@:2}" + echo 'Database initialized' + + SOCKET="$(_get_config 'socket' "$@")" + "$@" --skip-networking --socket="${SOCKET}" & + pid="$!" + + mysql=( mysql --protocol=socket -uroot -hlocalhost --socket="${SOCKET}" ) + + for i in {30..0}; do + if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then + break + fi + echo 'MySQL init process in progress...' + sleep 1 + done + if [ "$i" = 0 ]; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then + # sed is for https://bugs.mysql.com/bug.php?id=20545 + mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql + fi + + if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then + export MYSQL_ROOT_PASSWORD="$(pwgen -1 32)" + echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD" + fi + + rootCreate= + # default root to listen for connections from anywhere + file_env 'MYSQL_ROOT_HOST' '%' + if [ ! -z "$MYSQL_ROOT_HOST" -a "$MYSQL_ROOT_HOST" != 'localhost' ]; then + # no, we don't care if read finds a terminating character in this heredoc + # https://unix.stackexchange.com/questions/265149/why-is-set-o-errexit-breaking-this-read-heredoc-expression/265151#265151 + read -r -d '' rootCreate <<-EOSQL || true + CREATE USER 'root'@'${MYSQL_ROOT_HOST}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; + GRANT ALL ON *.* TO 'root'@'${MYSQL_ROOT_HOST}' WITH GRANT OPTION ; + EOSQL + fi + + "${mysql[@]}" <<-EOSQL + -- What's done in this file shouldn't be replicated + -- or products like mysql-fabric won't work + SET @@SESSION.SQL_LOG_BIN=0; + + DELETE FROM mysql.user WHERE user NOT IN ('mysql.sys', 'mysqlxsys', 'root') OR host NOT IN ('localhost') ; + SET PASSWORD FOR 'root'@'localhost'=PASSWORD('${MYSQL_ROOT_PASSWORD}') ; + GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ; + ${rootCreate} + DROP DATABASE IF EXISTS test ; + FLUSH PRIVILEGES ; + EOSQL + + if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then + mysql+=( -p"${MYSQL_ROOT_PASSWORD}" ) + fi + + file_env 'MYSQL_DATABASE' + if [ "$MYSQL_DATABASE" ]; then + echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}" + mysql+=( "$MYSQL_DATABASE" ) + fi + + file_env 'MYSQL_USER' + file_env 'MYSQL_PASSWORD' + if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then + echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" | "${mysql[@]}" + + if [ "$MYSQL_DATABASE" ]; then + echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" | "${mysql[@]}" + fi + fi + + echo + for f in /docker-entrypoint-initdb.d/*; do + case "$f" in + *.sh) echo "$0: running $f"; . "$f" ;; + *.sql) echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;; + *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;; + *) echo "$0: ignoring $f" ;; + esac + echo + done + + if ! kill -s TERM "$pid" || ! wait "$pid"; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + echo + echo 'MySQL init process done. Ready for start up.' + echo + fi +fi -exec "$@" +exec "$@" \ No newline at end of file diff --git a/deploy/db/mariadb-ping.sh b/deploy/db/mariadb-ping.sh new file mode 100755 index 0000000000..331f6bb5a0 --- /dev/null +++ b/deploy/db/mariadb-ping.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mysqladmin --password="$MYSQL_ROOT_PASSWORD" --user=root ping \ No newline at end of file diff --git a/deploy/kubernetes/build.sh b/deploy/kubernetes/build.sh index 0fcf535154..e7c2c0ad79 100755 --- a/deploy/kubernetes/build.sh +++ b/deploy/kubernetes/build.sh @@ -26,8 +26,9 @@ ADD_OFFICIAL_TAG="false" TAG_LATEST="false" NO_PUSH="true" DOCKER_REG_DEFAULTS="true" +CHART_ONLY="false" -while getopts ":ho:r:t:Tclb:On" opt; do +while getopts ":ho:r:t:Tclb:Op" opt; do case $opt in h) echo @@ -64,6 +65,9 @@ while getopts ":ho:r:t:Tclb:On" opt; do p) NO_PUSH="false" ;; + c) + CHART_ONLY="true" + ;; \?) echo "Invalid option: -${OPTARG}" >&2 exit 1 @@ -175,22 +179,24 @@ cleanup updateTagForRelease -# Build all of the components that make up the Console +if [ "${CHART_ONLY}" == "false" ]; then + # Build all of the components that make up the Console -log "-- Build & publish the runtime container image for Jetstream (backend)" -patchAndPushImage stratos-jetstream deploy/Dockerfile.bk "${STRATOS_PATH}" prod-build + log "-- Build & publish the runtime container image for Jetstream (backend)" + patchAndPushImage stratos-jetstream deploy/Dockerfile.bk "${STRATOS_PATH}" prod-build -# Build the postflight container -log "-- Build & publish the runtime container image for the postflight job" -patchAndPushImage stratos-postflight-job deploy/Dockerfile.bk "${STRATOS_PATH}" postflight-job + # Build the postflight container + log "-- Build & publish the runtime container image for the postflight job" + patchAndPushImage stratos-postflight-job deploy/Dockerfile.bk "${STRATOS_PATH}" postflight-job -# Build and push an image based on the mariab db container -log "-- Building/publishing MariaDB" -patchAndPushImage stratos-mariadb Dockerfile.mariadb "${STRATOS_PATH}/deploy/db" + # Build and push an image based on the mariab db container + log "-- Building/publishing MariaDB" + patchAndPushImage stratos-mariadb Dockerfile.mariadb "${STRATOS_PATH}/deploy/db" -# Build and push an image based on the nginx container (Front-end) -log "-- Building/publishing the runtime container image for the Console web server (frontend)" -patchAndPushImage stratos-console deploy/Dockerfile.ui "${STRATOS_PATH}" prod-build + # Build and push an image based on the nginx container (Front-end) + log "-- Building/publishing the runtime container image for the Console web server (frontend)" + patchAndPushImage stratos-console deploy/Dockerfile.ui "${STRATOS_PATH}" prod-build +fi log "-- Building Helm Chart" diff --git a/deploy/kubernetes/console/templates/__helpers.tpl b/deploy/kubernetes/console/templates/__helpers.tpl index 5511d09bad..c24f888f74 100644 --- a/deploy/kubernetes/console/templates/__helpers.tpl +++ b/deploy/kubernetes/console/templates/__helpers.tpl @@ -106,6 +106,24 @@ tls.crt: {{ $cert.Cert | b64enc }} tls.key: {{ $cert.Key | b64enc }} {{- end -}} +{{/* +Generate self-signed certificate for ingress if needed +*/}} +{{- define "console.generateIngressCertificate" -}} +{{- $altNames := list (printf "%s" .Values.console.service.ingress.host) (printf "%s.%s" (include "console.certName" .) .Release.Namespace ) ( printf "%s.%s.svc" (include "console.certName" .) .Release.Namespace ) -}} +{{- $ca := genCA "stratos-ca" 365 -}} +{{- $cert := genSignedCert ( include "console.certName" . ) nil $altNames 365 $ca -}} +{{- if .Values.console.service.ingress.tls.crt }} + tls.crt: {{ .Values.console.service.ingress.tls.crt | b64enc | quote }} +{{- else }} + tls.crt: {{ $cert.Cert | b64enc | quote }} +{{- end -}} +{{- if .Values.console.service.ingress.tls.key }} + tls.key: {{ .Values.console.service.ingress.tls.key | b64enc | quote }} +{{- else }} + tls.key: {{ $cert.Key | b64enc | quote }} +{{- end -}} +{{- end -}} {{/* Ingress Host from .Values.console.service diff --git a/deploy/kubernetes/console/templates/deployment.yaml b/deploy/kubernetes/console/templates/deployment.yaml index 1f36fc9548..e419592e6b 100644 --- a/deploy/kubernetes/console/templates/deployment.yaml +++ b/deploy/kubernetes/console/templates/deployment.yaml @@ -1,5 +1,9 @@ --- +{{- if semverCompare ">=1.16" (printf "%s.%s" .Capabilities.KubeVersion.Major .Capabilities.KubeVersion.Minor)}} +apiVersion: apps/v1 +{{- else }} apiVersion: apps/v1beta1 +{{- end }} kind: StatefulSet metadata: name: stratos @@ -309,7 +313,11 @@ spec: name: {{ .Values.console.templatesConfigMapName }} {{- end }} --- +{{- if semverCompare ">=1.16" (printf "%s.%s" .Capabilities.KubeVersion.Major .Capabilities.KubeVersion.Minor)}} +apiVersion: apps/v1 +{{- else }} apiVersion: extensions/v1beta1 +{{- end }} kind: Deployment metadata: name: stratos-db @@ -353,7 +361,7 @@ spec: key: mariadb-password {{- end }} {{- else }} - - name: ALLOW_EMPTY_PASSWORD + - name: MYSQL_ALLOW_EMPTY_PASSWORD value: "yes" {{- end }} - name: MYSQL_USER @@ -366,15 +374,13 @@ spec: livenessProbe: exec: command: - - mysqladmin - - ping - initialDelaySeconds: 30 + - /dbping.sh + initialDelaySeconds: 10 timeoutSeconds: 5 readinessProbe: exec: command: - - mysqladmin - - ping + - /dbping.sh initialDelaySeconds: 5 timeoutSeconds: 1 resources: diff --git a/deploy/kubernetes/console/templates/ingress.yaml b/deploy/kubernetes/console/templates/ingress.yaml index 2578b52df5..f50266c71b 100644 --- a/deploy/kubernetes/console/templates/ingress.yaml +++ b/deploy/kubernetes/console/templates/ingress.yaml @@ -21,13 +21,16 @@ metadata: app.kubernetes.io/component: "console-ingress-tls" helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" data: - tls.crt: {{ .Values.console.service.ingress.tls.crt | default "" | b64enc | quote }} - tls.key: {{ .Values.console.service.ingress.tls.key | default "" | b64enc | quote }} +{{ template "console.generateIngressCertificate" . }} {{- end }} --- # Ingress for the Console UI service +{{- if semverCompare ">=1.16" (printf "%s.%s" .Capabilities.KubeVersion.Major .Capabilities.KubeVersion.Minor) }} +apiVersion: "networking.k8s.io/v1beta1" +{{- else }} apiVersion: "extensions/v1beta1" +{{- end }} kind: "Ingress" metadata: name: "{{ .Release.Name }}-ingress" @@ -35,6 +38,9 @@ metadata: {{- if hasKey .Values.console.service.ingress.annotations "kubernetes.io/ingress.class" | not -}} {{ $_ := set .Values.console.service.ingress.annotations "kubernetes.io/ingress.class" "nginx" }} {{- end }} + {{- if hasKey .Values.console.service.ingress.annotations "kubernetes.io/ingress.allow-http" | not -}} + {{ $_ := set .Values.console.service.ingress.annotations "kubernetes.io/ingress.allow-http" "false" }} + {{- end }} {{- if hasKey .Values.console.service.ingress.annotations "nginx.ingress.kubernetes.io/secure-backends" | not -}} {{ $_ := set .Values.console.service.ingress.annotations "nginx.ingress.kubernetes.io/secure-backends" "true" }} {{- end }} @@ -60,7 +66,7 @@ metadata: {{- end }} spec: tls: - - secretName: {{ default "{{ .Release.Name }}-ingress-tls" .Values.console.service.ingress.secretName | quote }} + - secretName: {{ default (print .Release.Name "-ingress-tls") .Values.console.service.ingress.secretName | quote }} hosts: - {{ template "ingress.host" . }} rules: diff --git a/deploy/kubernetes/console/tests/ingress_test.yaml b/deploy/kubernetes/console/tests/ingress_test.yaml index d275098c9b..50f214b502 100644 --- a/deploy/kubernetes/console/tests/ingress_test.yaml +++ b/deploy/kubernetes/console/tests/ingress_test.yaml @@ -47,10 +47,9 @@ tests: - equal: path: kind value: "Secret" - - equal: + - isNotEmpty: path: data.tls\.crt - value: "" - - equal: + - isNotEmpty: path: data.tls\.key value: "" - it: should create secret with specified values @@ -147,6 +146,7 @@ tests: - equal: path: metadata.annotations value: + kubernetes.io/ingress.allow-http: "false" kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/secure-backends: "true" nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" @@ -171,6 +171,7 @@ tests: - equal: path: metadata.annotations value: + kubernetes.io/ingress.allow-http: "false" kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/secure-backends: "true" nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" @@ -196,6 +197,7 @@ tests: path: metadata.annotations value: test-annotation: "test" + kubernetes.io/ingress.allow-http: "false" kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/secure-backends: "true" nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" diff --git a/deploy/kubernetes/console/tests/kube_version_test.yaml b/deploy/kubernetes/console/tests/kube_version_test.yaml new file mode 100644 index 0000000000..c803485484 --- /dev/null +++ b/deploy/kubernetes/console/tests/kube_version_test.yaml @@ -0,0 +1,31 @@ +suite: test stratos deployment with kube versions +templates: + - deployment.yaml +tests: + - it: should use newer API versions when kube >= 1.16 + capabilities: + kubeVersion: + major: 1 + minor: 16 + asserts: + - equal: + path: apiVersion + value: apps/v1 + - it: should use newer API versions when kube >= 2 + capabilities: + kubeVersion: + major: 2 + minor: 1 + asserts: + - equal: + path: apiVersion + value: apps/v1 + - it: should use older API versions when kube < 1.16 + capabilities: + kubeVersion: + major: 1 + minor: 14 + asserts: + - equal: + path: apiVersion + value: apps/v1beta1 diff --git a/deploy/stratos-base-images/Dockerfile.stratos-base.tmpl b/deploy/stratos-base-images/Dockerfile.stratos-base.tmpl index 701a2fbde5..b956a582b7 100644 --- a/deploy/stratos-base-images/Dockerfile.stratos-base.tmpl +++ b/deploy/stratos-base-images/Dockerfile.stratos-base.tmpl @@ -1,3 +1,7 @@ FROM {{BASE_IMAGE}} +{{#IS_SLE}} +RUN rm -f /usr/lib/zypp/plugins/services/container-suseconnect-zypp +{{/IS_SLE}} + WORKDIR /srv diff --git a/deploy/stratos-base-images/Dockerfile.stratos-bk-base.tmpl b/deploy/stratos-base-images/Dockerfile.stratos-bk-base.tmpl index 712ce98087..8416e9ea83 100644 --- a/deploy/stratos-base-images/Dockerfile.stratos-bk-base.tmpl +++ b/deploy/stratos-base-images/Dockerfile.stratos-bk-base.tmpl @@ -1,6 +1,7 @@ FROM {{BASE_IMAGE}} {{#IS_SLE}} +RUN rm -f /usr/lib/zypp/plugins/services/container-suseconnect-zypp RUN zypper addrepo -G -c '{{SMT_INTERNAL}}' smt_internal RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_UPDATE}}' smt_internal_update RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SDK}}' smt_internal_sdk @@ -8,22 +9,32 @@ RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SERVER}}' smt_internal_server RUN zypper ref {{/IS_SLE}} RUN zypper in -y ca-certificates && \ + # Can remove this when we remove the postflight job + zypper in -y mariadb-client curl && \ mkdir -p /srv && \ mkdir -p /root/.npm-global # Install latest git from devel/tools/scm repository {{^IS_SLE}} -RUN zypper ar http://download.opensuse.org/repositories/devel:/tools:/scm/openSUSE_Leap_42.3/devel:tools:scm.repo && \ - zypper --no-gpg-checks in -y git +RUN zypper ar http://download.opensuse.org/repositories/devel:/tools:/scm/openSUSE_Leap_15.1/devel:tools:scm.repo && \ + zypper --no-gpg-checks in -y git && \ + zypper rr devel_tools_scm && \ + zypper clean -a && \ + rm -f /var/log/zypper.log /var/log/zypp/history + {{/IS_SLE}} {{#IS_SLE}} -RUN zypper ar http://download.opensuse.org/repositories/devel:/tools:/scm/SLE_12_SP3/devel:tools:scm.repo && \ - zypper --no-gpg-checks in -y git +RUN zypper ar http://download.opensuse.org/repositories/devel:/tools:/scm/SLE_15/devel:tools:scm.repo && \ + zypper --no-gpg-checks in -y git && \ + zypper rr devel_tools_scm && \ + zypper clean -a && \ + rm -f /var/log/zypper.log /var/log/zypp/history + {{/IS_SLE}} {{#IS_SLE}} -RUN zypper rr smt_internal -RUN zypper rr smt_internal_update -RUN zypper rr smt_internal_sdk -RUN zypper rr smt_internal_server +RUN zypper rr smt_internal +RUN zypper rr smt_internal_update +RUN zypper rr smt_internal_sdk +RUN zypper rr smt_internal_server {{/IS_SLE}} WORKDIR /srv diff --git a/deploy/stratos-base-images/Dockerfile.stratos-bk-build-base.tmpl b/deploy/stratos-base-images/Dockerfile.stratos-bk-build-base.tmpl index fa9e497e52..04137d442f 100644 --- a/deploy/stratos-base-images/Dockerfile.stratos-bk-build-base.tmpl +++ b/deploy/stratos-base-images/Dockerfile.stratos-bk-build-base.tmpl @@ -6,8 +6,11 @@ RUN useradd -ms /bin/bash stratos && \ chgrp -R users /home/stratos RUN cd / && wget https://nodejs.org/dist/v12.13.0/node-v12.13.0-linux-x64.tar.xz && \ - tar -xf node-v12.13.0-linux-x64.tar.xz + tar -xf node-v12.13.0-linux-x64.tar.xz && \ + rm node-v12.13.0-linux-x64.tar.xz ENV USER=stratos ENV PATH=$PATH:/node-v12.13.0-linux-x64/bin USER stratos WORKDIR /home/stratos + + diff --git a/deploy/stratos-base-images/Dockerfile.stratos-go-build-base.tmpl b/deploy/stratos-base-images/Dockerfile.stratos-go-build-base.tmpl index 53b1ff5821..871c53c857 100644 --- a/deploy/stratos-base-images/Dockerfile.stratos-go-build-base.tmpl +++ b/deploy/stratos-base-images/Dockerfile.stratos-go-build-base.tmpl @@ -1,6 +1,7 @@ FROM {{BASE_IMAGE}} {{#IS_SLE}} +RUN rm -f /usr/lib/zypp/plugins/services/container-suseconnect-zypp RUN zypper addrepo -G -c '{{SMT_INTERNAL}}' smt_internal RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_UPDATE}}' smt_internal_update RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SDK}}' smt_internal_sdk @@ -9,10 +10,14 @@ RUN zypper ref {{/IS_SLE}} RUN zypper -n ref && \ -zypper -n up && \ -zypper in -y which tar git gcc curl wget + zypper -n up && \ + zypper in -y which tar git gcc curl wget gzip xz && \ + zypper clean -a && \ + rm -f /var/log/zypper.log /var/log/zypp/history + RUN wget https://storage.googleapis.com/golang/go1.13.4.linux-amd64.tar.gz && \ tar -xzf go1.13.4.linux-amd64.tar.gz -C /usr/local/ && \ + rm go1.13.4.linux-amd64.tar.gz && \ mkdir -p /home/stratos/go/bin && \ mkdir -p /home/stratos/go/src diff --git a/deploy/stratos-base-images/Dockerfile.stratos-mariadb-base.tmpl b/deploy/stratos-base-images/Dockerfile.stratos-mariadb-base.tmpl index 61d6d758df..7c4cce74ff 100644 --- a/deploy/stratos-base-images/Dockerfile.stratos-mariadb-base.tmpl +++ b/deploy/stratos-base-images/Dockerfile.stratos-mariadb-base.tmpl @@ -1,7 +1,11 @@ FROM {{BASE_IMAGE}} {{#IS_SLE}} +RUN rm -f /usr/lib/zypp/plugins/services/container-suseconnect-zypp RUN zypper addrepo -G -c '{{SMT_INTERNAL}}' smt_internal +RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_UPDATE}}' smt_internal_update +RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SDK}}' smt_internal_sdk +RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SERVER}}' smt_internal_server RUN zypper ref {{/IS_SLE}} @@ -9,8 +13,13 @@ RUN zypper ref ENV MYSQL_ROOT_PASSWORD mysecretpassword # Add repo for the latest mariadb -RUN zypper in -y mariadb net-tools \ - && zypper clean --all +RUN zypper in -y mariadb net-tools mariadb-tools timezone wget awk grep && \ + zypper clean -a && \ + rm -f /var/log/zypper.log /var/log/zypp/history + +ARG GOSU_VERSION=1.11 +RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-amd64" \ + && chmod +x /usr/local/bin/gosu # Config for mariadb RUN rm -rf /var/lib/mysql \ @@ -20,7 +29,10 @@ RUN rm -rf /var/lib/mysql \ && chown -R mysql:mysql /var/log/mysql {{#IS_SLE}} -RUN zypper rr smt_internal +RUN zypper rr smt_internal +RUN zypper rr smt_internal_update +RUN zypper rr smt_internal_sdk +RUN zypper rr smt_internal_server {{/IS_SLE}} VOLUME ["/var/lib/mysql"] \ No newline at end of file diff --git a/deploy/stratos-base-images/Dockerfile.stratos-nginx-base.tmpl b/deploy/stratos-base-images/Dockerfile.stratos-nginx-base.tmpl index 021c2d9ff3..d42d542d5b 100644 --- a/deploy/stratos-base-images/Dockerfile.stratos-nginx-base.tmpl +++ b/deploy/stratos-base-images/Dockerfile.stratos-nginx-base.tmpl @@ -1,12 +1,23 @@ FROM {{BASE_IMAGE}} {{#IS_SLE}} -RUN zypper addrepo -G -t yum -c 'http://nginx.org/packages/sles/12' nginx +RUN rm -f /usr/lib/zypp/plugins/services/container-suseconnect-zypp +RUN zypper addrepo -G -t yum -c 'http://nginx.org/packages/sles/15' nginx +RUN zypper addrepo -G -c '{{SMT_INTERNAL}}' smt_internal +RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_UPDATE}}' smt_internal_update +RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SDK}}' smt_internal_sdk RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SERVER}}' smt_internal_server {{/IS_SLE}} + RUN zypper -n ref && \ zypper -n up && \ - zypper in -y nginx apache2-utils + zypper in -y nginx apache2-utils && \ + zypper clean -a && \ + rm -f /var/log/zypper.log /var/log/zypp/history {{#IS_SLE}} +RUN zypper rr nginx +RUN zypper rr smt_internal +RUN zypper rr smt_internal_update +RUN zypper rr smt_internal_sdk RUN zypper rr smt_internal_server -{{/IS_SLE}} \ No newline at end of file +{{/IS_SLE}} \ No newline at end of file diff --git a/deploy/stratos-base-images/Dockerfile.stratos-ruby-build-base.tmpl b/deploy/stratos-base-images/Dockerfile.stratos-ruby-build-base.tmpl index 2a23f226a8..fd1728474c 100644 --- a/deploy/stratos-base-images/Dockerfile.stratos-ruby-build-base.tmpl +++ b/deploy/stratos-base-images/Dockerfile.stratos-ruby-build-base.tmpl @@ -1,6 +1,7 @@ FROM {{BASE_IMAGE}} {{#IS_SLE}} +RUN rm -f /usr/lib/zypp/plugins/services/container-suseconnect-zypp RUN zypper addrepo -G -c '{{SMT_INTERNAL}}' smt_internal RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_UPDATE}}' smt_internal_update RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SDK}}' smt_internal_sdk @@ -9,18 +10,20 @@ RUN zypper ref {{/IS_SLE}} RUN zypper -n ref && \ -zypper -n up && \ -zypper in -y which tar curl wget - -{{#IS_SLE}} -RUN zypper rr smt_internal -RUN zypper rr smt_internal_update -RUN zypper rr smt_internal_sdk -RUN zypper rr smt_internal_server -{{/IS_SLE}} + zypper -n up && \ + zypper in -y which tar curl wget gzip && \ + zypper clean -a && \ + rm -f /var/log/zypper.log /var/log/zypp/history WORKDIR / USER root ADD install-ruby.sh /install-ruby.sh RUN /install-ruby.sh + +{{#IS_SLE}} +RUN zypper rr smt_internal +RUN zypper rr smt_internal_update +RUN zypper rr smt_internal_sdk +RUN zypper rr smt_internal_server +{{/IS_SLE}} diff --git a/deploy/stratos-base-images/Dockerfile.stratos-ui-build-base.tmpl b/deploy/stratos-base-images/Dockerfile.stratos-ui-build-base.tmpl index a143dfcac5..55cdaf6c5b 100644 --- a/deploy/stratos-base-images/Dockerfile.stratos-ui-build-base.tmpl +++ b/deploy/stratos-base-images/Dockerfile.stratos-ui-build-base.tmpl @@ -1,6 +1,7 @@ FROM {{BASE_IMAGE}} {{#IS_SLE}} +RUN rm -f /usr/lib/zypp/plugins/services/container-suseconnect-zypp RUN zypper addrepo -G -c '{{SMT_INTERNAL}}' smt_internal RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_UPDATE}}' smt_internal_update RUN zypper addrepo -t rpm-md -G -c '{{SMT_INTERNAL_SDK}}' smt_internal_sdk @@ -9,8 +10,11 @@ RUN zypper ref {{/IS_SLE}} RUN zypper -n ref && \ -zypper -n up && \ -zypper in -y wget tar git + zypper -n up && \ + zypper in -y wget tar git xz && \ + zypper clean -a && \ + rm -f /var/log/zypper.log /var/log/zypp/history + RUN cd / && wget https://nodejs.org/dist/v12.13.0/node-v12.13.0-linux-x64.tar.xz && \ tar -xf node-v12.13.0-linux-x64.tar.xz ENV USER=stratos @@ -24,11 +28,11 @@ RUN useradd -ms /bin/bash stratos && \ chgrp users /usr/dist {{#IS_SLE}} -RUN zypper rr smt_internal -RUN zypper rr smt_internal_update -RUN zypper rr smt_internal_sdk -RUN zypper rr smt_internal_server +RUN zypper rr smt_internal +RUN zypper rr smt_internal_update +RUN zypper rr smt_internal_sdk +RUN zypper rr smt_internal_server {{/IS_SLE}} -USER stratos +USER stratos WORKDIR /home/stratos diff --git a/deploy/stratos-base-images/build-base-images.sh b/deploy/stratos-base-images/build-base-images.sh index e85a2f6061..2e58c97a4c 100755 --- a/deploy/stratos-base-images/build-base-images.sh +++ b/deploy/stratos-base-images/build-base-images.sh @@ -7,11 +7,13 @@ YELLOW="\033[93m" RESET="\033[0m" BOLD="\033[1m" -BASE_IMAGE=opensuse:42.3 +BASE_IMAGE=opensuse/leap:15.1 REGISTRY=docker.io ORGANIZATION=splatform -TAG=opensuse +TAG=leap15_1 PROG=$(basename ${BASH_SOURCE[0]}) +SQUASH_ARGS="--squash" +NO_SQUASH="stratos-base" function usage { echo "usage: $PROG [-b BASE] [-r REGISTRY] [-o ORGANIZATION] [-t TAG] [-p] [h]" @@ -115,8 +117,15 @@ build_and_push_image() { printf "${CYAN}Building image ${YELLOW}${image_name}${CYAN} with docker file ${YELLOW}${docker_file}${RESET}\n" printf "${CYAN}========= >>>>${RESET}\n" echo "" + + # We can't squash base image as its the same as the base (just re-tagged) + ARG="" + if [ "${image_name}" != "$NO_SQUASH" ]; then + ARG="${SQUASH_ARGS}" + fi set -x - docker build . -f $docker_file -t ${REGISTRY}/${ORGANIZATION}/${image_name}:${TAG} + # Always remove intermediate containers + docker build --force-rm ${ARG} . -f $docker_file -t ${REGISTRY}/${ORGANIZATION}/${image_name}:${TAG} if [ ! -z ${PUSH_IMAGES} ]; then docker push ${REGISTRY}/${ORGANIZATION}/${image_name}:${TAG} fi diff --git a/deploy/stratos-base-images/install-ruby.sh b/deploy/stratos-base-images/install-ruby.sh index fc8a4bd622..237851a43a 100755 --- a/deploy/stratos-base-images/install-ruby.sh +++ b/deploy/stratos-base-images/install-ruby.sh @@ -15,20 +15,11 @@ set -e if [ "$IS_SLES" == "false" ]; then zypper in -y curl jq make gcc-c++ - zypper in -y libopenssl-devel readline-devel + zypper in -y libopenssl-devel readline-devel + zypper in -y ruby-devel fi -# Build from source -mkdir -p /tmp/ruby -cd /tmp/ruby -wget https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.8.tar.gz -gunzip ./ruby-2.3.8.tar.gz -tar -xvf ./ruby-2.3.8.tar -cd ruby-2.3.8 - -./configure - -make -make install +# OpenSUSE Leap 15.1 will install ruby 2.5 +zypper in -y ruby ruby --version diff --git a/docs/customizing.md b/docs/customizing.md index 2171212e3c..c167a77516 100644 --- a/docs/customizing.md +++ b/docs/customizing.md @@ -50,7 +50,7 @@ In this file you can set any or all of the following variables: |---|---| |$stratos-theme|The main theme to use for Stratos| |$stratos-nav-theme|Theme to use for the side navigation panel| -$stratos-status-theme|Theme to use for displaying status in Stratos| +|$stratos-status-theme|Theme to use for displaying status in Stratos| Note that you do not have to specify all of these - defaults will be used if they are not set. @@ -72,6 +72,24 @@ $suse-app-theme: mat-light-theme($suse-app-primary, $suse-app-primary, $suse-the $stratos-theme: $suse-app-theme; ``` +#### Creating or disabling the Dark theme + +You can also change the Dark theme, if you wish, by defining the following variables: + +|Variable|Purpose| +|---|---| +|$stratos-dark-theme|The dark theme to use for Stratos| +|$stratos-dark-nav-theme|Dark theme to use for the side navigation panel| +|$stratos-dark-status-theme|Dark theme to use for displaying status in Stratos| + +Note that minimally you must supply `stratos-dark-theme` to create a dark theme. + +By default a dark theme is assumed to be available and the default will be used if not overridden. You can disable dark theme support in the UI by setting the following variable in your `custom.scss`: + +``` +$stratos-dark-theme-supported: false; +``` + ### Changing Styles We don't generally recommend modifying styles, since from version to version of Stratos, we may change the styles used slightly which can mean any modifications you made will need updating. Should you wish to do so, you can modify these in the same `custom.scss` file that is used for theming. diff --git a/examples/custom-src/frontend/sass/custom.scss b/examples/custom-src/frontend/sass/custom.scss index 308b1ef362..e3eb84c38a 100644 --- a/examples/custom-src/frontend/sass/custom.scss +++ b/examples/custom-src/frontend/sass/custom.scss @@ -2,11 +2,17 @@ // ACME Primary Material Design pallette // From http://mcg.mbitson.com/#!?mcgpalette0=%233f51b5 -$acme-primary: (50: #e0f7f0, 100: #b3ecd9, 200: #80e0c0, 300: #4dd3a7, 400: #26c994, 500: #00c081, 600: #00ba79, 700: #00b26e, 800: #00aa64, 900: #009c51, A100: #c7ffe0, A200: #94ffc4, A400: #61ffa8, A700: #47ff9a, contrast: (50: #000, 100: #000, 200: #000, 300: #000, 400: #000, 500: #fff, 600: #fff, 700: #fff, 800: #fff, 900: #fff, A100: #000, A200: #000, A400: #000, A700: #000)); +$acme-primary: (50: #e8eaf6, 100: #c5cbe9, 200: #9fa8da, 300: #7985cb, 400: #5c6bc0, 500: #3f51b5, 600: #394aae, 700: #3140a5, 800: #29379d, 900: #1b278d, A100: #c6cbff, A200: #939dff, A400: #606eff, A700: #4757ff, contrast: ( 50: #000000, 100: #000000, 200: #000000, 300: #000000, 400: #ffffff, 500: #ffffff, 600: #ffffff, 700: #ffffff, 800: #ffffff, 900: #ffffff, A100: #000000, A200: #000000, A400: #ffffff, A700: #ffffff, )); $mat-red: ( 50: #ffebee, 100: #ffcdd2, 200: #ef9a9a, 300: #e57373, 400: #ef5350, 500: #f44336, 600: #e53935, 700: #d32f2f, 800: #c62828, 900: #b71c1c, A100: #ff8a80, A200: #ff5252, A400: #ff1744, A700: #d50000, contrast: ( 50: $black-87-opacity, 100: $black-87-opacity, 200: $black-87-opacity, 300: $black-87-opacity, 400: $black-87-opacity, 500: white, 600: white, 700: white, 800: $white-87-opacity, 900: $white-87-opacity, A100: $black-87-opacity, A200: white, A400: white, A700: white, )); +// Common $acme-theme-primary: mat-palette($acme-primary); $acme-theme-warn: mat-palette($mat-red); + +// Dark Theme +$stratos-dark-theme: mat-dark-theme($acme-theme-primary, $acme-theme-primary, $acme-theme-warn); + +// Default Theme $stratos-theme: mat-light-theme($acme-theme-primary, $acme-theme-primary, $acme-theme-warn); @import 'custom/acme'; diff --git a/examples/custom-src/frontend/sass/custom/acme.scss b/examples/custom-src/frontend/sass/custom/acme.scss index d153c4bc36..75794ebb6e 100644 --- a/examples/custom-src/frontend/sass/custom/acme.scss +++ b/examples/custom-src/frontend/sass/custom/acme.scss @@ -11,7 +11,7 @@ $acme-blue: #073155; $acme-side-nav: $acme-secondary; $acme-side-nav-active: #003358; -.stratos { +body.stratos { app-page-subheader { .page-subheader { background-color: #fff; diff --git a/manifest.yml b/manifest.yml index 9d229edd2b..9d22373b44 100644 --- a/manifest.yml +++ b/manifest.yml @@ -4,7 +4,7 @@ applications: disk_quota: 1024M host: console timeout: 180 - buildpack: https://github.com/cloudfoundry-incubator/stratos-buildpack#v2.4 + buildpack: https://github.com/cloudfoundry-incubator/stratos-buildpack#v3 health-check-type: port # env: # Override CF API endpoint URL inferred from VCAP_APPLICATION env @@ -13,6 +13,8 @@ applications: # CF_API_FORCE_SECURE: true # Turn on backend debugging # LOG_LEVEL: debug +# Turn on staging debugging in stratos-buildpack +# STRATOS_BP_DEBUG: true #Remove line to turn off debugging # User provided services can also be used to set environment properties: # env: diff --git a/package-lock.json b/package-lock.json index ce5b2d62f2..a548d0cc34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "stratos", - "version": "2.6.0", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f642afd00f..7856c8e7ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stratos", - "version": "2.6.0", + "version": "3.0.0", "description": "Stratos Console", "main": "index.js", "scripts": { diff --git a/src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/app-autoscaler-metric-chart-card.component.scss b/src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/app-autoscaler-metric-chart-card.component.scss index 002bddcdbd..de27bc5c5e 100644 --- a/src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/app-autoscaler-metric-chart-card.component.scss +++ b/src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/app-autoscaler-metric-chart-card.component.scss @@ -1,6 +1,5 @@ mat-card-header { .autoscaler-metric-subtitle { - color: rgba(0, 0, 0, .54); font-size: .9em; margin-top: .3em; } diff --git a/src/frontend/packages/cloud-foundry/src/actions/deploy-applications.actions.ts b/src/frontend/packages/cloud-foundry/src/actions/deploy-applications.actions.ts index 7264dee6a7..36baef9f80 100644 --- a/src/frontend/packages/cloud-foundry/src/actions/deploy-applications.actions.ts +++ b/src/frontend/packages/cloud-foundry/src/actions/deploy-applications.actions.ts @@ -9,7 +9,7 @@ import { GitAppDetails, OverrideAppDetails, SourceType } from '../store/types/de import { GitBranch, GitCommit } from '../store/types/git.types'; export const SET_APP_SOURCE_DETAILS = '[Deploy App] Application Source'; -export const CHECK_PROJECT_EXISTS = '[Deploy App] Check Projet exists'; +export const CHECK_PROJECT_EXISTS = '[Deploy App] Check Project exists'; export const PROJECT_DOESNT_EXIST = '[Deploy App] Project Doesn\'t exist'; export const PROJECT_FETCH_FAILED = '[Deploy App] Project Fetch Failed'; export const PROJECT_EXISTS = '[Deploy App] Project exists'; diff --git a/src/frontend/packages/cloud-foundry/src/actions/organization.actions.ts b/src/frontend/packages/cloud-foundry/src/actions/organization.actions.ts index 61bbeef953..58077aa52f 100644 --- a/src/frontend/packages/cloud-foundry/src/actions/organization.actions.ts +++ b/src/frontend/packages/cloud-foundry/src/actions/organization.actions.ts @@ -18,7 +18,7 @@ import { } from '../entity-relations/entity-relations.types'; import { CFStartAction } from './cf-action.types'; import { createDefaultUserRelations } from './user.actions.helpers'; -import { HttpRequest } from '@angular/common/http'; +import { HttpRequest, HttpHeaders } from '@angular/common/http'; export const GET_ORGANIZATION = '[Organization] Get one'; export const GET_ORGANIZATION_SUCCESS = '[Organization] Get one success'; @@ -172,10 +172,10 @@ export class DeleteOrganization extends CFStartAction implements ICFAction { 'DELETE', `organizations/${guid}`, { - params: { + params: new HttpHeaders({ recursive: 'true', async: 'false' - } + }) } ); } diff --git a/src/frontend/packages/cloud-foundry/src/actions/relation.actions.ts b/src/frontend/packages/cloud-foundry/src/actions/relation.actions.ts index ade4c360b2..bbf82324c9 100644 --- a/src/frontend/packages/cloud-foundry/src/actions/relation.actions.ts +++ b/src/frontend/packages/cloud-foundry/src/actions/relation.actions.ts @@ -7,7 +7,7 @@ import { PaginatedAction } from '../../../store/src/types/pagination.types'; import { RequestEntityLocation, RequestActionEntity } from '../../../store/src/types/request.types'; import { CFStartAction } from './cf-action.types'; import { EntityTreeRelation } from '../entity-relations/entity-relation-tree'; -import { HttpRequest } from '@angular/common/http'; +import { HttpRequest, HttpParams } from '@angular/common/http'; const relationActionId = 'FetchRelationAction'; @@ -27,7 +27,7 @@ export abstract class FetchRelationAction extends CFStartAction implements Entit 'GET', url.startsWith('/v2/') ? url.substring(4, url.length) : url, { - params: {} + params: new HttpParams() } ); this.parentEntityConfig = parent.entity; diff --git a/src/frontend/packages/cloud-foundry/src/actions/service-instances.actions.ts b/src/frontend/packages/cloud-foundry/src/actions/service-instances.actions.ts index 44654b6f0f..9f0a3f6a55 100644 --- a/src/frontend/packages/cloud-foundry/src/actions/service-instances.actions.ts +++ b/src/frontend/packages/cloud-foundry/src/actions/service-instances.actions.ts @@ -84,11 +84,13 @@ export class DeleteServiceInstance extends CFStartAction implements ICFAction { 'DELETE', `service_instances/${guid}`, { - params: { - accepts_incomplete: 'true', - async: 'false', - recursive: 'true' - } + params: new HttpParams({ + fromObject: { + accepts_incomplete: 'true', + async: 'false', + recursive: 'true' + } + }) } ); } diff --git a/src/frontend/packages/cloud-foundry/src/actions/space.actions.ts b/src/frontend/packages/cloud-foundry/src/actions/space.actions.ts index 4d3387dd25..53c27a466b 100644 --- a/src/frontend/packages/cloud-foundry/src/actions/space.actions.ts +++ b/src/frontend/packages/cloud-foundry/src/actions/space.actions.ts @@ -24,7 +24,7 @@ import { CFStartAction } from './cf-action.types'; import { GetAllOrgUsers } from './organization.actions'; import { RouteEvents } from './route.actions'; import { getServiceInstanceRelations } from './service-instances.actions'; -import { HttpRequest } from '@angular/common/http'; +import { HttpRequest, HttpParams } from '@angular/common/http'; export const GET_SPACES = '[Space] Get all'; export const GET_SPACES_SUCCESS = '[Space] Get all success'; @@ -75,7 +75,7 @@ export class GetAllSpaces extends CFStartAction implements PaginatedAction, Enti super(); this.options = new HttpRequest( 'GET', - 'spsace' + 'space' ); } actions = [GET_SPACES, GET_SPACES_SUCCESS, GET_SPACES_FAILED]; @@ -177,10 +177,12 @@ export class DeleteSpace extends BaseSpaceAction { 'DELETE', `spaces/${guid}`, { - params: { - recursive: 'true', - async: 'false' - } + params: new HttpParams({ + fromObject: { + recursive: 'true', + async: 'false' + } + }) } ); } diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.theme.scss b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.theme.scss index 0ce40249ca..d5f3293582 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.theme.scss +++ b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.theme.scss @@ -1,7 +1,17 @@ @import '~@angular/material/theming'; @mixin app-deploy-app-theme($theme, $app-theme) { + $is-dark: map-get($theme, is-dark); + $ansi-colors: map-get($app-theme, ansi-colors); + $background-color: map-get(map-get($ansi-colors, 'white'), intense); + + @if $is-dark == true { + $background-colors: map-get($theme, background); + $foreground-colors: map-get($theme, foreground); + $background-color: lighten(mat-color($background-colors, background), 5%); + } + .deploy-app { - background-color: map-get(map-get($ansi-colors, 'white'), intense); + background-color: $background-color; } } diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell.service.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell.service.ts index 4f737b86e6..c9a8089fbe 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell.service.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell.service.ts @@ -1,15 +1,19 @@ import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; import { combineLatest, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { EntityServiceFactory } from '../../../../../../../core/src/core/entity-service-factory.service'; +import { CfCellHelper } from '../../../../../../../core/src/features/cloud-foundry/cf-cell.helpers'; import { MetricsConfig } from '../../../../../../../core/src/shared/components/metrics-chart/metrics-chart.component'; import { MetricsLineChartConfig } from '../../../../../../../core/src/shared/components/metrics-chart/metrics-chart.types'; import { MetricsChartHelpers, } from '../../../../../../../core/src/shared/components/metrics-chart/metrics.component.helpers'; +import { PaginationMonitorFactory } from '../../../../../../../core/src/shared/monitors/pagination-monitor.factory'; import { MetricQueryType } from '../../../../../../../core/src/shared/services/metrics-range-selector.types'; import { MetricQueryConfig } from '../../../../../../../store/src/actions/metrics.actions'; +import { AppState } from '../../../../../../../store/src/app-state'; import { IMetricMatrixResult, IMetrics, IMetricVectorResult } from '../../../../../../../store/src/types/base-metric.types'; import { IMetricCell } from '../../../../../../../store/src/types/metric.types'; import { FetchCFCellMetricsAction } from '../../../../../actions/cf-metrics.actions'; @@ -17,7 +21,14 @@ import { ActiveRouteCfCell } from '../../../cf-page.types'; export const enum CellMetrics { - HEALTHY = 'firehose_value_metric_rep_unhealthy_cell', + /** + * Deprecated since Diego v2.31.0. See https://github.com/bosh-prometheus/prometheus-boshrelease/issues/333 + */ + HEALTHY_DEP = 'firehose_value_metric_rep_unhealthy_cell', + /** + * Available from Diego v2.31.0. See https://github.com/bosh-prometheus/prometheus-boshrelease/issues/333 + */ + HEALTHY = 'firehose_value_metric_rep_garden_health_check_failed', REMAINING_CONTAINERS = 'firehose_value_metric_rep_capacity_remaining_containers', REMAINING_DISK = 'firehose_value_metric_rep_capacity_remaining_disk', REMAINING_MEMORY = 'firehose_value_metric_rep_capacity_remaining_memory', @@ -27,6 +38,10 @@ export const enum CellMetrics { CPUS = 'firehose_value_metric_rep_num_cpus' } + +/** + * Designed to be used once drilled down to a cell (see ActiveRouteCfCell) + */ @Injectable() export class CloudFoundryCellService { @@ -52,12 +67,13 @@ export class CloudFoundryCellService { constructor( activeRouteCfCell: ActiveRouteCfCell, - private entityServiceFactory: EntityServiceFactory) { + private entityServiceFactory: EntityServiceFactory, + store: Store, + paginationMonitorFactory: PaginationMonitorFactory) { this.cellId = activeRouteCfCell.cellId; this.cfGuid = activeRouteCfCell.cfGuid; - this.healthy$ = this.generate(CellMetrics.HEALTHY); this.remainingContainers$ = this.generate(CellMetrics.REMAINING_CONTAINERS); this.totalContainers$ = this.generate(CellMetrics.TOTAL_CONTAINERS); this.remainingDisk$ = this.generate(CellMetrics.REMAINING_DISK); @@ -70,8 +86,19 @@ export class CloudFoundryCellService { this.usageDisk$ = this.generateUsage(this.remainingDisk$, this.totalDisk$); this.usageMemory$ = this.generateUsage(this.remainingMemory$, this.totalMemory$); - this.cellMetric$ = this.generate(CellMetrics.HEALTHY, true); - + const cellHelper = new CfCellHelper(store, paginationMonitorFactory); + const action$ = cellHelper.createCellMetricAction(this.cfGuid); + this.cellMetric$ = action$.pipe( + switchMap(action => { + this.healthyMetricId = action.guid; + return this.generate(action.query.metric as CellMetrics, true); + }) + ); + this.healthy$ = action$.pipe( + switchMap(action => { + return this.generate(action.query.metric as CellMetrics, false); + }) + ); } public buildMetricConfig( @@ -99,17 +126,14 @@ export class CloudFoundryCellService { return lineChartConfig; } - private generate(metric: CellMetrics, isMetric = false): Observable { - const action = new FetchCFCellMetricsAction( + private generate(metric: CellMetrics, isMetric = false, customAction?: FetchCFCellMetricsAction): Observable { + const action = customAction || new FetchCFCellMetricsAction( this.cfGuid, this.cellId, new MetricQueryConfig(metric + `{bosh_job_id="${this.cellId}"}`, {}), MetricQueryType.QUERY, false ); - if (metric === CellMetrics.HEALTHY) { - this.healthyMetricId = action.guid; - } return this.entityServiceFactory.create>>( action.guid, action, diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cells.component.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cells.component.ts index dd2e028c36..dd9fb19aa9 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cells.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cells.component.ts @@ -1,7 +1,11 @@ import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { CfCellHelper } from '../../../../../../core/src/features/cloud-foundry/cf-cell.helpers'; import { ListConfig } from '../../../../../../core/src/shared/components/list/list.component.types'; +import { PaginationMonitorFactory } from '../../../../../../core/src/shared/monitors/pagination-monitor.factory'; +import { AppState } from '../../../../../../store/src/app-state'; import { CfCellsListConfigService, } from '../../../../shared/components/list/list-types/cf-cells/cf-cells-list-config.service'; @@ -23,7 +27,12 @@ import { CloudFoundryEndpointService } from '../../services/cloud-foundry-endpoi export class CloudFoundryCellsComponent { hasCellMetrics$: Observable; - constructor(cfEndpointService: CloudFoundryEndpointService) { - this.hasCellMetrics$ = cfEndpointService.hasCellMetrics(cfEndpointService.cfGuid); + constructor( + cfEndpointService: CloudFoundryEndpointService, + store: Store, + paginationMonitorFactory: PaginationMonitorFactory + ) { + const cellHelper = new CfCellHelper(store, paginationMonitorFactory); + this.hasCellMetrics$ = cellHelper.hasCellMetrics(cfEndpointService.cfGuid); } } diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-instance/cf-app-instances-config.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-instance/cf-app-instances-config.service.ts index 4e0b40a398..31d25e1a8d 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-instance/cf-app-instances-config.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-instance/cf-app-instances-config.service.ts @@ -2,17 +2,14 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { combineLatest, filter, map, switchMap } from 'rxjs/operators'; +import { combineLatest, map, switchMap } from 'rxjs/operators'; import { DeleteApplicationInstance } from '../../../../../../../cloud-foundry/src/actions/application.actions'; import { FetchApplicationMetricsAction } from '../../../../../../../cloud-foundry/src/actions/cf-metrics.actions'; import { CFAppState } from '../../../../../../../cloud-foundry/src/cf-app-state'; -import { ApplicationService } from '../../../../../../../cloud-foundry/src/features/applications/application.service'; -import { - CloudFoundryEndpointService, -} from '../../../../../../../cloud-foundry/src/features/cloud-foundry/services/cloud-foundry-endpoint.service'; import { EntityServiceFactory } from '../../../../../../../core/src/core/entity-service-factory.service'; import { UtilsService } from '../../../../../../../core/src/core/utils.service'; +import { CfCellHelper } from '../../../../../../../core/src/features/cloud-foundry/cf-cell.helpers'; import { ConfirmationDialogConfig } from '../../../../../../../core/src/shared/components/confirmation-dialog.config'; import { ConfirmationDialogService } from '../../../../../../../core/src/shared/components/confirmation-dialog.service'; import { @@ -24,10 +21,12 @@ import { IListConfig, ListViewTypes, } from '../../../../../../../core/src/shared/components/list/list.component.types'; +import { PaginationMonitorFactory } from '../../../../../../../core/src/shared/monitors/pagination-monitor.factory'; import { MetricQueryType } from '../../../../../../../core/src/shared/services/metrics-range-selector.types'; import { MetricQueryConfig } from '../../../../../../../store/src/actions/metrics.actions'; import { IMetricMatrixResult, IMetrics } from '../../../../../../../store/src/types/base-metric.types'; import { IMetricApplication } from '../../../../../../../store/src/types/metric.types'; +import { ApplicationService } from '../../../../../features/applications/application.service'; import { ListAppInstance } from './app-instance-types'; import { CfAppInstancesDataSource } from './cf-app-instances-data-source'; import { TableCellCfCellComponent } from './table-cell-cf-cell/table-cell-cf-cell.component'; @@ -199,10 +198,11 @@ export class CfAppInstancesConfigService implements IListConfig private router: Router, private confirmDialog: ConfirmationDialogService, entityServiceFactory: EntityServiceFactory, - cfEndpointService: CloudFoundryEndpointService + paginationMonitorFactory: PaginationMonitorFactory ) { + const cellHelper = new CfCellHelper(store, paginationMonitorFactory); - this.initialised$ = cfEndpointService.hasCellMetrics(appService.cfGuid).pipe( + this.initialised$ = cellHelper.hasCellMetrics(appService.cfGuid).pipe( map(hasMetrics => { if (hasMetrics) { this.columns.splice(1, 0, this.cfCellColumn); diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/base-cf/base-cf-list-config.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/base-cf/base-cf-list-config.ts index d92a018ff8..dbd2be20a1 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/base-cf/base-cf-list-config.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/base-cf/base-cf-list-config.ts @@ -1,3 +1,5 @@ +import { of } from 'rxjs'; + import { IListDataSource, } from '../../../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source-types'; @@ -19,4 +21,5 @@ export class BaseCfListConfig implements IListConfig { getMultiActions = () => []; getSingleActions = () => []; getMultiFiltersConfigs = () => []; + getInitialised = () => of(true); } diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cell-health/cf-cell-health-list-config.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cell-health/cf-cell-health-list-config.service.ts index e01675f771..4c62959ca5 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cell-health/cf-cell-health-list-config.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cell-health/cf-cell-health-list-config.service.ts @@ -2,8 +2,10 @@ import { DatePipe } from '@angular/common'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { first, tap } from 'rxjs/operators'; -import { CFAppState } from '../../../../../../../cloud-foundry/src/cf-app-state'; +import { CfCellHelper } from '../../../../../../../core/src/features/cloud-foundry/cf-cell.helpers'; import { BooleanIndicatorType, } from '../../../../../../../core/src/shared/components/boolean-indicator/boolean-indicator.component'; @@ -13,10 +15,10 @@ import { } from '../../../../../../../core/src/shared/components/list/list-table/table-cell-boolean-indicator/table-cell-boolean-indicator.component'; import { ITableColumn } from '../../../../../../../core/src/shared/components/list/list-table/table.types'; import { ListViewTypes } from '../../../../../../../core/src/shared/components/list/list.component.types'; -import { MetricQueryType } from '../../../../../../../core/src/shared/services/metrics-range-selector.types'; +import { PaginationMonitorFactory } from '../../../../../../../core/src/shared/monitors/pagination-monitor.factory'; import { ListView } from '../../../../../../../store/src/actions/list.actions'; -import { MetricQueryConfig } from '../../../../../../../store/src/actions/metrics.actions'; import { FetchCFCellMetricsPaginatedAction } from '../../../../../actions/cf-metrics.actions'; +import { CFAppState } from '../../../../../cf-app-state'; import { CloudFoundryCellService, } from '../../../../../features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell.service'; @@ -37,6 +39,7 @@ export class CfCellHealthListConfigService extends BaseCfListConfig; private boolIndicatorConfig: TableCellBooleanIndicatorComponentConfig = { isEnabled: (row: CfCellHealthEntry) => @@ -46,25 +49,28 @@ export class CfCellHealthListConfigService extends BaseCfListConfig, cloudFoundryCellService: CloudFoundryCellService, private datePipe: DatePipe) { + constructor( + private store: Store, + cloudFoundryCellService: CloudFoundryCellService, + private datePipe: DatePipe, + private paginationMonitorFactory: PaginationMonitorFactory) { super(); - const action = this.createMetricsAction(cloudFoundryCellService.cfGuid, cloudFoundryCellService.cellId); - this.dataSource = new CfCellHealthDataSource(store, this, action); + + this.init$ = this.createMetricsAction(cloudFoundryCellService.cfGuid, cloudFoundryCellService.cellId).pipe( + first(), + tap(action => { + this.dataSource = new CfCellHealthDataSource(this.store, this, action); + }) + ); this.showCustomTime = true; } - private createMetricsAction(cfGuid: string, cellId: string): FetchCFCellMetricsPaginatedAction { - const action = new FetchCFCellMetricsPaginatedAction( - cfGuid, - cellId, - new MetricQueryConfig(`firehose_value_metric_rep_unhealthy_cell{bosh_job_id="${cellId}"}`, {}), - MetricQueryType.QUERY - ); - action.initialParams['order-direction-field'] = 'dateTime'; - action.initialParams['order-direction'] = 'asc'; - return action; + private createMetricsAction(cfGuid: string, cellId: string): Observable { + const cellHelper = new CfCellHelper(this.store, this.paginationMonitorFactory); + return cellHelper.createCellMetricAction(cfGuid, cellId); } + getInitialised = () => this.init$; getColumns = (): ITableColumn[] => [ { columnId: 'dateTime', diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cells/cf-cells-data-source.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cells/cf-cells-data-source.ts index cf7e8e6512..44190871bf 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cells/cf-cells-data-source.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cells/cf-cells-data-source.ts @@ -1,20 +1,17 @@ import { Store } from '@ngrx/store'; import { map } from 'rxjs/operators'; -import { CFAppState } from '../../../../../../../cloud-foundry/src/cf-app-state'; import { ListDataSource, } from '../../../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source'; import { IListConfig } from '../../../../../../../core/src/shared/components/list/list.component.types'; -import { MetricQueryType } from '../../../../../../../core/src/shared/services/metrics-range-selector.types'; -import { MetricQueryConfig } from '../../../../../../../store/src/actions/metrics.actions'; import { IMetrics, IMetricVectorResult } from '../../../../../../../store/src/types/base-metric.types'; import { IMetricCell } from '../../../../../../../store/src/types/metric.types'; import { FetchCFCellMetricsPaginatedAction } from '../../../../../actions/cf-metrics.actions'; +import { CFAppState } from '../../../../../cf-app-state'; import { cfEntityFactory } from '../../../../../cf-entity-factory'; // TODO: Move file to CF package (#3769) - export class CfCellsDataSource extends ListDataSource, IMetrics>> { @@ -23,14 +20,11 @@ export class CfCellsDataSource static cellHealthyPath = 'value.1'; static cellDeploymentPath = 'metric.bosh_deployment'; - constructor(store: Store, cfGuid: string, listConfig: IListConfig>) { - const action = new FetchCFCellMetricsPaginatedAction( - cfGuid, - cfGuid, - new MetricQueryConfig('firehose_value_metric_rep_unhealthy_cell', {}), - MetricQueryType.QUERY - ); - + constructor( + store: Store, + listConfig: IListConfig>, + action: FetchCFCellMetricsPaginatedAction + ) { super({ store, action, diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cells/cf-cells-list-config.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cells/cf-cells-list-config.service.ts index bb03b0907e..6e664f59e2 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cells/cf-cells-list-config.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-cells/cf-cells-list-config.service.ts @@ -1,8 +1,11 @@ // tslint:disable:max-line-length import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { first, tap } from 'rxjs/operators'; import { CFAppState } from '../../../../../../../cloud-foundry/src/cf-app-state'; +import { CfCellHelper } from '../../../../../../../core/src/features/cloud-foundry/cf-cell.helpers'; import { BooleanIndicatorType, } from '../../../../../../../core/src/shared/components/boolean-indicator/boolean-indicator.component'; @@ -12,6 +15,7 @@ import { } from '../../../../../../../core/src/shared/components/list/list-table/table-cell-boolean-indicator/table-cell-boolean-indicator.component'; import { ITableColumn } from '../../../../../../../core/src/shared/components/list/list-table/table.types'; import { ListViewTypes } from '../../../../../../../core/src/shared/components/list/list.component.types'; +import { PaginationMonitorFactory } from '../../../../../../../core/src/shared/monitors/pagination-monitor.factory'; import { ListView } from '../../../../../../../store/src/actions/list.actions'; import { IMetricVectorResult } from '../../../../../../../store/src/types/base-metric.types'; import { IMetricCell } from '../../../../../../../store/src/types/metric.types'; @@ -33,6 +37,7 @@ export class CfCellsListConfigService extends BaseCfListConfig; private boolIndicatorConfig: TableCellBooleanIndicatorComponentConfig> = { // "0 signifies healthy, and 1 signifies unhealthy" @@ -98,11 +103,21 @@ export class CfCellsListConfigService extends BaseCfListConfig, private activeRouteCfCell: ActiveRouteCfCell) { + constructor( + store: Store, + private activeRouteCfCell: ActiveRouteCfCell, + paginationMonitorFactory: PaginationMonitorFactory) { super(); - this.dataSource = new CfCellsDataSource(store, activeRouteCfCell.cfGuid, this); + const cellHelper = new CfCellHelper(store, paginationMonitorFactory); + this.init$ = cellHelper.createCellMetricAction(activeRouteCfCell.cfGuid).pipe( + first(), + tap(action => { + this.dataSource = new CfCellsDataSource(store, this, action); + }) + ); } + getInitialised = () => this.init$; getColumns = () => this.columns; getDataSource = () => this.dataSource; } diff --git a/src/frontend/packages/core/misc/custom/custom.scss b/src/frontend/packages/core/misc/custom/custom.scss index eb5d596b7e..f6d83b743c 100644 --- a/src/frontend/packages/core/misc/custom/custom.scss +++ b/src/frontend/packages/core/misc/custom/custom.scss @@ -3,4 +3,3 @@ // This file is in the .gitignore - changes will not be flagged // The customization build step will replace this file with the custom one if provided - diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss index 184dc594e9..924e13afcf 100644 --- a/src/frontend/packages/core/sass/_all-theme.scss +++ b/src/frontend/packages/core/sass/_all-theme.scss @@ -32,6 +32,7 @@ @import '../src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.theme'; @import '../src/shared/components/upload-progress-indicator/upload-progress-indicator.component.theme'; @import '../src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.theme'; +@import '../src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.theme'; @import '../src/shared/components/start-end-date/start-end-date.component.theme'; @import '../src/shared/components/metrics-chart/metrics-chart.component.theme'; @import '../src/shared/components/metrics-range-selector/metrics-range-selector.component.theme'; @@ -41,7 +42,8 @@ @import '../src/features/user-profile/profile-info/profile-info.component.theme'; @import '../src/core/stateful-icon/stateful-icon.component.theme'; @import '../src/shared/components/markdown-preview/markdown-preview.component.theme'; -@import './components/mat-tabs.theme'; +@import './components/mat-snack-bar.theme'; +@import './components/ngx-charts-gauge.theme'; @import './components/text-status.theme'; @import './components/hyperlinks.theme'; @import './mat-themes'; @@ -82,12 +84,10 @@ $side-nav-light-active: #484848; $warn: map-get($theme, warn); $subdued: mat-contrast($primary, 50); - @if $is-dark==true { + @if $is-dark == true { $app-background-color: lighten(mat-color($background-colors, background), 10%); - $subdued: darken($subdued, 50); - } - - @else { + $subdued: lighten($subdued, 90); + } @else { $app-background-color: darken(mat-color($background-colors, background), 2%); $subdued: lighten($subdued, 50); } @@ -106,7 +106,8 @@ $side-nav-light-active: #484848; @include steppers-theme($theme, $app-theme); @include list-theme($theme, $app-theme); @include app-base-page-theme($theme, $app-theme); - @include app-mat-tabs-theme($theme, $app-theme); + @include app-mat-snack-bar-theme($theme, $app-theme); + @include ngx-charts-gauge($theme, $app-theme); @include app-text-status-theme($theme, $app-theme); @include app-card-status-theme($theme, $app-theme); @include app-usage-gauge-theme($theme, $app-theme); @@ -149,15 +150,14 @@ $side-nav-light-active: #484848; @include page-side-nav-theme($theme, $app-theme); @include cf-admin-add-user-warning($theme, $app-theme); @include entity-summary-title-theme($theme, $app-theme); + @include app-meta-card-item-theme($theme, $app-theme); @include error-page-theme($theme, $app-theme); } @function app-generate-nav-theme($theme, $nav-theme: null) { @if ($nav-theme) { @return $nav-theme; - } - - @else { + } @else { // Use default palette for side navigation @return (background: $side-nav-light-bg, background-top: $side-nav-light-bg, text: darken($side-nav-light-text, 10%), active: $side-nav-light-active, active-text: $side-nav-light-text, hover: $side-nav-light-hover, hover-text: $side-nav-light-text); } @@ -166,9 +166,7 @@ $side-nav-light-active: #484848; @function app-generate-status-theme($theme, $status-theme: null) { @if ($status-theme) { @return $status-theme; - } - - @else { + } @else { $warn: map-get($theme, warn); $primary: map-get($theme, primary); $white: #fff; // Use default palette for status diff --git a/src/frontend/packages/core/sass/components/mat-snack-bar.theme.scss b/src/frontend/packages/core/sass/components/mat-snack-bar.theme.scss new file mode 100644 index 0000000000..536d6434d7 --- /dev/null +++ b/src/frontend/packages/core/sass/components/mat-snack-bar.theme.scss @@ -0,0 +1,20 @@ +@mixin app-mat-snack-bar-theme($theme, $app-theme) { + $is-dark: map-get($theme, is-dark); + $background-colors: map-get($theme, background); + $foreground-colors: map-get($theme, foreground); + + $background-color: mat-color($foreground-colors, text); + $color: mat-color($background-colors, text); + + @if $is-dark == true { + $background-color: lighten(mat-color($background-colors, background), 5%); + $color: mat-color($foreground-colors, text); + } + + .mat-snack-bar-container { + background-color: $background-color; + .mat-simple-snackbar { + color: $color; + } + } +} diff --git a/src/frontend/packages/core/sass/components/mat-tabs.theme.scss b/src/frontend/packages/core/sass/components/mat-tabs.theme.scss deleted file mode 100644 index 29f7023533..0000000000 --- a/src/frontend/packages/core/sass/components/mat-tabs.theme.scss +++ /dev/null @@ -1,20 +0,0 @@ -// Fix the color of the underline for the active tab -// when on a primary backgrdound -// Need to revist this and check if this is an issue with the Angular Material library -@mixin app-mat-tabs-theme($theme, $app-theme) { - $is-dark: map-get($theme, is-dark); - $primary: map-get($theme, primary); - $tabs-ink-color: mat-color($primary); - @if $is-dark == true { - $tabs-ink-color: lighten($tabs-ink-color, 20%); - } @else { - $tabs-ink-color: darken($tabs-ink-color, 20%); - } - - .mat-tab-nav-bar.mat-primary.mat-background-primary { - .mat-ink-bar { - //background-color: $tabs-ink-color; - } - - } -} diff --git a/src/frontend/packages/core/sass/components/ngx-charts-gauge.theme.scss b/src/frontend/packages/core/sass/components/ngx-charts-gauge.theme.scss new file mode 100644 index 0000000000..406a49f950 --- /dev/null +++ b/src/frontend/packages/core/sass/components/ngx-charts-gauge.theme.scss @@ -0,0 +1,6 @@ +@mixin ngx-charts-gauge($theme, $app-theme) { + $foreground-colors: map-get($theme, foreground); + ngx-charts-chart text { + fill: mat-color($foreground-colors, text); + } +} diff --git a/src/frontend/packages/core/sass/theme.scss b/src/frontend/packages/core/sass/theme.scss index eb3e41787c..ed6428d931 100644 --- a/src/frontend/packages/core/sass/theme.scss +++ b/src/frontend/packages/core/sass/theme.scss @@ -6,18 +6,38 @@ // Custom theme support @import './custom'; -// Themes palettes and colors -$oss-theme-primary: mat-palette($mat-blue); -$oss-theme-accent: mat-palette($mat-blue); -$oss-theme-warn: mat-palette($mat-red); -$oss-theme: mat-light-theme($oss-theme-primary, $oss-theme-accent, $oss-theme-warn); +.dark-theme { + // Dark Theme defaults + $dark-primary: mat-palette($mat-blue); + $dark-accent: mat-palette($mat-amber, A400, A100, A700); + $dark-warn: mat-palette($mat-red); + $dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn); -// Default to using the open source theme -$stratos-theme: $oss-theme !default; -$stratos-nav-theme: null !default; -$stratos-status-theme: null !default; + $stratos-dark-theme: $dark-theme !default; + $stratos-dark-nav-theme: null !default; + $stratos-dark-status-theme: null !default; + + @include angular-material-theme($stratos-dark-theme); + @include app-theme($stratos-dark-theme, $stratos-dark-nav-theme, $stratos-dark-status-theme); +} + +.default { + // Themes palettes and colors + $oss-theme-primary: mat-palette($mat-blue); + $oss-theme-accent: mat-palette($mat-blue); + $oss-theme-warn: mat-palette($mat-red); + $oss-theme: mat-light-theme($oss-theme-primary, $oss-theme-accent, $oss-theme-warn); + + // Default to using the open source theme + $stratos-theme: $oss-theme !default; + $stratos-nav-theme: null !default; + $stratos-status-theme: null !default; + + @include angular-material-theme($stratos-theme); + @include app-theme($stratos-theme, $stratos-nav-theme, $stratos-status-theme); +} + +$stratos-dark-theme-supported: true !default; // Create the theme -@include mat-core(); -@include angular-material-theme($stratos-theme); -@include app-theme($stratos-theme, $stratos-nav-theme, $stratos-status-theme); +@include mat-core; diff --git a/src/frontend/packages/core/src/api-driven-views/components/api-type-selector/api-entity-type-selector.component.spec.ts b/src/frontend/packages/core/src/api-driven-views/components/api-type-selector/api-entity-type-selector.component.spec.ts index 9f538b0477..b54398c933 100644 --- a/src/frontend/packages/core/src/api-driven-views/components/api-type-selector/api-entity-type-selector.component.spec.ts +++ b/src/frontend/packages/core/src/api-driven-views/components/api-type-selector/api-entity-type-selector.component.spec.ts @@ -3,15 +3,15 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ApiEntityType } from '../../api-drive-views.types'; import { SharedModule } from '../../../shared/shared.module'; import { ApiEntityTypeSelectorComponent } from './api-entity-type-selector.component'; +import { ApiDrivenViewsModule } from '../../api-driven-views.module'; -fdescribe('ApiEntityTypeSelectorComponent', () => { +describe('ApiEntityTypeSelectorComponent', () => { let component: ApiEntityTypeSelectorComponent; let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ApiEntityTypeSelectorComponent], - imports: [SharedModule] + imports: [SharedModule, ApiDrivenViewsModule] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-select-page/api-endpoint-select-page.component.spec.ts b/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-select-page/api-endpoint-select-page.component.spec.ts index 46d0bff541..8edb118f05 100644 --- a/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-select-page/api-endpoint-select-page.component.spec.ts +++ b/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-select-page/api-endpoint-select-page.component.spec.ts @@ -1,6 +1,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ApiEndpointSelectPageComponent } from './api-endpoint-select-page.component'; +import { SharedModule } from '../../../shared/shared.module'; +import { ApiDrivenViewsModule } from '../../api-driven-views.module'; +import { CoreModule } from '../../../core/core.module'; +import { RouterTestingModule } from '@angular/router/testing'; +import { createBasicStoreModule } from '../../../../test-framework/store-test-helper'; +import { TabNavService } from '../../../../tab-nav.service'; +import { EntityCatalogueModule } from '../../../core/entity-catalogue.module'; +import { generateStratosEntities } from '../../../base-entity-types'; describe('ApiEndpointSelectPageComponent', () => { let component: ApiEndpointSelectPageComponent; @@ -8,9 +16,17 @@ describe('ApiEndpointSelectPageComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ApiEndpointSelectPageComponent ] + imports: [ + EntityCatalogueModule.forFeature(generateStratosEntities), + CoreModule, + RouterTestingModule, + SharedModule, + createBasicStoreModule(), + ApiDrivenViewsModule, + ], + providers: [TabNavService] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-select-page/api-endpoint-select-page.component.ts b/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-select-page/api-endpoint-select-page.component.ts index 7bf4a74a36..4e0374aa43 100644 --- a/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-select-page/api-endpoint-select-page.component.ts +++ b/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-select-page/api-endpoint-select-page.component.ts @@ -5,7 +5,7 @@ import { ApiEntityType } from '../../api-drive-views.types'; import { Store } from '@ngrx/store'; import { connectedEndpointsOfTypesSelector } from '../../../../../store/src/selectors/endpoint.selectors'; import { ActivatedRoute, Router } from '@angular/router'; -import { map } from 'rxjs/operators'; +import { map, filter } from 'rxjs/operators'; @Component({ selector: 'app-api-endpoint-select-page', @@ -26,7 +26,9 @@ export class ApiEndpointSelectPageComponent implements OnInit { const endpointType = this.route.snapshot.params.endpointType; this.connectedEndpointsOfType$ = this.store.select(connectedEndpointsOfTypesSelector(endpointType)).pipe( - map(endpoints => Object.values(endpoints).map(endpoint => new ApiEntityType( + map(endpointsMap => Object.values(endpointsMap)), + filter(endpoints => !!endpoints || !endpoints.length), + map(endpoints => endpoints.map(endpoint => new ApiEntityType( endpoint.guid, endpoint.name // TODO Get icon from entity catalogue diff --git a/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-type-select-page/api-endpoint-type-select-page.component.spec.ts b/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-type-select-page/api-endpoint-type-select-page.component.spec.ts index f07f695835..ccf3a5249f 100644 --- a/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-type-select-page/api-endpoint-type-select-page.component.spec.ts +++ b/src/frontend/packages/core/src/api-driven-views/features/api-endpoint-type-select-page/api-endpoint-type-select-page.component.spec.ts @@ -2,6 +2,15 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ApiEndpointTypeSelectPageComponent } from './api-endpoint-type-select-page.component'; import { ApiDrivenViewsModule } from '../../api-driven-views.module'; +import { StoreModule } from '@ngrx/store'; +import { appReducers } from '../../../../../store/src/reducers.module'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CoreModule } from '../../../core/core.module'; +import { SharedModule } from '../../../shared/shared.module'; +import { createBasicStoreModule } from '../../../../test-framework/store-test-helper'; +import { TabNavService } from '../../../../tab-nav.service'; +import { EntityCatalogueModule } from '../../../core/entity-catalogue.module'; +import { generateStratosEntities } from '../../../base-entity-types'; describe('ApiEndpointTypeSelectPageComponent', () => { let component: ApiEndpointTypeSelectPageComponent; @@ -9,7 +18,15 @@ describe('ApiEndpointTypeSelectPageComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ApiDrivenViewsModule] + imports: [ + EntityCatalogueModule.forFeature(generateStratosEntities), + CoreModule, + RouterTestingModule, + SharedModule, + createBasicStoreModule(), + ApiDrivenViewsModule, + ], + providers: [TabNavService] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.html b/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.html index 86fe2daaa3..03efd9c9ab 100644 --- a/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.html +++ b/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.spec.ts b/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.spec.ts index 8469756c7d..340228d85c 100644 --- a/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.spec.ts +++ b/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.spec.ts @@ -1,6 +1,13 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ApiEntityListPageComponent } from './api-entity-list-page.component'; +import { ApiDrivenViewsModule } from '../../api-driven-views.module'; +import { RouterTestingModule } from '@angular/router/testing'; +import { EntityCatalogueModule } from '../../../core/entity-catalogue.module'; +import { generateStratosEntities } from '../../../base-entity-types'; +import { CoreModule } from '../../../core/core.module'; +import { SharedModule } from '../../../shared/shared.module'; +import { createBasicStoreModule } from '../../../../test-framework/store-test-helper'; describe('ApiEntityListPageComponent', () => { let component: ApiEntityListPageComponent; @@ -8,9 +15,16 @@ describe('ApiEntityListPageComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ApiEntityListPageComponent ] + imports: [ + EntityCatalogueModule.forFeature(generateStratosEntities), + CoreModule, + RouterTestingModule, + SharedModule, + createBasicStoreModule(), + ApiDrivenViewsModule, + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.ts b/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.ts index b15e37ba8f..0e2df91460 100644 --- a/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.ts +++ b/src/frontend/packages/core/src/api-driven-views/features/api-entity-list-page/api-entity-list-page.component.ts @@ -16,21 +16,25 @@ export class ApiEntityListPageComponent implements OnInit { ) { } ngOnInit() { - const endpointType = this.route.parent.snapshot.params.endpointType; + const endpointType = this.route.parent ? this.route.parent.snapshot.params.endpointType : null; const entityType = this.route.snapshot.params.entityType; - const a = entityCatalogue.getEntity(endpointType, entityType); - // All these missing properties will need wiring in for CF case, maybe only endpointGuid for k8s? - this.config = { - endpointGuid: null, - entityConfig: { - endpointType: a.endpointType, - entityType: a.entityKey, - schemaKey: null, - subType: null - }, - extraArgs: null, - paginationKey: null - }; + const entityConfig = entityCatalogue.getEntity(endpointType, entityType); + if (entityConfig) { + // All these missing properties will need wiring in for CF case, maybe only endpointGuid for k8s? + this.config = { + endpointGuid: null, + entityConfig: { + endpointType: entityConfig.endpointType, + entityType: entityConfig.entityKey, + schemaKey: null, + subType: null + }, + extraArgs: null, + paginationKey: null + }; + } else { + console.warn(`Failed to find entity for. Endpoint Type: ${endpointType}. Entity Type: ${entityType}`); + } } } diff --git a/src/frontend/packages/core/src/api-driven-views/features/api-entity-type-select-page/api-entity-type-select-page.component.spec.ts b/src/frontend/packages/core/src/api-driven-views/features/api-entity-type-select-page/api-entity-type-select-page.component.spec.ts index 257136f53e..1dcdf1be51 100644 --- a/src/frontend/packages/core/src/api-driven-views/features/api-entity-type-select-page/api-entity-type-select-page.component.spec.ts +++ b/src/frontend/packages/core/src/api-driven-views/features/api-entity-type-select-page/api-entity-type-select-page.component.spec.ts @@ -1,6 +1,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ApiEntityTypeSelectPageComponent } from './api-entity-type-select-page.component'; +import { ApiDrivenViewsModule } from '../../api-driven-views.module'; +import { RouterTestingModule } from '@angular/router/testing'; +import { EntityCatalogueModule } from '../../../core/entity-catalogue.module'; +import { generateStratosEntities } from '../../../base-entity-types'; +import { CoreModule } from '../../../core/core.module'; +import { SharedModule } from '../../../shared/shared.module'; +import { createBasicStoreModule } from '../../../../test-framework/store-test-helper'; +import { TabNavService } from '../../../../tab-nav.service'; describe('ApiEntityTypeSelectPageComponent', () => { let component: ApiEntityTypeSelectPageComponent; @@ -8,9 +16,17 @@ describe('ApiEntityTypeSelectPageComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ApiEntityTypeSelectPageComponent ] + imports: [ + EntityCatalogueModule.forFeature(generateStratosEntities), + CoreModule, + RouterTestingModule, + SharedModule, + createBasicStoreModule(), + ApiDrivenViewsModule, + ], + providers: [TabNavService] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/src/frontend/packages/core/src/api-driven-views/features/api-entity-type-select-page/api-entity-type-select-page.component.ts b/src/frontend/packages/core/src/api-driven-views/features/api-entity-type-select-page/api-entity-type-select-page.component.ts index 0a9d57966c..f0dce95d7c 100644 --- a/src/frontend/packages/core/src/api-driven-views/features/api-entity-type-select-page/api-entity-type-select-page.component.ts +++ b/src/frontend/packages/core/src/api-driven-views/features/api-entity-type-select-page/api-entity-type-select-page.component.ts @@ -32,13 +32,14 @@ export class ApiEntityTypeSelectPageComponent implements OnInit { const endpointEntity = entityCatalogue.getEndpoint(endpointType); const endpointEntities = entityCatalogue.getAllEntitiesForEndpointType(endpointType); const entitiesWithGetMultiple = endpointEntities.filter( - entity => entity.definition.tableConfig && entity.actionOrchestrator.hasActionBuilder('getMultiple') + entity => entity && entity.definition.tableConfig && entity.actionOrchestrator.hasActionBuilder('getMultiple') ); this.connectedEndpointsOfType$ = this.store.select(connectedEndpointsOfTypesSelector(endpointType)).pipe( map(endpoints => endpoints[endpointGuid] ? endpoints[endpointGuid].name : 'Entities') ); - - this.tabNavService.setHeader(endpointEntity.definition.label); + if (endpointEntity) { + this.tabNavService.setHeader(endpointEntity.definition.label); + } this.tabs = entitiesWithGetMultiple.map(entity => { return { link: entity.type, diff --git a/src/frontend/packages/core/src/app.component.html b/src/frontend/packages/core/src/app.component.html index 192fe40149..16017c9762 100644 --- a/src/frontend/packages/core/src/app.component.html +++ b/src/frontend/packages/core/src/app.component.html @@ -1,2 +1,5 @@ - -
{{userId}}
\ No newline at end of file + + + +
{{userId}}
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/app.component.ts b/src/frontend/packages/core/src/app.component.ts index 62df36b02f..5dce6095bc 100644 --- a/src/frontend/packages/core/src/app.component.ts +++ b/src/frontend/packages/core/src/app.component.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; import { create } from 'rxjs-spy'; import { AuthOnlyAppState } from '../../store/src/app-state'; +import { ThemeService } from './core/theme.service'; import { environment } from './environments/environment'; import { LoggedInService } from './logged-in.service'; @@ -20,7 +21,8 @@ export class AppComponent implements OnInit, OnDestroy, AfterContentInit { constructor( private loggedInService: LoggedInService, - store: Store + store: Store, + public themeService: ThemeService ) { // We use the username to key the session storage. We could replace this with the users id? this.userId$ = store.select(state => state.auth.sessionData && state.auth.sessionData.user ? state.auth.sessionData.user.name : null); diff --git a/src/frontend/packages/core/src/core/endpoints.service.ts b/src/frontend/packages/core/src/core/endpoints.service.ts index 1195cdeb90..7a887277c1 100644 --- a/src/frontend/packages/core/src/core/endpoints.service.ts +++ b/src/frontend/packages/core/src/core/endpoints.service.ts @@ -2,18 +2,15 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; import { Store } from '@ngrx/store'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { filter, first, map, skipWhile, withLatestFrom } from 'rxjs/operators'; +import { first, map, skipWhile, withLatestFrom } from 'rxjs/operators'; import { RouterNav } from '../../../store/src/actions/router.actions'; import { EndpointOnlyAppState, IRequestEntityTypeState } from '../../../store/src/app-state'; import { AuthState } from '../../../store/src/reducers/auth.reducer'; -import { - endpointEntitiesSelector, - endpointsEntityRequestDataSelector, - endpointStatusSelector, -} from '../../../store/src/selectors/endpoint.selectors'; +import { endpointEntitiesSelector, endpointStatusSelector } from '../../../store/src/selectors/endpoint.selectors'; import { EndpointModel, EndpointState } from '../../../store/src/types/endpoint.types'; import { EndpointHealthCheck, EndpointHealthChecks } from '../../endpoints-health-checks'; +import { endpointHasMetricsByAvailable } from '../features/endpoints/endpoint-helpers'; import { entityCatalogue } from './entity-catalogue/entity-catalogue.service'; import { UserService } from './user.service'; @@ -121,11 +118,7 @@ export class EndpointsService implements CanActivate { } hasMetrics(endpointId: string): Observable { - return this.store.select(endpointsEntityRequestDataSelector(endpointId)).pipe( - filter(endpoint => !!endpoint), - map(endpoint => endpoint.metricsAvailable), - first() - ); + return endpointHasMetricsByAvailable(this.store, endpointId); } doesNotHaveConnectedEndpointType(type: string): Observable { diff --git a/src/frontend/packages/core/src/core/style.service.ts b/src/frontend/packages/core/src/core/style.service.ts new file mode 100644 index 0000000000..f832f06c0d --- /dev/null +++ b/src/frontend/packages/core/src/core/style.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class StyleService { + + private rules: string[] = []; + constructor() { + this.rules = this.getAllSelectors(); + } + + hasSelector = (selector) => { + return !!this.rules.find(ruleSelector => ruleSelector === selector); + } + + private getAllSelectors = (): string[] => { + const ret = []; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < document.styleSheets.length; i++) { + const styleSheet = document.styleSheets[i]; + if (!(styleSheet instanceof CSSStyleSheet)) { + continue; + } + const rules = styleSheet.rules || styleSheet.cssRules; + // tslint:disable-next-line:prefer-for-of + for (let y = 0; y < rules.length; y++) { + const rule = rules[y]; + if (!(rule instanceof CSSStyleRule)) { + continue; + } + if (typeof rule.selectorText === 'string') { ret.push(rule.selectorText); } + } + } + return ret; + } + +} diff --git a/src/frontend/packages/core/src/core/theme.service.ts b/src/frontend/packages/core/src/core/theme.service.ts new file mode 100644 index 0000000000..f5ee16ba18 --- /dev/null +++ b/src/frontend/packages/core/src/core/theme.service.ts @@ -0,0 +1,151 @@ +import { OverlayContainer } from '@angular/cdk/overlay'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; + +import { SetThemeAction } from '../../../store/src/actions/dashboard-actions'; +import { DashboardOnlyAppState } from '../../../store/src/app-state'; +import { selectDashboardState } from '../../../store/src/selectors/dashboard.selectors'; +import { StyleService } from './style.service'; + +export interface StratosTheme { + key: string; + label: string; + styleName: string; +} + +const lightTheme: StratosTheme = { + key: 'default', + label: 'Light', + styleName: 'default' +}; +const darkTheme: StratosTheme = { + key: 'dark', + label: 'Dark', + styleName: 'dark-theme' +}; +const osTheme: StratosTheme = { + key: 'os', + label: 'OS', + styleName: '' +}; + +@Injectable({ + providedIn: 'root', +}) +export class ThemeService { + + private osThemeInfo = { + supports: false, + isDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches, + isLightMode: window.matchMedia('(prefers-color-scheme: light)').matches, + isNotSpecified: window.matchMedia('(prefers-color-scheme: no-preference)').matches + }; + private themes: StratosTheme[] = [lightTheme]; + + constructor( + private store: Store, + private overlayContainer: OverlayContainer, + private styleService: StyleService) { + this.initialiseStratosThemeInfo(); + } + + getThemes(): StratosTheme[] { + return this.themes; + } + + getTheme(): Observable { + return this.store.select(selectDashboardState).pipe( + map(dashboardState => this.findTheme(dashboardState.themeKey)), + ); + } + + setTheme(themeKey: string) { + const findTheme = this.findTheme(themeKey); + this.setOverlay(findTheme); + this.store.dispatch(new SetThemeAction(findTheme)); + } + + /** + * Initialize the service with a theme that may already exists in the store + */ + initialize() { + this.getTheme().pipe(first()).subscribe(theme => this.setOverlay(theme)); + } + + private initialiseStratosThemeInfo() { + const hasDarkTheme = this.styleService.hasSelector('.dark-theme-supported'); + + if (hasDarkTheme) { + this.themes.push(darkTheme); + + this.initialiseOsThemeInfo(); + } + + } + + private initialiseOsThemeInfo() { + this.osThemeInfo.supports = this.osThemeInfo.isDarkMode || this.osThemeInfo.isLightMode || this.osThemeInfo.isNotSpecified; + + if (this.osThemeInfo.supports) { + this.themes.push(osTheme); + + // Watch for changes at run time + window.matchMedia('(prefers-color-scheme: dark)').addListener(e => e.matches && this.updateFollowingOsThemeChange()); + window.matchMedia('(prefers-color-scheme: light)').addListener(e => e.matches && this.updateFollowingOsThemeChange()); + window.matchMedia('(prefers-color-scheme: no-preference)').addListener(e => e.matches && this.updateFollowingOsThemeChange()); + } + } + + /** + * Find a theme in a safe way with fall backs + */ + private findTheme(themeKey: string): StratosTheme { + if (themeKey === osTheme.key && this.getThemes().find(theme => theme.key === osTheme.key)) { + return this.getOsTheme() || lightTheme; + } + return this.getThemes().find(theme => theme.key === themeKey) || lightTheme; + } + + /** + * Create an `OS` theme that contains the relevant style + */ + private getOsTheme(): StratosTheme { + if (this.osThemeInfo.supports) { + return this.osThemeInfo.isDarkMode ? { + ...osTheme, + styleName: darkTheme.styleName + } : this.osThemeInfo.isLightMode || this.osThemeInfo.isNotSpecified ? { + ...osTheme, + styleName: lightTheme.styleName + } : null; + } + } + + /** + * Overlays require the theme specifically set, see https://material.angular.io/guide/theming#multiple-themes + * `Multiple themes and overlay-based components` + */ + private setOverlay(newTheme: StratosTheme) { + // Remove pre-existing styles + this.getThemes() + .filter(theme => theme.styleName) + .forEach(theme => this.overlayContainer.getContainerElement().classList.remove(theme.styleName)); + // Add new style (not from getThemes list, handles OS case) + this.overlayContainer.getContainerElement().classList.add(newTheme.styleName); + } + + /** + * Update theme given changes in OS theme settings + */ + private updateFollowingOsThemeChange() { + this.osThemeInfo.isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; + this.osThemeInfo.isLightMode = window.matchMedia('(prefers-color-scheme: light)').matches; + this.osThemeInfo.isNotSpecified = window.matchMedia('(prefers-color-scheme: no-preference)').matches; + + this.store.select(selectDashboardState).pipe( + first() + ).subscribe(dashboardState => dashboardState.themeKey === osTheme.key && this.setTheme(osTheme.key)); + } +} diff --git a/src/frontend/packages/core/src/features/cloud-foundry/cf-cell.helpers.ts b/src/frontend/packages/core/src/features/cloud-foundry/cf-cell.helpers.ts new file mode 100644 index 0000000000..af49499bbf --- /dev/null +++ b/src/frontend/packages/core/src/features/cloud-foundry/cf-cell.helpers.ts @@ -0,0 +1,71 @@ +import { Store } from '@ngrx/store'; +import { Observable, of } from 'rxjs'; +import { filter, first, map, publishReplay, refCount, switchMap } from 'rxjs/operators'; + +import { CFEntityConfig } from '../../../../cloud-foundry/cf-types'; +import { FetchCFCellMetricsPaginatedAction } from '../../../../cloud-foundry/src/actions/cf-metrics.actions'; +import { + CellMetrics, +} from '../../../../cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell.service'; +import { MetricQueryConfig } from '../../../../store/src/actions/metrics.actions'; +import { AppState } from '../../../../store/src/app-state'; +import { getPaginationObservables } from '../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; +import { IMetrics } from '../../../../store/src/types/base-metric.types'; +import { PaginationMonitorFactory } from '../../shared/monitors/pagination-monitor.factory'; +import { MetricQueryType } from '../../shared/services/metrics-range-selector.types'; +import { endpointHasMetricsByAvailable } from '../endpoints/endpoint-helpers'; + +export class CfCellHelper { + + constructor( + private store: Store, + private paginationMonitorFactory: PaginationMonitorFactory) { + } + + public createCellMetricAction(cfId: string, cellId?: string): Observable { + const cellIdString = !!cellId ? `{bosh_job_id="${cellId}"}` : ''; + + const newMetricAction: FetchCFCellMetricsPaginatedAction = new FetchCFCellMetricsPaginatedAction( + cfId, + cfId, + new MetricQueryConfig(CellMetrics.HEALTHY + cellIdString, {}), + MetricQueryType.QUERY + ); + return this.hasMetric(newMetricAction).pipe( + switchMap(hasNewMetric => hasNewMetric ? + of(hasNewMetric) : + this.hasMetric(new FetchCFCellMetricsPaginatedAction( + cfId, + cfId, + new MetricQueryConfig(CellMetrics.HEALTHY_DEP + cellIdString, {}), + MetricQueryType.QUERY + )) + ) + ); + } + + private hasMetric(action: FetchCFCellMetricsPaginatedAction): Observable { + return getPaginationObservables({ + store: this.store, + action, + paginationMonitor: this.paginationMonitorFactory.create( + action.paginationKey, + new CFEntityConfig(action.entityType) + ) + }).entities$.pipe( + filter(entities => !!entities && !!entities.length), + first(), + map(entities => !!entities.find(entity => !!entity.data.result.length) ? action : null), + publishReplay(1), + refCount() + ); + } + + public hasCellMetrics(endpointId: string): Observable { + return endpointHasMetricsByAvailable(this.store, endpointId).pipe( + // If metrics set up for this endpoint check if we can fetch cell metrics from it. + // If the metric is unknown an empty list is returned + switchMap(hasMetrics => hasMetrics ? this.createCellMetricAction(endpointId).pipe(map(action => !!action)) : of(false)) + ); + } +} diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss index 0884263a2b..8c10e82191 100644 --- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss +++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss @@ -3,7 +3,6 @@ $app-sub-header-height: 48px; .dashboard { - background-color: transparent; display: flex; flex-direction: column; height: 100%; diff --git a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss index a05435ff5a..d7c1ebb669 100644 --- a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss +++ b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss @@ -23,7 +23,7 @@ } } &__item { - color: rgba(0, 0, 0, .6); + color: map-get($app-theme, subdued-color); &--active { background-color: transparentize($primary-color, .9); color: $primary-color; diff --git a/src/frontend/packages/core/src/features/endpoints/endpoint-helpers.ts b/src/frontend/packages/core/src/features/endpoints/endpoint-helpers.ts index 2b83b6b969..0d6a5a43ae 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoint-helpers.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoint-helpers.ts @@ -1,10 +1,13 @@ import { Type } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { filter, first, map } from 'rxjs/operators'; -import { EndpointOnlyAppState } from '../../../../store/src/app-state'; -import { endpointEntitiesSelector } from '../../../../store/src/selectors/endpoint.selectors'; +import { AppState, EndpointOnlyAppState } from '../../../../store/src/app-state'; +import { + endpointEntitiesSelector, + endpointsEntityRequestDataSelector, +} from '../../../../store/src/selectors/endpoint.selectors'; import { EndpointModel } from '../../../../store/src/types/endpoint.types'; import { EndpointListDetailsComponent } from '../../shared/components/list/list-types/endpoint/endpoint-list.helpers'; @@ -39,3 +42,12 @@ export function endpointHasMetrics(endpointGuid: string, store: Store !!state[endpointGuid].metadata && !!state[endpointGuid].metadata.metrics) ); } + +// There are two different methods for checking if an endpoint has metrics. Need to understand use cases +export function endpointHasMetricsByAvailable(store: Store, endpointId: string): Observable { + return store.select(endpointsEntityRequestDataSelector(endpointId)).pipe( + filter(endpoint => !!endpoint), + map(endpoint => endpoint.metricsAvailable), + first() + ); +} diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html index 93feabd5f5..7e807d5687 100644 --- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html +++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html @@ -92,6 +92,18 @@

User Profile

polling may result in some pages showing out-of-date information. + diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts index fda5d8da05..b504c6832f 100644 --- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts +++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts @@ -7,10 +7,11 @@ import { SetPollingEnabledAction, SetSessionTimeoutAction } from '../../../../.. import { DashboardOnlyAppState } from '../../../../../store/src/app-state'; import { selectDashboardState } from '../../../../../store/src/selectors/dashboard.selectors'; import { UserProfileInfo } from '../../../../../store/src/types/user-profile.types'; +import { ThemeService } from '../../../core/theme.service'; +import { UserService } from '../../../core/user.service'; import { ConfirmationDialogConfig } from '../../../shared/components/confirmation-dialog.config'; import { ConfirmationDialogService } from '../../../shared/components/confirmation-dialog.service'; import { UserProfileService } from '../user-profile.service'; -import { UserService } from '../../../core/user.service'; @Component({ selector: 'app-profile-info', @@ -31,6 +32,7 @@ export class ProfileInfoComponent implements OnInit { userProfile$: Observable; primaryEmailAddress$: Observable; + hasMultipleThemes: boolean; private sessionDialogConfig = new ConfirmationDialogConfig( 'Disable session timeout', @@ -60,6 +62,7 @@ export class ProfileInfoComponent implements OnInit { private store: Store, private confirmDialog: ConfirmationDialogService, public userService: UserService, + public themeService: ThemeService ) { this.isError$ = userProfileService.isError$; this.userProfile$ = userProfileService.userProfile$; @@ -67,6 +70,8 @@ export class ProfileInfoComponent implements OnInit { this.primaryEmailAddress$ = this.userProfile$.pipe( map((profile: UserProfileInfo) => userProfileService.getPrimaryEmailAddress(profile)) ); + + this.hasMultipleThemes = themeService.getThemes().length > 1; } ngOnInit() { diff --git a/src/frontend/packages/core/src/shared/components/code-block/code-block.component.theme.scss b/src/frontend/packages/core/src/shared/components/code-block/code-block.component.theme.scss index 1010773073..bfac5a8b6e 100644 --- a/src/frontend/packages/core/src/shared/components/code-block/code-block.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/code-block/code-block.component.theme.scss @@ -1,16 +1,22 @@ @import '~@angular/material/theming'; @mixin display-value-theme($theme, $app-theme) { + $is-dark: map-get($theme, is-dark); $primary: map-get($theme, primary); $background-colors: map-get($theme, background); $foreground-colors: map-get($theme, foreground); .app-code-block { - background-color: mat-color($foreground-colors, text); - color: darken(mat-color($background-colors, background), 2%); - &___copied-icon { - color: mat-color($primary); + $background-color: mat-color($foreground-colors, text); + $color: darken(mat-color($background-colors, background), 2%); + @if $is-dark == true { + // See the app variable and cf cli pages + $background-color: lighten(mat-color($background-colors, background), 5%); + $color: mat-color($foreground-colors, text); } - &__copied-div { - background-color: mat-color($foreground-colors, text); + + background-color: $background-color; + color: $color; + &__copied-icon { + color: mat-color($primary); } } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.scss b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.scss index ab1ec9abec..7ad3af708e 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.scss @@ -1,6 +1,5 @@ .meta-card-item-row, .meta-card-item-row-top { - $text-color: rgba(0, 0, 0, .6); align-items: center; display: flex; justify-content: space-between; @@ -20,8 +19,6 @@ } .meta-card-item__key { - $text-color: rgba(0, 0, 0, .6); - color: $text-color; flex: none; font-weight: 300; padding-right: 10px; @@ -51,7 +48,6 @@ line-height: 18px; position: relative; &::after { - background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 75%); bottom: 0; content: ''; height: 1.2em; diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.theme.scss b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.theme.scss new file mode 100644 index 0000000000..890951c936 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.theme.scss @@ -0,0 +1,12 @@ +@mixin app-meta-card-item-theme($theme, $app-theme) { + $backgrounds: map-get($theme, background); + $background: mat-color($backgrounds, card); + + .meta-card-item-long-text-fixed { + .meta-card-item__value { + &::after { + background: linear-gradient(to right, rgba(255, 255, 255, 0), $background 50%); + } + } + } +} diff --git a/src/frontend/packages/core/src/shared/components/list/list-generics/list-view/list-view.component.ts b/src/frontend/packages/core/src/shared/components/list/list-generics/list-view/list-view.component.ts index 9229fa3609..9438e7ad30 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-generics/list-view/list-view.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-generics/list-view/list-view.component.ts @@ -17,7 +17,7 @@ export class ListViewComponent implements OnInit { @Input() config: ListConfigProvider; - @ViewChild(ListHostDirective) + @ViewChild(ListHostDirective, { static: true }) public listHost: ListHostDirective; private componentRef: ComponentRef>; diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.theme.scss b/src/frontend/packages/core/src/shared/components/list/list.component.theme.scss index 6c51012b83..c7e86ff821 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/list/list.component.theme.scss @@ -4,10 +4,19 @@ $primary: map-get($theme, primary); $foreground-colors: map-get($theme, foreground); $background-colors: map-get($theme, background); + $is-dark: map-get($theme, is-dark); + + $header-selected-color: #fff; + @if $is-dark == true { + $header-selected-color: lighten(mat-color($background-colors, background), 20%); + } @else { + $header-selected-color: mat-color($primary, 50); + } + .list-component { &__header { &--selected { - background-color: mat-color($primary, 50); + background-color: $header-selected-color; } &__right, &__left--multi-filters { diff --git a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss index 736c50de6b..65ed55245f 100644 --- a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss @@ -7,6 +7,7 @@ $error: map-get($status-colors, danger); $warning: map-get($status-colors, warning); $info: lighten($primmary-color, 20%); + $subdued: map-get($app-theme, subdued-color); .page-header { &__warning-count { background-color: mat-color($primary); @@ -18,6 +19,12 @@ &__menu-separator { background-color: mat-color($foreground, divider); } + &__menu-inner { + color: mat-contrast($primary, 500); + } + &__history { + color: $subdued; + } &__underflow { background-color: mat-color($primary); } diff --git a/src/frontend/packages/core/src/styles.scss b/src/frontend/packages/core/src/styles.scss index 1fe35bd298..f6e062111e 100644 --- a/src/frontend/packages/core/src/styles.scss +++ b/src/frontend/packages/core/src/styles.scss @@ -65,3 +65,11 @@ button.mat-simple-snackbar-action { // flex: 1; // flex-direction: column; // } + + +// Add selector so that the UI can detect if a dark theme is available +@if $stratos-dark-theme-supported { + .dark-theme-supported { + margin: 0; + } +} diff --git a/src/frontend/packages/core/test-framework/store-test-helper.ts b/src/frontend/packages/core/test-framework/store-test-helper.ts index c2383e2937..3a6277b913 100644 --- a/src/frontend/packages/core/test-framework/store-test-helper.ts +++ b/src/frontend/packages/core/test-framework/store-test-helper.ts @@ -164,7 +164,8 @@ function getDefaultInitialTestStratosStoreState() { isMobile: false, isMobileNavOpen: false, sideNavPinned: false, - pollingEnabled: true + pollingEnabled: true, + themeKey: null }, actionHistory: [], lists: {}, diff --git a/src/frontend/packages/store/src/actions/dashboard-actions.ts b/src/frontend/packages/store/src/actions/dashboard-actions.ts index 88c8f8019c..3918585f85 100644 --- a/src/frontend/packages/store/src/actions/dashboard-actions.ts +++ b/src/frontend/packages/store/src/actions/dashboard-actions.ts @@ -1,5 +1,6 @@ import { Action } from '@ngrx/store'; +import { StratosTheme } from '../../../core/src/core/theme.service'; import { DashboardState } from '../reducers/dashboard-reducer'; export const OPEN_SIDE_NAV = '[Dashboard] Open side nav'; @@ -16,6 +17,7 @@ export const CLOSE_SIDE_HELP = '[Dashboard] Close side help'; export const TIMEOUT_SESSION = '[Dashboard] Timeout Session'; export const ENABLE_POLLING = '[Dashboard] Enable Polling'; +export const SET_STRATOS_THEME = '[Dashboard] Set Theme'; export const HYDRATE_DASHBOARD_STATE = '[Dashboard] Hydrate dashboard state'; @@ -73,3 +75,7 @@ export class HydrateDashboardStateAction implements Action { type = HYDRATE_DASHBOARD_STATE; } +export class SetThemeAction implements Action { + constructor(public theme: StratosTheme) { } + type = SET_STRATOS_THEME; +} diff --git a/src/frontend/packages/store/src/effects/dashboard.effects.ts b/src/frontend/packages/store/src/effects/dashboard.effects.ts new file mode 100644 index 0000000000..0e796db044 --- /dev/null +++ b/src/frontend/packages/store/src/effects/dashboard.effects.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { map } from 'rxjs/operators'; + +import { ThemeService } from '../../../core/src/core/theme.service'; +import { HYDRATE_DASHBOARD_STATE, HydrateDashboardStateAction } from '../actions/dashboard-actions'; + + +@Injectable() +export class DashboardEffect { + + constructor( + private actions$: Actions, + private themeService: ThemeService + ) { } + + @Effect({ dispatch: false }) hydrate$ = this.actions$.pipe( + ofType(HYDRATE_DASHBOARD_STATE), + map(() => { + // Ensure the previous theme is applied + this.themeService.initialize(); + }) + ); +} diff --git a/src/frontend/packages/store/src/reducers/dashboard-reducer.ts b/src/frontend/packages/store/src/reducers/dashboard-reducer.ts index 5963b912b9..b5f606d521 100644 --- a/src/frontend/packages/store/src/reducers/dashboard-reducer.ts +++ b/src/frontend/packages/store/src/reducers/dashboard-reducer.ts @@ -7,8 +7,10 @@ import { HYDRATE_DASHBOARD_STATE, HydrateDashboardStateAction, OPEN_SIDE_NAV, + SET_STRATOS_THEME, SetPollingEnabledAction, SetSessionTimeoutAction, + SetThemeAction, SHOW_SIDE_HELP, TIMEOUT_SESSION, TOGGLE_SIDE_NAV, @@ -23,6 +25,7 @@ export interface DashboardState { sideNavPinned: boolean; sideHelpOpen: boolean; sideHelpDocument: string; + themeKey: string; } export const defaultDashboardState: DashboardState = { @@ -34,6 +37,7 @@ export const defaultDashboardState: DashboardState = { sideNavPinned: true, sideHelpOpen: false, sideHelpDocument: null, + themeKey: null }; export function dashboardReducer(state: DashboardState = defaultDashboardState, action): DashboardState { @@ -79,6 +83,12 @@ export function dashboardReducer(state: DashboardState = defaultDashboardState, ...state, ...hydrateDashboardStateAction.dashboardState }; + case SET_STRATOS_THEME: + const setThemeAction = action as SetThemeAction; + return { + ...state, + themeKey: setThemeAction.theme ? setThemeAction.theme.key : null + }; default: return state; } diff --git a/src/frontend/packages/store/src/store.module.ts b/src/frontend/packages/store/src/store.module.ts index 386db19890..14c8160af6 100644 --- a/src/frontend/packages/store/src/store.module.ts +++ b/src/frontend/packages/store/src/store.module.ts @@ -6,6 +6,7 @@ import { ActionHistoryEffect } from './effects/action-history.effects'; import { APIEffect } from './effects/api.effects'; import { AppEffects } from './effects/app.effects'; import { AuthEffect } from './effects/auth.effects'; +import { DashboardEffect } from './effects/dashboard.effects'; import { EndpointApiError } from './effects/endpoint-api-errors.effects'; import { EndpointsEffect } from './effects/endpoint.effects'; import { MetricsEffect } from './effects/metrics.effects'; @@ -21,8 +22,8 @@ import { UpdateAppEffects } from './effects/update-app-effects'; import { UserFavoritesEffect } from './effects/user-favorites-effect'; import { UserProfileEffect } from './effects/user-profile.effects'; import { UsersRolesEffects } from './effects/users-roles.effects'; -import { AppReducersModule } from './reducers.module'; import { PipelineHttpClient } from './entity-request-pipeline/pipline-http-client.service'; +import { AppReducersModule } from './reducers.module'; @NgModule({ @@ -33,6 +34,7 @@ import { PipelineHttpClient } from './entity-request-pipeline/pipline-http-clien AppReducersModule, HttpClientModule, EffectsModule.forRoot([ + DashboardEffect, APIEffect, EndpointApiError, AuthEffect, diff --git a/src/jetstream/datastore/20191008121900_PrimaryKeys.go b/src/jetstream/datastore/20191008121900_PrimaryKeys.go new file mode 100644 index 0000000000..9e1210616f --- /dev/null +++ b/src/jetstream/datastore/20191008121900_PrimaryKeys.go @@ -0,0 +1,46 @@ +package datastore + +import ( + "database/sql" + "strings" + + "bitbucket.org/liamstask/goose/lib/goose" +) + +func init() { + RegisterMigration(20191008121900, "PrimaryKeys", func(txn *sql.Tx, conf *goose.DBConf) error { + + // Make sure all tokens have a CNSI guid + ensureTokensHaveCnsi := "UPDATE tokens SET cnsi_guid='STRATOS' WHERE token_type='uaa' and cnsi_guid IS NULL;" + _, err := txn.Exec(ensureTokensHaveCnsi) + if err != nil { + return err + } + + // Note: SQLite does not allow constraints to be added after table creation + if strings.Contains(conf.Driver.Name, "sqlite3") { + return nil + } + + // Need cnsi_guid to not be NULL in order to be able to create this primary key + addTokensPrimaryKey := "ALTER TABLE tokens ADD CONSTRAINT PK_Tokens PRIMARY KEY (user_guid, cnsi_guid, token_guid);" + _, err = txn.Exec(addTokensPrimaryKey) + if err != nil { + return err + } + + addSetupConfigPrimaryKey := "ALTER TABLE console_config ADD CONSTRAINT PK_ConsoleConfig PRIMARY KEY (uaa_endpoint, console_admin_scope);" + _, err = txn.Exec(addSetupConfigPrimaryKey) + if err != nil { + return err + } + + addConfigPrimaryKey := "ALTER TABLE config ADD CONSTRAINT PK_Config PRIMARY KEY (groupName, name);" + _, err = txn.Exec(addConfigPrimaryKey) + if err != nil { + return err + } + + return nil + }) +} diff --git a/src/jetstream/go.mod b/src/jetstream/go.mod index e7efd40bdb..12bb0de986 100644 --- a/src/jetstream/go.mod +++ b/src/jetstream/go.mod @@ -50,7 +50,7 @@ require ( github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.10 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect - github.com/mattn/go-sqlite3 v1.10.0 + github.com/mattn/go-sqlite3 v1.13.0 github.com/mholt/archiver v3.1.1+incompatible github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/moby/moby v1.13.1 // indirect @@ -72,7 +72,7 @@ require ( google.golang.org/appengine v1.5.0 // indirect gopkg.in/DATA-DOG/go-sqlmock.v1 v1.0.0-00010101000000-000000000000 gopkg.in/cheggaaa/pb.v1 v1.0.27 // indirect - gopkg.in/yaml.v2 v2.2.2 + gopkg.in/yaml.v2 v2.2.7 ) replace ( diff --git a/src/jetstream/go.sum b/src/jetstream/go.sum index 3319739e5a..351a27e60d 100644 --- a/src/jetstream/go.sum +++ b/src/jetstream/go.sum @@ -203,6 +203,8 @@ github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c= +github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU= @@ -344,6 +346,8 @@ gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/src/jetstream/plugins/metrics/cloud_foundry.go b/src/jetstream/plugins/metrics/cloud_foundry.go index f7fa8c8e84..7fd1e6e1e2 100644 --- a/src/jetstream/plugins/metrics/cloud_foundry.go +++ b/src/jetstream/plugins/metrics/cloud_foundry.go @@ -15,6 +15,7 @@ import ( var ( cellQueryWhiteList = []string{ "firehose_value_metric_rep_unhealthy_cell", + "firehose_value_metric_rep_garden_health_check_failed", "firehose_value_metric_rep_capacity_remaining_containers", "firehose_value_metric_rep_capacity_remaining_disk", "firehose_value_metric_rep_capacity_remaining_memory", @@ -205,6 +206,5 @@ func (m *MetricsSpecification) getCloudFoundryCellMetrics(c echo.Context) error } cnsiList := strings.Split(c.Request().Header.Get("x-cap-cnsi-list"), ",") - return m.makePrometheusRequest(c, cnsiList, "") } diff --git a/src/jetstream/repository/console_config/env_lookup.go b/src/jetstream/repository/console_config/env_lookup.go index 8c87844b34..d5d3997200 100644 --- a/src/jetstream/repository/console_config/env_lookup.go +++ b/src/jetstream/repository/console_config/env_lookup.go @@ -104,8 +104,14 @@ func MigrateSetupData(portal interfaces.PortalProxy, configStore Repository) err return err } - if err := migrateConfigSetting(portal.Env(), configStore, "SSO_LOGIN", strconv.FormatBool(config.UseSSO), "false"); err != nil { - return err + // Don't store previous SSO_LOGIN value if it's false. + // SSO_LOGIN was incorrectly being set in previous console config table, this was then transferred over here where the console expects + // previous values to have been explicitly set by user (and as such should take precedents over env vars) + // See https://github.com/cloudfoundry/stratos/issues/4013 + if config.UseSSO == true { + if err := migrateConfigSetting(portal.Env(), configStore, "SSO_LOGIN", strconv.FormatBool(config.UseSSO), "false"); err != nil { + return err + } } // Delete the content form the legacy table diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index 427323ef83..37e715551c 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -14,18 +14,18 @@ import ( var findAuthToken = `SELECT token_guid, auth_token, refresh_token, token_expiry, auth_type, meta_data FROM tokens - WHERE token_type = 'uaa' AND user_guid = $1` + WHERE token_type = 'uaa' AND cnsi_guid = 'STRATOS' AND user_guid = $1` var countAuthTokens = `SELECT COUNT(*) FROM tokens - WHERE token_type = 'uaa' AND user_guid = $1` + WHERE token_type = 'uaa' AND cnsi_guid = 'STRATOS' AND user_guid = $1 ` -var insertAuthToken = `INSERT INTO tokens (token_guid, user_guid, token_type, auth_token, refresh_token, token_expiry) - VALUES ($1, $2, $3, $4, $5, $6)` +var insertAuthToken = `INSERT INTO tokens (cnsi_guid, token_guid, user_guid, token_type, auth_token, refresh_token, token_expiry) + VALUES ('STRATOS', $1, $2, $3, $4, $5, $6)` var updateAuthToken = `UPDATE tokens SET auth_token = $1, refresh_token = $2, token_expiry = $3 - WHERE user_guid = $4 AND token_type = $5` + WHERE cnsi_guid = 'STRATOS' AND user_guid = $4 AND token_type = $5` var getToken = `SELECT token_guid, auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data, user_guid, linked_token FROM tokens diff --git a/src/test-e2e/application/application-delete-e2e.spec.ts b/src/test-e2e/application/application-delete-e2e.spec.ts index e866920677..c6e6b74ffb 100644 --- a/src/test-e2e/application/application-delete-e2e.spec.ts +++ b/src/test-e2e/application/application-delete-e2e.spec.ts @@ -58,7 +58,7 @@ describe('Application Delete', () => { appSummaryPage.navigateTo(); appSummaryPage.waitForPage(40000); // Open delete app dialog - const deleteApp = appSummaryPage.delete(testAppName); + const deleteApp = appSummaryPage.delete(); // App did not have a route, so there should be no routes step expect(deleteApp.hasRouteStep()).toBeFalsy(); // 1 step - np header shown @@ -71,7 +71,7 @@ describe('Application Delete', () => { }); describe('Long running tests', () => { - const timeout = 100000; + const timeout = 120000; extendE2ETestTime(timeout); beforeAll(() => { @@ -93,7 +93,7 @@ describe('Application Delete', () => { const appSummaryPage = new ApplicationBasePage(cfGuid, app.metadata.guid); appSummaryPage.navigateTo(); appSummaryPage.waitForPage(); - const deleteApp = appSummaryPage.delete(testAppName); + const deleteApp = appSummaryPage.delete(); // App did not have a route, so there should be no routes step expect(deleteApp.hasRouteStep()).toBeFalsy(); diff --git a/src/test-e2e/application/application-deploy-e2e.spec.ts b/src/test-e2e/application/application-deploy-e2e.spec.ts index 1c8e349a74..519bfe11c2 100644 --- a/src/test-e2e/application/application-deploy-e2e.spec.ts +++ b/src/test-e2e/application/application-deploy-e2e.spec.ts @@ -204,7 +204,8 @@ describe('Application Deploy -', () => { }); }); - describe('Tab Tests -', () => { + describe('Tab Tests (app status independent) -', () => { + // These tests don't rely on the app status beforeAll(() => { // Should be deployed, no web-socket open, so we can wait for angular again @@ -267,105 +268,109 @@ describe('Application Deploy -', () => { }); }); - it('App Summary', () => { - // Does app to be fully started - const appSummary = new ApplicationPageSummaryTab(appDetails.cfGuid, appDetails.appGuid); - appSummary.goToSummaryTab(); + describe('Tab Tests (app status dependent) -', () => { - appSummary.cardStatus.getStatus().then(res => { - expect(res.status).toBe('Deployed'); - expect(res.subStatus).toBe('Online'); - }); - - appSummary.cardInstances.waitForRunningInstancesText('1 / 1'); + it('App Summary', () => { + // Does app to be fully started + const appSummary = new ApplicationPageSummaryTab(appDetails.cfGuid, appDetails.appGuid); + appSummary.goToSummaryTab(); - expect(appSummary.cardUptime.getTitle()).not.toBe('Application is not running'); - expect(appSummary.cardUptime.getUptime().isDisplayed()).toBeTruthy(); - expect(appSummary.cardUptime.getUptimeText()).not.toBeNull(); + appSummary.cardStatus.getStatus().then(res => { + expect(res.status).toBe('Deployed'); + expect(res.subStatus).toBe('Online'); + }); - expect(appSummary.cardInfo.memQuota.getValue()).toBe('16 MB'); - expect(appSummary.cardInfo.diskQuota.getValue()).toBe('64 MB'); - expect(appSummary.cardInfo.appState.getValue()).toBe('STARTED'); - expect(appSummary.cardInfo.packageState.getValue()).toBe('STAGED'); - expect(appSummary.cardInfo.services.getValue()).toBe('0'); - expect(appSummary.cardInfo.routes.getValue()).toBe('1'); + appSummary.cardInstances.waitForRunningInstancesText('1 / 1'); - expect(appSummary.cardCfInfo.cf.getValue()).toBe(cfName); - expect(appSummary.cardCfInfo.org.getValue()).toBe(orgName); - expect(appSummary.cardCfInfo.space.getValue()).toBe(spaceName); + expect(appSummary.cardUptime.getTitle()).not.toBe('Application is not running'); + expect(appSummary.cardUptime.getUptime().isDisplayed()).toBeTruthy(); + expect(appSummary.cardUptime.getUptimeText()).not.toBeNull(); - expect(appSummary.cardBuildInfo.buildPack.getValue()).toBe('binary_buildpack'); - expect(appSummary.cardBuildInfo.stack.getValue()).toBe(testAppStack || defaultStack); + expect(appSummary.cardInfo.memQuota.getValue()).toBe('16 MB'); + expect(appSummary.cardInfo.diskQuota.getValue()).toBe('64 MB'); + expect(appSummary.cardInfo.appState.getValue()).toBe('STARTED'); + expect(appSummary.cardInfo.packageState.getValue()).toBe('STAGED'); + expect(appSummary.cardInfo.services.getValue()).toBe('0'); + expect(appSummary.cardInfo.routes.getValue()).toBe('1'); - appSummary.cardDeployInfo.waitForTitle('Deployment Info'); - appSummary.cardDeployInfo.github.waitUntilShown('Waiting for GitHub deployment information'); - expect(appSummary.cardDeployInfo.github.isDisplayed()).toBeTruthy(); - appSummary.cardDeployInfo.github.getValue().then(commitHash => { - expect(commitHash).toBeDefined(); - expect(commitHash.length).toBe(8); - }); + expect(appSummary.cardCfInfo.cf.getValue()).toBe(cfName); + expect(appSummary.cardCfInfo.org.getValue()).toBe(orgName); + expect(appSummary.cardCfInfo.space.getValue()).toBe(spaceName); - }); + expect(appSummary.cardBuildInfo.buildPack.getValue()).toBe('binary_buildpack'); + expect(appSummary.cardBuildInfo.stack.getValue()).toBe(testAppStack || defaultStack); - it('Instances Tab', () => { - // Does app to be fully started - const appInstances = new ApplicationPageInstancesTab(appDetails.cfGuid, appDetails.appGuid); - appInstances.goToInstancesTab(); + appSummary.cardDeployInfo.waitForTitle('Deployment Info'); + appSummary.cardDeployInfo.github.waitUntilShown('Waiting for GitHub deployment information'); + expect(appSummary.cardDeployInfo.github.isDisplayed()).toBeTruthy(); + appSummary.cardDeployInfo.github.getValue().then(commitHash => { + expect(commitHash).toBeDefined(); + expect(commitHash.length).toBe(8); + }); - appInstances.cardStatus.getStatus().then(res => { - expect(res.status).toBe('Deployed'); - expect(res.subStatus).toBe('Online'); }); - appInstances.cardInstances.waitForRunningInstancesText('1 / 1'); + it('Instances Tab', () => { + // Does app to be fully started + const appInstances = new ApplicationPageInstancesTab(appDetails.cfGuid, appDetails.appGuid); + appInstances.goToInstancesTab(); - expect(appInstances.cardUsage.getTitleElement().isPresent()).toBeFalsy(); - expect(appInstances.cardUsage.getUsageTable().isDisplayed()).toBeTruthy(); + appInstances.cardStatus.getStatus().then(res => { + expect(res.status).toBe('Deployed'); + expect(res.subStatus).toBe('Online'); + }); - expect(appInstances.list.empty.getDefault().isPresent()).toBeFalsy(); - expect(appInstances.list.table.getCell(0, 1).getText()).toBe('RUNNING'); + appInstances.cardInstances.waitForRunningInstancesText('1 / 1'); - }); + expect(appInstances.cardUsage.getTitleElement().isPresent()).toBeFalsy(); + expect(appInstances.cardUsage.getUsageTable().isDisplayed()).toBeTruthy(); - it('Routes Tab', () => { - const appRoutes = new ApplicationPageRoutesTab(appDetails.cfGuid, appDetails.appGuid); - appRoutes.goToRoutesTab(); - - expect(appRoutes.list.empty.getDefault().isPresent()).toBeFalsy(); - expect(appRoutes.list.empty.getCustom().getComponent().isPresent()).toBeFalsy(); - appRoutes.list.table.getCell(0, 1).getText().then((route: string) => { - expect(route).not.toBeNull(); - expect(route.length).toBeGreaterThan(testAppName.length); - const randomRouteStyleAppName = testAppName.replace(/[\.:]/g, ''); - expect(route.startsWith(randomRouteStyleAppName.substring(0, randomRouteStyleAppName.length - 11), 7)).toBeTruthy(); + expect(appInstances.list.empty.getDefault().isPresent()).toBeFalsy(); + expect(appInstances.list.table.getCell(0, 1).getText()).toBe('RUNNING'); + + }); + + it('Routes Tab', () => { + const appRoutes = new ApplicationPageRoutesTab(appDetails.cfGuid, appDetails.appGuid); + appRoutes.goToRoutesTab(); + + expect(appRoutes.list.empty.getDefault().isPresent()).toBeFalsy(); + expect(appRoutes.list.empty.getCustom().getComponent().isPresent()).toBeFalsy(); + appRoutes.list.table.getCell(0, 1).getText().then((route: string) => { + expect(route).not.toBeNull(); + expect(route.length).toBeGreaterThan(testAppName.length); + const randomRouteStyleAppName = testAppName.replace(/[\.:]/g, ''); + expect(route.startsWith(randomRouteStyleAppName.substring(0, randomRouteStyleAppName.length - 11), 7)).toBeTruthy(); + }); + appRoutes.list.table.getCell(0, 2).getText().then((tcpRoute: string) => { + expect(tcpRoute).not.toBeNull(); + expect(tcpRoute).toBe('highlight_off'); + }); }); - appRoutes.list.table.getCell(0, 2).getText().then((tcpRoute: string) => { - expect(tcpRoute).not.toBeNull(); - expect(tcpRoute).toBe('highlight_off'); + + it('Events Tab', () => { + // Does app to be fully started + const appEvents = new ApplicationPageEventsTab(appDetails.cfGuid, appDetails.appGuid); + appEvents.goToEventsTab(); + + expect(appEvents.list.empty.isDisplayed()).toBeFalsy(); + expect(appEvents.list.isTableView()).toBeTruthy(); + expect(appEvents.list.getTotalResults()).toBeGreaterThanOrEqual(3); + // Ensure that the earliest events are at the top + appEvents.list.table.toggleSort('Timestamp'); + + const currentUser = e2e.secrets.getDefaultCFEndpoint().creds.nonAdmin.username; + // Create + expect(appEvents.list.table.getCell(0, 1).getText()).toBe('audit\napp\ncreate'); + expect(appEvents.list.table.getCell(0, 2).getText()).toBe(`person\n${currentUser}`); + // Map Route + expect(appEvents.list.table.getCell(1, 1).getText()).toBe('audit\napp\nmap-route'); + expect(appEvents.list.table.getCell(1, 2).getText()).toBe(`person\n${currentUser}`); + // Update (route) + expect(appEvents.list.table.getCell(2, 1).getText()).toBe('audit\napp\nupdate'); + expect(appEvents.list.table.getCell(2, 2).getText()).toBe(`person\n${currentUser}`); }); - }); - it('Events Tab', () => { - // Does app to be fully started - const appEvents = new ApplicationPageEventsTab(appDetails.cfGuid, appDetails.appGuid); - appEvents.goToEventsTab(); - - expect(appEvents.list.empty.isDisplayed()).toBeFalsy(); - expect(appEvents.list.isTableView()).toBeTruthy(); - expect(appEvents.list.getTotalResults()).toBeGreaterThanOrEqual(2); - // Ensure that the earliest events are at the top - appEvents.list.table.toggleSort('Timestamp'); - - const currentUser = e2e.secrets.getDefaultCFEndpoint().creds.nonAdmin.username; - // Create - expect(appEvents.list.table.getCell(0, 1).getText()).toBe('audit\napp\ncreate'); - expect(appEvents.list.table.getCell(0, 2).getText()).toBe(`person\n${currentUser}`); - // Map Route - expect(appEvents.list.table.getCell(1, 1).getText()).toBe('audit\napp\nmap-route'); - expect(appEvents.list.table.getCell(1, 2).getText()).toBe(`person\n${currentUser}`); - // Update (route) - expect(appEvents.list.table.getCell(2, 1).getText()).toBe('audit\napp\nupdate'); - expect(appEvents.list.table.getCell(2, 2).getText()).toBe(`person\n${currentUser}`); }); describe('Instance scaling', () => { diff --git a/src/test-e2e/application/po/application-page-variables.po.ts b/src/test-e2e/application/po/application-page-variables.po.ts index b754ed6a30..679901c539 100644 --- a/src/test-e2e/application/po/application-page-variables.po.ts +++ b/src/test-e2e/application/po/application-page-variables.po.ts @@ -32,7 +32,7 @@ export class ApplicationPageVariablesTab extends ApplicationBasePage { this.list.table.openRowActionMenuByIndex(rowIndex).clickItem('Delete'); const confirm = new ConfirmDialogComponent(); confirm.waitUntilShown(); - expect(confirm.getMessage()).toBe(`Are you sure you want to delete '${variableName}'?`); + confirm.waitForMessage(`Are you sure you want to delete '${variableName}'?`); confirm.confirm(); return this.list.table.waitUntilNotBusy(); } diff --git a/src/test-e2e/application/po/application-page.po.ts b/src/test-e2e/application/po/application-page.po.ts index cb32b19887..07b2a5ad17 100644 --- a/src/test-e2e/application/po/application-page.po.ts +++ b/src/test-e2e/application/po/application-page.po.ts @@ -68,7 +68,7 @@ export class ApplicationBasePage extends CFPage { return this.header.getTitleText(); } - public delete(appName: string): DeleteApplication { + public delete(): DeleteApplication { const deleteApp = new DeleteApplication(this.cfGuid, this.appGuid); this.subHeader.clickIconButton('delete'); deleteApp.waitForPage(); diff --git a/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts b/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts index 36ef6c6792..f7b6095de0 100644 --- a/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts +++ b/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts @@ -4,6 +4,7 @@ import { e2e } from '../../e2e'; import { E2EConfigCloudFoundry } from '../../e2e.types'; import { CFHelpers } from '../../helpers/cf-helpers'; import { ConsoleUserType, E2EHelpers } from '../../helpers/e2e-helpers'; +import { extendE2ETestTime } from '../../helpers/extend-test-helpers'; import { CFPage } from '../../po/cf-page.po'; import { ListComponent } from '../../po/list.po'; import { MetaCardTitleType } from '../../po/meta-card.po'; @@ -42,36 +43,45 @@ describe('CF - Space Level -', () => { } function navToPage() { - const page = new CFPage(); - page.sideNav.goto(SideNavMenuItem.CloudFoundry); - CfTopLevelPage.detect().then(cfPage => { - cfPage.waitForPageOrChildPage(); - cfPage.loadingIndicator.waitUntilNotShown(); - cfPage.goToOrgTab(); - - // Find the Org and click on it - const list = new ListComponent(); - list.cards.findCardByTitle(defaultCf.testOrg, MetaCardTitleType.CUSTOM, true).then(card => { - expect(card).toBeDefined(); - card.click(); - }); - CfOrgLevelPage.detect().then(orgPage => { - orgPage.waitForPageOrChildPage(); - orgPage.loadingIndicator.waitUntilNotShown(); - orgPage.goToSpacesTab(); - - // Find the Space and click on it - const spaceList = new ListComponent(); - spaceList.cards.findCardByTitle(defaultCf.testSpace, MetaCardTitleType.CUSTOM, true).then(card => { - expect(card).toBeDefined(); - card.click(); - }); - CfSpaceLevelPage.detect().then(s => { - spacePage = s; - spacePage.waitForPageOrChildPage(); - spacePage.loadingIndicator.waitUntilNotShown(); + describe('', () => { + + // Allow additional time for navigation + extendE2ETestTime(70000); + + // Tests that the given users can navigate through the org and space lists + it('Nav to Space', () => { + const page = new CFPage(); + page.sideNav.goto(SideNavMenuItem.CloudFoundry); + CfTopLevelPage.detect().then(cfPage => { + cfPage.waitForPageOrChildPage(); + cfPage.loadingIndicator.waitUntilNotShown(); + cfPage.goToOrgTab(); + + // Find the Org and click on it + const list = new ListComponent(); + list.cards.findCardByTitle(defaultCf.testOrg, MetaCardTitleType.CUSTOM, true).then(card => { + expect(card).toBeDefined(); + card.click(); + }); + CfOrgLevelPage.detect().then(orgPage => { + orgPage.waitForPageOrChildPage(); + orgPage.loadingIndicator.waitUntilNotShown(); + orgPage.goToSpacesTab(); + + // Find the Space and click on it + const spaceList = new ListComponent(); + spaceList.cards.findCardByTitle(defaultCf.testSpace, MetaCardTitleType.CUSTOM, true).then(card => { + expect(card).toBeDefined(); + card.click(); + }); + CfSpaceLevelPage.detect().then(s => { + spacePage = s; + spacePage.waitForPageOrChildPage(); + spacePage.loadingIndicator.waitUntilNotShown(); + }); + + }); }); - }); }); } @@ -106,7 +116,7 @@ describe('CF - Space Level -', () => { }); describe('Basic Tests -', () => { - beforeEach(navToPage); + navToPage(); it('Breadcrumb', testBreadcrumb); @@ -134,7 +144,7 @@ describe('CF - Space Level -', () => { }); describe('Basic Tests -', () => { - beforeEach(navToPage); + navToPage(); it('Breadcrumb', testBreadcrumb); diff --git a/src/test-e2e/po/cf-users-list.po.ts b/src/test-e2e/po/cf-users-list.po.ts index e9f121aefd..b35193aae5 100644 --- a/src/test-e2e/po/cf-users-list.po.ts +++ b/src/test-e2e/po/cf-users-list.po.ts @@ -32,9 +32,7 @@ export class UserRoleChip extends ChipComponent { remove(): promise.Promise { this.getCross().click(); const confirm = new ConfirmDialogComponent(); - confirm.getMessage().then(message => { - expect(message).toContain(this.roleText); - }); + confirm.waitForMessage(this.roleText); confirm.confirm(); confirm.waitUntilNotShown('Confirmation dialog'); return this.waitUntilNotShown('User Role Chip still shown: ' + this.roleText); diff --git a/src/test-e2e/po/confirm-dialog.ts b/src/test-e2e/po/confirm-dialog.ts index dd99eea3eb..60792479b5 100644 --- a/src/test-e2e/po/confirm-dialog.ts +++ b/src/test-e2e/po/confirm-dialog.ts @@ -1,8 +1,10 @@ -import { by, element, promise } from 'protractor'; +import { browser, by, element, ElementFinder, promise, protractor } from 'protractor'; import { Component } from './component.po'; import { FormComponent } from './form.po'; +const until = protractor.ExpectedConditions; + export class DialogButton { index: number; label: string; @@ -69,15 +71,23 @@ export class ConfirmDialogComponent extends Component { return this.locator.element(by.className('confirm-dialog__header-title')).getText(); } + getMessageElement(): ElementFinder { + return this.locator.element(by.className('confirm-dialog__message')); + } + getMessage(): promise.Promise { - return this.locator.element(by.className('confirm-dialog__message')).getText(); + return this.getMessageElement().getText(); + } + + waitForMessage(message: string): promise.Promise { + return browser.wait(until.textToBePresentInElement(this.getMessageElement(), message), 5000); } // Get metadata for all of the fields in the form getButtons(): promise.Promise { return this.locator.all(by.tagName('button')).map((elm, index) => { return { - index: index, + index, label: elm.getText(), class: elm.getAttribute('class'), isWarning: elm.getAttribute('class').then(v => v.indexOf('mat-warn') >= 0),