From 5a1279aa253df3c877c675ddf730f081227f3237 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 25 Mar 2020 21:41:37 +0800 Subject: [PATCH 001/280] Minimum changes required to get it running on Linux --- helpers/shared-to-shared-migrate.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index a3878c909a..f1ba5491e5 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -144,16 +144,16 @@ fi SECRETS=/tmp/${NAMESPACE}-migration.yaml oc -n ${NAMESPACE} get secret mariadb-servicebroker-credentials -o yaml > $SECRETS -DB_NETWORK_SERVICE=$(cat $SECRETS | shyaml get-value data.DB_HOST | base64 -D) +DB_NETWORK_SERVICE=$(cat $SECRETS | shyaml get-value data.DB_HOST | base64 -d) if cat ${SECRETS} | grep DB_READREPLICA_HOSTS > /dev/null ; then - DB_READREPLICA_HOSTS=$(cat $SECRETS | shyaml get-value data.DB_READREPLICA_HOSTS | base64 -D) + DB_READREPLICA_HOSTS=$(cat $SECRETS | shyaml get-value data.DB_READREPLICA_HOSTS | base64 -d) else DB_READREPLICA_HOSTS="" fi -DB_USER=$(cat $SECRETS | shyaml get-value data.DB_USER | base64 -D) -DB_PASSWORD=$(cat $SECRETS | shyaml get-value data.DB_PASSWORD | base64 -D) -DB_NAME=$(cat $SECRETS | shyaml get-value data.DB_NAME | base64 -D) -DB_PORT=$(cat $SECRETS | shyaml get-value data.DB_PORT | base64 -D) +DB_USER=$(cat $SECRETS | shyaml get-value data.DB_USER | base64 -d) +DB_PASSWORD=$(cat $SECRETS | shyaml get-value data.DB_PASSWORD | base64 -d) +DB_NAME=$(cat $SECRETS | shyaml get-value data.DB_NAME | base64 -d) +DB_PORT=$(cat $SECRETS | shyaml get-value data.DB_PORT | base64 -d) shw_grey "================================================" shw_grey " DB_NETWORK_SERVICE=$DB_NETWORK_SERVICE" @@ -238,7 +238,7 @@ shw_info "> Testing the route https://${ROUTE}/?${TIMESTAMP}" shw_info "================================================" curl -skLIXGET "https://${ROUTE}/?${TIMESTAMP}" \ -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36" \ - --cookie "NO_CACHE=1" | grep -E "HTTP|Cache|Location|LAGOON" || TRUE + --cookie "NO_CACHE=1" | grep -E "HTTP|Cache|Location|LAGOON" || true shw_grey "================================================" shw_grey "" From cb11357fd759c92cc7a7698d902cbddb63eeec8f Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 26 Mar 2020 22:16:01 +0800 Subject: [PATCH 002/280] Rework the color functions to appease shellcheck --- helpers/shared-to-shared-migrate.sh | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index f1ba5491e5..2a6965a167 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -63,19 +63,34 @@ TIMESTAMP=$(date +%s) # Colours. shw_grey () { - echo $(tput bold)$(tput setaf 0) $@ $(tput sgr 0) + tput bold + tput setaf 0 + echo "$@" + tput sgr0 } shw_norm () { - echo $(tput bold)$(tput setaf 9) $@ $(tput sgr 0) + tput bold + tput setaf 9 + echo "$@" + tput sgr0 } shw_info () { - echo $(tput bold)$(tput setaf 4) $@ $(tput sgr 0) + tput bold + tput setaf 4 + echo "$@" + tput sgr0 } shw_warn () { - echo $(tput bold)$(tput setaf 2) $@ $(tput sgr 0) + tput bold + tput setaf 2 + echo "$@" + tput sgr0 } shw_err () { - echo $(tput bold)$(tput setaf 1) $@ $(tput sgr 0) + tput bold + tput setaf 1 + echo "$@" + tput sgr0 } # Parse input arguments. From 5421ae496b124df36b8e8d73d5475e05b0f0cf10 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 26 Mar 2020 22:19:58 +0800 Subject: [PATCH 003/280] Various quoting and test style fixes --- helpers/shared-to-shared-migrate.sh | 50 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 2a6965a167..8400d69afe 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -50,9 +50,6 @@ # set -euo pipefail -# Reset in case getopts has been used previously in the shell. -OPTIND=1 - # Initialize our own variables: SOURCE_CLUSTER="" DESTINATION_CLUSTER="" @@ -151,13 +148,13 @@ if [ ! -f "$CONF_FILE" ]; then exit 2 fi -if [ ! -z "${DRY_RUN}" ] ; then +if [ "$DRY_RUN" ] ; then shw_warn "Dry run is enabled, so no network service changes will take place." fi # Load the DBaaS credentials for the project SECRETS=/tmp/${NAMESPACE}-migration.yaml -oc -n ${NAMESPACE} get secret mariadb-servicebroker-credentials -o yaml > $SECRETS +oc -n "$NAMESPACE" get secret mariadb-servicebroker-credentials -o yaml > "$SECRETS" DB_NETWORK_SERVICE=$(cat $SECRETS | shyaml get-value data.DB_HOST | base64 -d) if cat ${SECRETS} | grep DB_READREPLICA_HOSTS > /dev/null ; then @@ -176,6 +173,7 @@ shw_grey " DB_READREPLICA_HOSTS=$DB_READREPLICA_HOSTS" shw_grey " DB_USER=$DB_USER" shw_grey " DB_PASSWORD=$DB_PASSWORD" shw_grey " DB_NAME=$DB_NAME" +shw_grey " DB_PORT=$DB_PORT" shw_grey "================================================" # Ensure there is a database in the destination. @@ -193,41 +191,41 @@ shw_info "================================================" mysql --defaults-file="$CONF_FILE" -e "SELECT * FROM mysql.db WHERE Db = '${DB_NAME}'\G;" # Dump the database inside the CLI pod. -POD=$(oc -n ${NAMESPACE} get pods -o json --show-all=false -l service=cli | jq -r '.items[].metadata.name') -shw_info "> Dumping database ${DB_NAME} on pod ${POD} on host ${DB_NETWORK_SERVICE}" +POD=$(oc -n "$NAMESPACE" get pods -o json --show-all=false -l service=cli | jq -r '.items[].metadata.name') +shw_info "> Dumping database $DB_NAME on pod $POD on host $DB_NETWORK_SERVICE" shw_info "================================================" -oc -n ${NAMESPACE} exec ${POD} -- bash -c "time mysqldump -h ${DB_NETWORK_SERVICE} -u ${DB_USER} -p${DB_PASSWORD} ${DB_NAME} > /tmp/migration.sql" -oc -n ${NAMESPACE} exec ${POD} -- ls -lath /tmp/migration.sql || exit 1 -oc -n ${NAMESPACE} exec ${POD} -- head -n 5 /tmp/migration.sql -oc -n ${NAMESPACE} exec ${POD} -- tail -n 5 /tmp/migration.sql || exit 1 +oc -n "$NAMESPACE" exec "$POD" -- bash -c "time mysqldump -h '$DB_NETWORK_SERVICE' -u '$DB_USER' -p'$DB_PASSWORD' '$DB_NAME' > /tmp/migration.sql" +oc -n "$NAMESPACE" exec "$POD" -- ls -lath /tmp/migration.sql || exit 1 +oc -n "$NAMESPACE" exec "$POD" -- head -n 5 /tmp/migration.sql +oc -n "$NAMESPACE" exec "$POD" -- tail -n 5 /tmp/migration.sql || exit 1 shw_norm "> Dump is done" shw_norm "================================================" # Import to new database. shw_info "> Importing the dump into ${DESTINATION_CLUSTER}" shw_info "================================================" -oc -n ${NAMESPACE} exec ${POD} -- bash -c "time mysql -h ${DESTINATION_CLUSTER} -u ${DB_USER} -p${DB_PASSWORD} ${DB_NAME} < /tmp/migration.sql" -oc -n ${NAMESPACE} exec ${POD} -- bash -c "rm /tmp/migration.sql" +oc -n "$NAMESPACE" exec "$POD" -- bash -c "time mysql -h '$DESTINATION_CLUSTER' -u '$DB_USER' -p'$DB_PASSWORD' '$DB_NAME' < /tmp/migration.sql" +oc -n "$NAMESPACE" exec "$POD" -- bash -c "rm /tmp/migration.sql" shw_norm "> Import is done" shw_norm "================================================" # Alter the network service(s). -shw_info "> Altering the Network Service ${DB_NETWORK_SERVICE} to point at ${DESTINATION_CLUSTER}" +shw_info "> Altering the Network Service $DB_NETWORK_SERVICE to point at $DESTINATION_CLUSTER" shw_info "================================================" -oc -n ${NAMESPACE} get svc/${DB_NETWORK_SERVICE} -o yaml > /tmp/${NAMESPACE}-svc.yaml -if [ -z "${DRY_RUN}" ] ; then - oc -n ${NAMESPACE} patch svc/${DB_NETWORK_SERVICE} -p "{\"spec\":{\"externalName\": \"${DESTINATION_CLUSTER}\"}}" +oc -n "$NAMESPACE" get "svc/$DB_NETWORK_SERVICE" -o yaml > "/tmp/$NAMESPACE-svc.yaml" +if [ -z "$DRY_RUN" ] ; then + oc -n "$NAMESPACE" patch "svc/$DB_NETWORK_SERVICE" -p "{\"spec\":{\"externalName\": \"${DESTINATION_CLUSTER}\"}}" else echo "**DRY RUN**" fi -if [ ! -z "${DB_READREPLICA_HOSTS}" ]; then - shw_info "> Altering the Network Service ${DB_READREPLICA_HOSTS} to point at ${REPLICA_CLUSTER}" +if [ "$DB_READREPLICA_HOSTS" ]; then + shw_info "> Altering the Network Service $DB_READREPLICA_HOSTS to point at $REPLICA_CLUSTER" shw_info "================================================" - oc -n ${NAMESPACE} get svc/${DB_READREPLICA_HOSTS} -o yaml > /tmp/${NAMESPACE}-svc-replica.yaml + oc -n "$NAMESPACE" get "svc/$DB_READREPLICA_HOSTS" -o yaml > "/tmp/$NAMESPACE-svc-replica.yaml" ORIGINAL_DB_READREPLICA_HOSTS=$(cat /tmp/${NAMESPACE}-svc-replica.yaml | shyaml get-value spec.externalName) - if [ -z "${DRY_RUN}" ] ; then - oc -n ${NAMESPACE} patch svc/${DB_READREPLICA_HOSTS} -p "{\"spec\":{\"externalName\": \"${REPLICA_CLUSTER}\"}}" + if [ -z "$DRY_RUN" ] ; then + oc -n "$NAMESPACE" patch "svc/$DB_READREPLICA_HOSTS" -p '{"spec":{"externalName": "'"$REPLICA_CLUSTER"'"}}' else echo "**DRY RUN**" fi @@ -240,15 +238,15 @@ sleep 1 # Verify the correct RDS cluster. shw_info "> Output the RDS cluster that Drush is connecting to" shw_info "================================================" -oc -n ${NAMESPACE} exec ${POD} -- bash -c "drush sqlq 'SELECT @@aurora_server_id;'" +oc -n "$NAMESPACE" exec "$POD" -- bash -c "drush sqlq 'SELECT @@aurora_server_id;'" # Drush status. shw_info "> Drush status" shw_info "================================================" -oc -n ${NAMESPACE} exec ${POD} -- bash -c "drush status" +oc -n "$NAMESPACE" exec "$POD" -- bash -c "drush status" # Get routes, and ensure a cache bust works. -ROUTE=$(oc -n ${NAMESPACE} get routes -o json | jq --raw-output '.items[0].spec.host') +ROUTE=$(oc -n "$NAMESPACE" get routes -o json | jq --raw-output '.items[0].spec.host') shw_info "> Testing the route https://${ROUTE}/?${TIMESTAMP}" shw_info "================================================" curl -skLIXGET "https://${ROUTE}/?${TIMESTAMP}" \ @@ -260,7 +258,7 @@ shw_grey "" shw_grey "In order to rollback this change, edit the Network Service(s) like so:" shw_grey "" shw_grey "oc -n ${NAMESPACE} patch svc/${DB_NETWORK_SERVICE} -p \"{\\\"spec\\\":{\\\"externalName': \\\"${SOURCE_CLUSTER}\\\"}}\"" -if [ ! -z "${DB_READREPLICA_HOSTS}" ]; then +if [ "$DB_READREPLICA_HOSTS" ]; then shw_grey "oc -n ${NAMESPACE} patch svc/${DB_READREPLICA_HOSTS} -p \"{\\\"spec\\\":{\\\"externalName': \\\"${ORIGINAL_DB_READREPLICA_HOSTS}\\\"}}\"" fi From 5396cc7c7ec8a1554afe6f1617add701d7f942f4 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 26 Mar 2020 22:30:23 +0800 Subject: [PATCH 004/280] Remove check for unused local my.cnf for SOURCE_CLUSTER --- helpers/shared-to-shared-migrate.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 8400d69afe..a690ac6bf5 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -136,12 +136,6 @@ for util in oc jq mysql shyaml; do fi done -CONF_FILE=${HOME}/.my.cnf-${SOURCE_CLUSTER} -if [ ! -f "$CONF_FILE" ]; then - shw_err "ERROR: please create $CONF_FILE so I can know how to connect to ${SOURCE_CLUSTER}" - exit 2 -fi - CONF_FILE=${HOME}/.my.cnf-${DESTINATION_CLUSTER} if [ ! -f "$CONF_FILE" ]; then shw_err "ERROR: please create $CONF_FILE so I can know how to connect to ${DESTINATION_CLUSTER}" From ea125046ba2487305976266d0b70d23e332c5ab3 Mon Sep 17 00:00:00 2001 From: Thom Toogood Date: Tue, 31 Mar 2020 19:16:22 +1300 Subject: [PATCH 005/280] Remove reference to patches directory as it's been commented out in drupal8-composer-mariadb. --- docs/using_lagoon/drupal/lagoonize.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/using_lagoon/drupal/lagoonize.md b/docs/using_lagoon/drupal/lagoonize.md index 8be7a68f45..83b99a36f5 100644 --- a/docs/using_lagoon/drupal/lagoonize.md +++ b/docs/using_lagoon/drupal/lagoonize.md @@ -11,7 +11,6 @@ You find [these Files in our GitHub repository](https://github.com/amazeeio/lago - `sites/default/*` - These .php and .yml files teach Drupal how to communicate with Lagoon containers both locally and in production. It also provides an easy system for specific overrides in development and production environments. Unlike other Drupal hosting systems, Lagoon never ever injects Drupal settings files into your Drupal. Therefore you can edit them however you like. Like all other files, they contain sensible defaults and some commented parts. - `drush/aliases.drushrc.php` - These files are specific to Drush and tell Drush how to talk to the Lagoon GraphQL API in order to learn about all Site Aliases there are. - `drush/drushrc.php` - Some sensible defaults for Drush commands. -- Add `patches` directory if you choose [drupal8-composer-mariadb](../drupal/services/mariadb.md). ### Update your `.gitignore` Settings From ea08f3be4db879450fe3a1249e93707a71edd081 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 1 Apr 2020 22:10:49 +0800 Subject: [PATCH 006/280] Drop dependency on shyaml and base64 * shyaml is a bit yukky * base64 uses -D on macOS and -d on linux :'( --- helpers/shared-to-shared-migrate.sh | 40 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index a690ac6bf5..e5ec709c4d 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -129,7 +129,7 @@ shw_grey " REPLICA_CLUSTER=$REPLICA_CLUSTER" shw_grey " NAMESPACE=$NAMESPACE" shw_grey "================================================" -for util in oc jq mysql shyaml; do +for util in oc jq mysql; do if ! command -v ${util} > /dev/null; then shw_err "Please install ${util}" exit 1 @@ -147,19 +147,18 @@ if [ "$DRY_RUN" ] ; then fi # Load the DBaaS credentials for the project -SECRETS=/tmp/${NAMESPACE}-migration.yaml -oc -n "$NAMESPACE" get secret mariadb-servicebroker-credentials -o yaml > "$SECRETS" +SECRETS=$(oc -n "$NAMESPACE" get secret mariadb-servicebroker-credentials -o json) -DB_NETWORK_SERVICE=$(cat $SECRETS | shyaml get-value data.DB_HOST | base64 -d) -if cat ${SECRETS} | grep DB_READREPLICA_HOSTS > /dev/null ; then - DB_READREPLICA_HOSTS=$(cat $SECRETS | shyaml get-value data.DB_READREPLICA_HOSTS | base64 -d) +DB_NETWORK_SERVICE=$(echo "$SECRETS" | jq -er '.data.DB_HOST | @base64d') +if echo "$SECRETS" | grep -q DB_READREPLICA_HOSTS ; then + DB_READREPLICA_HOSTS=$(echo "$SECRETS" | jq -er '.data.DB_READREPLICA_HOSTS | @base64d') else DB_READREPLICA_HOSTS="" fi -DB_USER=$(cat $SECRETS | shyaml get-value data.DB_USER | base64 -d) -DB_PASSWORD=$(cat $SECRETS | shyaml get-value data.DB_PASSWORD | base64 -d) -DB_NAME=$(cat $SECRETS | shyaml get-value data.DB_NAME | base64 -d) -DB_PORT=$(cat $SECRETS | shyaml get-value data.DB_PORT | base64 -d) +DB_USER=$(echo "$SECRETS" | jq -er '.data.DB_USER | @base64d') +DB_PASSWORD=$(echo "$SECRETS" | jq -er '.data.DB_PASSWORD | @base64d') +DB_NAME=$(echo "$SECRETS" | jq -er '.data.DB_NAME | @base64d') +DB_PORT=$(echo "$SECRETS" | jq -er '.data.DB_PORT | @base64d') shw_grey "================================================" shw_grey " DB_NETWORK_SERVICE=$DB_NETWORK_SERVICE" @@ -171,7 +170,7 @@ shw_grey " DB_PORT=$DB_PORT" shw_grey "================================================" # Ensure there is a database in the destination. -shw_info "> Setting up the MySQL bits" +shw_info "> Preparing Database, User, and permissions on destination" shw_info "================================================" CONF_FILE=${HOME}/.my.cnf-${DESTINATION_CLUSTER} mysql --defaults-file="$CONF_FILE" -se "CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\`;" @@ -185,7 +184,7 @@ shw_info "================================================" mysql --defaults-file="$CONF_FILE" -e "SELECT * FROM mysql.db WHERE Db = '${DB_NAME}'\G;" # Dump the database inside the CLI pod. -POD=$(oc -n "$NAMESPACE" get pods -o json --show-all=false -l service=cli | jq -r '.items[].metadata.name') +POD=$(oc -n "$NAMESPACE" get pods -o json --show-all=false -l service=cli | jq -er '.items[].metadata.name') shw_info "> Dumping database $DB_NAME on pod $POD on host $DB_NETWORK_SERVICE" shw_info "================================================" oc -n "$NAMESPACE" exec "$POD" -- bash -c "time mysqldump -h '$DB_NETWORK_SERVICE' -u '$DB_USER' -p'$DB_PASSWORD' '$DB_NAME' > /tmp/migration.sql" @@ -207,21 +206,20 @@ shw_norm "================================================" # Alter the network service(s). shw_info "> Altering the Network Service $DB_NETWORK_SERVICE to point at $DESTINATION_CLUSTER" shw_info "================================================" -oc -n "$NAMESPACE" get "svc/$DB_NETWORK_SERVICE" -o yaml > "/tmp/$NAMESPACE-svc.yaml" -if [ -z "$DRY_RUN" ] ; then - oc -n "$NAMESPACE" patch "svc/$DB_NETWORK_SERVICE" -p "{\"spec\":{\"externalName\": \"${DESTINATION_CLUSTER}\"}}" -else +oc -n "$NAMESPACE" get "svc/$DB_NETWORK_SERVICE" -o json --export > "/tmp/$NAMESPACE-svc.json" +if [ "$DRY_RUN" ] ; then echo "**DRY RUN**" +else + oc -n "$NAMESPACE" patch "svc/$DB_NETWORK_SERVICE" -p "{\"spec\":{\"externalName\": \"${DESTINATION_CLUSTER}\"}}" fi if [ "$DB_READREPLICA_HOSTS" ]; then shw_info "> Altering the Network Service $DB_READREPLICA_HOSTS to point at $REPLICA_CLUSTER" shw_info "================================================" - oc -n "$NAMESPACE" get "svc/$DB_READREPLICA_HOSTS" -o yaml > "/tmp/$NAMESPACE-svc-replica.yaml" - ORIGINAL_DB_READREPLICA_HOSTS=$(cat /tmp/${NAMESPACE}-svc-replica.yaml | shyaml get-value spec.externalName) - if [ -z "$DRY_RUN" ] ; then - oc -n "$NAMESPACE" patch "svc/$DB_READREPLICA_HOSTS" -p '{"spec":{"externalName": "'"$REPLICA_CLUSTER"'"}}' - else + ORIGINAL_DB_READREPLICA_HOSTS=$(oc -n "$NAMESPACE" get "svc/$DB_READREPLICA_HOSTS" -o json --export | tee "/tmp/$NAMESPACE-svc-replica.json" | jq -er '.spec.externalName') + if [ "$DRY_RUN" ] ; then echo "**DRY RUN**" + else + oc -n "$NAMESPACE" patch "svc/$DB_READREPLICA_HOSTS" -p '{"spec":{"externalName": "'"$REPLICA_CLUSTER"'"}}' fi fi From 227bc1a395f841ab7a0c7d57251ed224b8615b87 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 1 Apr 2020 22:18:14 +0800 Subject: [PATCH 007/280] Remove unused SOURCE argument This also fixes an issue where the command to roll back to the original service was incorrect if you specified a source read-only endpoint. --- helpers/shared-to-shared-migrate.sh | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index e5ec709c4d..003b2ac90f 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -4,7 +4,7 @@ # What this script is for # ======================= # This script will migrate a database user, access, database and contents from -# a source cluster to a destination cluster. +# an existing cluster to a destination cluster. # # At the moment, this is geared towards the Ansible Service Broker, but likely # can be modified in the future to work with the DBaaS operator. @@ -18,9 +18,9 @@ # ============ # * You are logged into OpenShift CLI and have access to the NAMESPACE you want # to migrate. -# * You have a `.my.cnf` file for the source and desintation database clusters. -# * If your database clusters are not directly accessible, then you have -# created SSH tunnels to expose them on a local port. +# * You have a `.my.cnf` file for the desintation database cluster. +# * If your destination database cluster is not directly accessible, then you +# have created SSH tunnels to expose them on a local port. # # How to get your existing ASB root credentials # ============================================= @@ -42,7 +42,6 @@ # Example commands # ================ # ./helpers/shared-to-shared-migrate.sh \ -# --source shared-cluster.cluster-banana.ap-southeast-2.rds.amazonaws.com \ # --destination shared-cluster.cluster-apple.ap-southeast-2.rds.amazonaws.com \ # --replica shared-cluster.cluster-r0-apple.ap-southeast-2.rds.amazonaws.com \ # --namespace NAMESPACE \ @@ -51,7 +50,6 @@ set -euo pipefail # Initialize our own variables: -SOURCE_CLUSTER="" DESTINATION_CLUSTER="" REPLICA_CLUSTER="" NAMESPACE="" @@ -95,11 +93,6 @@ while [[ $# -gt 0 ]] ; do key="$1" case $key in - -s|--source) - SOURCE_CLUSTER="$2" - shift # past argument - shift # past value - ;; -d|--destination) DESTINATION_CLUSTER="$2" shift # past argument @@ -123,7 +116,6 @@ while [[ $# -gt 0 ]] ; do done shw_grey "================================================" -shw_grey " SOURCE_CLUSTER=$SOURCE_CLUSTER" shw_grey " DESTINATION_CLUSTER=$DESTINATION_CLUSTER" shw_grey " REPLICA_CLUSTER=$REPLICA_CLUSTER" shw_grey " NAMESPACE=$NAMESPACE" @@ -206,7 +198,7 @@ shw_norm "================================================" # Alter the network service(s). shw_info "> Altering the Network Service $DB_NETWORK_SERVICE to point at $DESTINATION_CLUSTER" shw_info "================================================" -oc -n "$NAMESPACE" get "svc/$DB_NETWORK_SERVICE" -o json --export > "/tmp/$NAMESPACE-svc.json" +ORIGINAL_DB_HOST=$(oc -n "$NAMESPACE" get "svc/$DB_NETWORK_SERVICE" -o json --export | tee "/tmp/$NAMESPACE-svc.json" | jq -er '.spec.externalName') if [ "$DRY_RUN" ] ; then echo "**DRY RUN**" else @@ -249,7 +241,7 @@ shw_grey "================================================" shw_grey "" shw_grey "In order to rollback this change, edit the Network Service(s) like so:" shw_grey "" -shw_grey "oc -n ${NAMESPACE} patch svc/${DB_NETWORK_SERVICE} -p \"{\\\"spec\\\":{\\\"externalName': \\\"${SOURCE_CLUSTER}\\\"}}\"" +shw_grey "oc -n ${NAMESPACE} patch svc/${DB_NETWORK_SERVICE} -p \"{\\\"spec\\\":{\\\"externalName': \\\"${ORIGINAL_DB_HOST}\\\"}}\"" if [ "$DB_READREPLICA_HOSTS" ]; then shw_grey "oc -n ${NAMESPACE} patch svc/${DB_READREPLICA_HOSTS} -p \"{\\\"spec\\\":{\\\"externalName': \\\"${ORIGINAL_DB_READREPLICA_HOSTS}\\\"}}\"" fi From 2e1fe9d901208aa1b95511069004aad278049a4f Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 1 Apr 2020 22:24:01 +0800 Subject: [PATCH 008/280] Use jq exit code to kill the script if the query fails --- helpers/shared-to-shared-migrate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 003b2ac90f..d25a9543b0 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -230,7 +230,7 @@ shw_info "================================================" oc -n "$NAMESPACE" exec "$POD" -- bash -c "drush status" # Get routes, and ensure a cache bust works. -ROUTE=$(oc -n "$NAMESPACE" get routes -o json | jq --raw-output '.items[0].spec.host') +ROUTE=$(oc -n "$NAMESPACE" get routes -o json | jq -er '.items[0].spec.host') shw_info "> Testing the route https://${ROUTE}/?${TIMESTAMP}" shw_info "================================================" curl -skLIXGET "https://${ROUTE}/?${TIMESTAMP}" \ From 12a46dc56198b430cf6eda93a7dfbf8f13da4b26 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 1 Apr 2020 22:39:00 +0800 Subject: [PATCH 009/280] Simplify string escapes --- helpers/shared-to-shared-migrate.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index d25a9543b0..e9eba11f0e 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -241,9 +241,9 @@ shw_grey "================================================" shw_grey "" shw_grey "In order to rollback this change, edit the Network Service(s) like so:" shw_grey "" -shw_grey "oc -n ${NAMESPACE} patch svc/${DB_NETWORK_SERVICE} -p \"{\\\"spec\\\":{\\\"externalName': \\\"${ORIGINAL_DB_HOST}\\\"}}\"" +shw_grey "oc -n $NAMESPACE patch svc/$DB_NETWORK_SERVICE -p '{\"spec\":{\"externalName\": \"$ORIGINAL_DB_HOST\"}}'" if [ "$DB_READREPLICA_HOSTS" ]; then - shw_grey "oc -n ${NAMESPACE} patch svc/${DB_READREPLICA_HOSTS} -p \"{\\\"spec\\\":{\\\"externalName': \\\"${ORIGINAL_DB_READREPLICA_HOSTS}\\\"}}\"" + shw_grey "oc -n $NAMESPACE patch svc/$DB_READREPLICA_HOSTS -p '{\"spec\":{\"externalName\": \"$ORIGINAL_DB_READREPLICA_HOSTS\"}}'" fi echo "" From a5509c48810e8fff6afcddbc5ae4f9e036276135 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 18:06:43 +0800 Subject: [PATCH 010/280] Exit early on invalid arguments This will catch e.g. --destination=foo.example.com --- helpers/shared-to-shared-migrate.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index e9eba11f0e..6d1a2c18b7 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -90,9 +90,7 @@ shw_err () { # Parse input arguments. while [[ $# -gt 0 ]] ; do - key="$1" - - case $key in + case $1 in -d|--destination) DESTINATION_CLUSTER="$2" shift # past argument @@ -112,6 +110,10 @@ while [[ $# -gt 0 ]] ; do DRY_RUN="TRUE" shift # past argument ;; + *) + echo "Invalid Argument: $1" + exit 3 + ;; esac done From ffc0997ff304683c4a01842875a57147b40faf72 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 18:08:56 +0800 Subject: [PATCH 011/280] Add start/end timestamps --- helpers/shared-to-shared-migrate.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 6d1a2c18b7..5cbd4910b9 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -117,6 +117,8 @@ while [[ $# -gt 0 ]] ; do esac done +shw_grey "================================================" +shw_grey " START_TIMESTAMP='$(date +%Y-%m-%dT%H:%M:%S%z)'" shw_grey "================================================" shw_grey " DESTINATION_CLUSTER=$DESTINATION_CLUSTER" shw_grey " REPLICA_CLUSTER=$REPLICA_CLUSTER" @@ -249,5 +251,8 @@ if [ "$DB_READREPLICA_HOSTS" ]; then fi echo "" +shw_grey "================================================" +shw_grey " END_TIMESTAMP='$(date +%Y-%m-%dT%H:%M:%S%z)'" +shw_grey "================================================" shw_norm "Done" exit 0 From 60e389bc849567501e1a02408466966cee5d40ef Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 18:11:49 +0800 Subject: [PATCH 012/280] Unnecessary bash --- helpers/shared-to-shared-migrate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 5cbd4910b9..037d5a8448 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -194,7 +194,7 @@ shw_norm "================================================" shw_info "> Importing the dump into ${DESTINATION_CLUSTER}" shw_info "================================================" oc -n "$NAMESPACE" exec "$POD" -- bash -c "time mysql -h '$DESTINATION_CLUSTER' -u '$DB_USER' -p'$DB_PASSWORD' '$DB_NAME' < /tmp/migration.sql" -oc -n "$NAMESPACE" exec "$POD" -- bash -c "rm /tmp/migration.sql" +oc -n "$NAMESPACE" exec "$POD" -- rm /tmp/migration.sql shw_norm "> Import is done" shw_norm "================================================" From d8bacfb8d8e20fb0a1c200c7ddca513feff5629b Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 19:34:40 +0800 Subject: [PATCH 013/280] Remove redundant exits --- helpers/shared-to-shared-migrate.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 037d5a8448..3e07c79ac6 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -184,9 +184,9 @@ POD=$(oc -n "$NAMESPACE" get pods -o json --show-all=false -l service=cli | jq - shw_info "> Dumping database $DB_NAME on pod $POD on host $DB_NETWORK_SERVICE" shw_info "================================================" oc -n "$NAMESPACE" exec "$POD" -- bash -c "time mysqldump -h '$DB_NETWORK_SERVICE' -u '$DB_USER' -p'$DB_PASSWORD' '$DB_NAME' > /tmp/migration.sql" -oc -n "$NAMESPACE" exec "$POD" -- ls -lath /tmp/migration.sql || exit 1 +oc -n "$NAMESPACE" exec "$POD" -- ls -lath /tmp/migration.sql oc -n "$NAMESPACE" exec "$POD" -- head -n 5 /tmp/migration.sql -oc -n "$NAMESPACE" exec "$POD" -- tail -n 5 /tmp/migration.sql || exit 1 +oc -n "$NAMESPACE" exec "$POD" -- tail -n 5 /tmp/migration.sql shw_norm "> Dump is done" shw_norm "================================================" From 20526eec02f53424e1d94289f3a61e72ca73758c Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 19:47:26 +0800 Subject: [PATCH 014/280] base64 decode ASB secret values --- helpers/shared-to-shared-migrate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 3e07c79ac6..00d24e706f 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -24,7 +24,7 @@ # # How to get your existing ASB root credentials # ============================================= -# oc -n openshift-ansible-service-broker get secret/lagoon-dbaas-db-credentials -o JSON | jq '.data' +# oc -n openshift-ansible-service-broker get secret/lagoon-dbaas-db-credentials -o json | jq '.data | map_values(@base64d)' # # How to create a `.my.cnf` file # ============================== From 7f39d1f6e85b6a129238975fff2b92bd9093c448 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 19:54:12 +0800 Subject: [PATCH 015/280] Avoid deprecated oc flag --- helpers/shared-to-shared-migrate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 00d24e706f..6929603439 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -180,7 +180,7 @@ shw_info "================================================" mysql --defaults-file="$CONF_FILE" -e "SELECT * FROM mysql.db WHERE Db = '${DB_NAME}'\G;" # Dump the database inside the CLI pod. -POD=$(oc -n "$NAMESPACE" get pods -o json --show-all=false -l service=cli | jq -er '.items[].metadata.name') +POD=$(oc -n "$NAMESPACE" get pods -o json --field-selector=status.phase=Running -l service=cli | jq -er '.items[0].metadata.name') shw_info "> Dumping database $DB_NAME on pod $POD on host $DB_NETWORK_SERVICE" shw_info "================================================" oc -n "$NAMESPACE" exec "$POD" -- bash -c "time mysqldump -h '$DB_NETWORK_SERVICE' -u '$DB_USER' -p'$DB_PASSWORD' '$DB_NAME' > /tmp/migration.sql" From 70cea3149e64c1acc90665e3b292530ce8b10493 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 19:58:25 +0800 Subject: [PATCH 016/280] Partially revert changes to oc command quote esacping --- helpers/shared-to-shared-migrate.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 6929603439..0025002546 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -206,7 +206,7 @@ ORIGINAL_DB_HOST=$(oc -n "$NAMESPACE" get "svc/$DB_NETWORK_SERVICE" -o json --ex if [ "$DRY_RUN" ] ; then echo "**DRY RUN**" else - oc -n "$NAMESPACE" patch "svc/$DB_NETWORK_SERVICE" -p "{\"spec\":{\"externalName\": \"${DESTINATION_CLUSTER}\"}}" + oc -n "$NAMESPACE" patch "svc/$DB_NETWORK_SERVICE" -p "{\"spec\":{\"externalName\": \"$DESTINATION_CLUSTER\"}}" fi if [ "$DB_READREPLICA_HOSTS" ]; then shw_info "> Altering the Network Service $DB_READREPLICA_HOSTS to point at $REPLICA_CLUSTER" @@ -215,7 +215,7 @@ if [ "$DB_READREPLICA_HOSTS" ]; then if [ "$DRY_RUN" ] ; then echo "**DRY RUN**" else - oc -n "$NAMESPACE" patch "svc/$DB_READREPLICA_HOSTS" -p '{"spec":{"externalName": "'"$REPLICA_CLUSTER"'"}}' + oc -n "$NAMESPACE" patch "svc/$DB_READREPLICA_HOSTS" -p "{\"spec\":{\"externalName\": \"$REPLICA_CLUSTER\"}}" fi fi From 8e5edf02cf41423874714ea3da789f0717fc58fb Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 20:15:55 +0800 Subject: [PATCH 017/280] Add duration --- helpers/shared-to-shared-migrate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 0025002546..f0ba68d10e 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -254,5 +254,5 @@ echo "" shw_grey "================================================" shw_grey " END_TIMESTAMP='$(date +%Y-%m-%dT%H:%M:%S%z)'" shw_grey "================================================" -shw_norm "Done" +shw_norm "Done in $SECONDS seconds" exit 0 From 4168ffcc57a9c58ec1215070ef0a188914a8ddf3 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 20:36:14 +0800 Subject: [PATCH 018/280] Auto scale missing cli pod --- helpers/shared-to-shared-migrate.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index f0ba68d10e..fd73cacac2 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -180,7 +180,14 @@ shw_info "================================================" mysql --defaults-file="$CONF_FILE" -e "SELECT * FROM mysql.db WHERE Db = '${DB_NAME}'\G;" # Dump the database inside the CLI pod. -POD=$(oc -n "$NAMESPACE" get pods -o json --field-selector=status.phase=Running -l service=cli | jq -er '.items[0].metadata.name') +POD=$(oc -n "$NAMESPACE" get pods -o json --field-selector=status.phase=Running -l service=cli | jq -r '.items[0].metadata.name // empty') +if [ -z "$POD" ]; then + shw_warn "No running cli pod in namespace $NAMESPACE" + shw_warn "Scaling up 1 cli DeploymentConfig pod" + oc -n "$NAMESPACE" scale dc cli --replicas=1 --timeout=2m + sleep 32 # hope for timely scheduling + POD=$(oc -n "$NAMESPACE" get pods -o json --field-selector=status.phase=Running -l service=cli | jq -er '.items[0].metadata.name') +fi shw_info "> Dumping database $DB_NAME on pod $POD on host $DB_NETWORK_SERVICE" shw_info "================================================" oc -n "$NAMESPACE" exec "$POD" -- bash -c "time mysqldump -h '$DB_NETWORK_SERVICE' -u '$DB_USER' -p'$DB_PASSWORD' '$DB_NAME' > /tmp/migration.sql" From 608c47248d9a4ae401d7cbf7ad7db84a542fd125 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 21:23:58 +0800 Subject: [PATCH 019/280] Simplify ls args --- helpers/shared-to-shared-migrate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index fd73cacac2..3c2c3f6a63 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -191,7 +191,7 @@ fi shw_info "> Dumping database $DB_NAME on pod $POD on host $DB_NETWORK_SERVICE" shw_info "================================================" oc -n "$NAMESPACE" exec "$POD" -- bash -c "time mysqldump -h '$DB_NETWORK_SERVICE' -u '$DB_USER' -p'$DB_PASSWORD' '$DB_NAME' > /tmp/migration.sql" -oc -n "$NAMESPACE" exec "$POD" -- ls -lath /tmp/migration.sql +oc -n "$NAMESPACE" exec "$POD" -- ls -lh /tmp/migration.sql oc -n "$NAMESPACE" exec "$POD" -- head -n 5 /tmp/migration.sql oc -n "$NAMESPACE" exec "$POD" -- tail -n 5 /tmp/migration.sql shw_norm "> Dump is done" From 9a9a19d645ed405323e3d61cb13f5f4c05343ca7 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 22:41:22 +0800 Subject: [PATCH 020/280] Add example loop command --- helpers/shared-to-shared-migrate.sh | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 3c2c3f6a63..626cfdba8f 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -39,14 +39,30 @@ # ======================================================================= # ssh -L 33007:shared-cluster.cluster-banana.ap-southeast-2.rds.amazonaws.com:3306 jumpbox.aws.amazee.io # -# Example commands -# ================ +# Example command 1 +# ================= # ./helpers/shared-to-shared-migrate.sh \ # --destination shared-cluster.cluster-apple.ap-southeast-2.rds.amazonaws.com \ # --replica shared-cluster.cluster-r0-apple.ap-southeast-2.rds.amazonaws.com \ # --namespace NAMESPACE \ # --dry-run # +# Example command 2 +# ================= +# namespaces=" +# foo-example-com-production +# bar-example-com-production +# baz-example-com-production +# quux-example-com-production +# " +# for namespace in $namespaces; do +# ./helpers/shared-to-shared-migrate.sh \ +# --dry-run \ +# --namespace "$namespace" \ +# --destination shared-mysql-production-1-cluster.cluster-plum.ap-southeast-2.rds.amazonaws.com \ +# --replica shared-mysql-production-1-cluster.cluster-ro-plum.ap-southeast-2.rds.amazonaws.com +# done +# set -euo pipefail # Initialize our own variables: From bd9085e6120abb5993c2efc6b198b19b0408ff16 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Apr 2020 22:51:16 +0800 Subject: [PATCH 021/280] Fix typo --- helpers/shared-to-shared-migrate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/shared-to-shared-migrate.sh b/helpers/shared-to-shared-migrate.sh index 626cfdba8f..f734e3d663 100755 --- a/helpers/shared-to-shared-migrate.sh +++ b/helpers/shared-to-shared-migrate.sh @@ -18,7 +18,7 @@ # ============ # * You are logged into OpenShift CLI and have access to the NAMESPACE you want # to migrate. -# * You have a `.my.cnf` file for the desintation database cluster. +# * You have a `.my.cnf` file for the destination database cluster. # * If your destination database cluster is not directly accessible, then you # have created SSH tunnels to expose them on a local port. # From 0a078cc57015171f2cccc494abd351d7fa6003c6 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 6 Apr 2020 09:36:09 +1000 Subject: [PATCH 022/280] fix typo in esclient --- services/api/src/clients/esClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/api/src/clients/esClient.js b/services/api/src/clients/esClient.js index 188452f438..386834ec82 100644 --- a/services/api/src/clients/esClient.js +++ b/services/api/src/clients/esClient.js @@ -2,10 +2,10 @@ const elasticsearch = require('elasticsearch'); -const { LOGSDB_ADMIN_PASSWORD, ELASTCISEARCH_HOST } = process.env; +const { LOGSDB_ADMIN_PASSWORD, ELASTICSEARCH_HOST } = process.env; const esClient = new elasticsearch.Client({ - host: ELASTCISEARCH_HOST || 'logs-db-service:9200', + host: ELASTICSEARCH_HOST || 'logs-db-service:9200', log: 'warning', httpAuth: `admin:${LOGSDB_ADMIN_PASSWORD || ''}`, }); From 2f1e710b2bb9bbba1de61b53b2c6ef9270178ba2 Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Mon, 6 Apr 2020 11:27:58 -0500 Subject: [PATCH 023/280] Adding harbor admin password to api service. --- services/api/.lagoon.app.yml | 5 +++++ services/api/Dockerfile | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/services/api/.lagoon.app.yml b/services/api/.lagoon.app.yml index 7199cfcc34..3c890c771f 100644 --- a/services/api/.lagoon.app.yml +++ b/services/api/.lagoon.app.yml @@ -124,6 +124,11 @@ objects: secretKeyRef: name: keycloak-api-client-secret key: KEYCLOAK_API_CLIENT_SECRET + - name: HARBOR_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: harbor-admin-password + key: HARBOR_ADMIN_PASSWORD - name: SERVICE_NAME value: ${SERVICE_NAME} - name: CRONJOBS diff --git a/services/api/Dockerfile b/services/api/Dockerfile index 8225d75107..7e220f2abb 100644 --- a/services/api/Dockerfile +++ b/services/api/Dockerfile @@ -27,7 +27,8 @@ ENV NODE_ENV=production \ KEYCLOAK_ADMIN_PASSWORD=admin \ ELASTICSEARCH_HOST=logs-db-service:9200 \ ELASTICSEARCH_URL=http://logs-db-service:9200 \ - KEYCLOAK_API_CLIENT_SECRET=39d5282d-3684-4026-b4ed-04bbc034b61a + KEYCLOAK_API_CLIENT_SECRET=39d5282d-3684-4026-b4ed-04bbc034b61a \ + HARBOR_ADMIN_PASSWORD=admin # The API is not very resilient to sudden mariadb restarts which can happen when the api and mariadb are starting # at the same time. So we have a small entrypoint which waits for mariadb to be fully ready. From 626a5ae72d1bfac7b9d27f54fcde1ab3f30c27bb Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Tue, 7 Apr 2020 10:01:17 -0500 Subject: [PATCH 024/280] Adding harbor project removal logic --- .../api/src/resources/project/harborSetup.js | 28 +++++++++++++++++++ .../api/src/resources/project/resolvers.js | 4 +++ 2 files changed, 32 insertions(+) diff --git a/services/api/src/resources/project/harborSetup.js b/services/api/src/resources/project/harborSetup.js index 18356d1c5c..4a7eb64aa6 100644 --- a/services/api/src/resources/project/harborSetup.js +++ b/services/api/src/resources/project/harborSetup.js @@ -152,6 +152,34 @@ const createHarborOperations = (sqlClient /* : MariaSQL */) => ({ logger.error(`Error while creating a webhook in the Harbor project for ${lagoonProjectName}, error: ${err}`) } } + deleteProject: async (lagoonProjectName) => { + // Delete harbor project + + // Get new harbor project's id + try { + const res = await harborClient.get(`projects?name=${lagoonProjectName}`) + var harborProjectID = res.body[0].project_id + logger.debug(`Got the harbor project id for project ${lagoonProjectName} successfully!`) + } catch (err) { + if (err.statusCode == 404) { + logger.warn(`Unable to get the harbor project id of "${lagoonProjectName}", as it does not exist in harbor!`) + return + } else { + logger.error(`Unable to get the harbor project id of "${lagoonProjectName}", error: ${err}`) + } + } + logger.debug(`Harbor project id for ${lagoonProjectName}: ${harborProjectID}`) + + // Delete harbor project + try { + var res = await harborClient.delete(`projects/${harborProjectID}`); + logger.debug(`Harbor project ${lagoonProjectName} deleted!`) + } catch (err) { + // 400 means registry ID is invalid or registry is being used by policies + // 404 means registry doesn't exist + logger.info(`Unable to delete the harbor project "${lagoonProjectName}", error: ${err}`) + } + } }) module.exports = createHarborOperations; diff --git a/services/api/src/resources/project/resolvers.js b/services/api/src/resources/project/resolvers.js index 9262feb145..511e4da330 100644 --- a/services/api/src/resources/project/resolvers.js +++ b/services/api/src/resources/project/resolvers.js @@ -399,6 +399,10 @@ const deleteProject = async ( logger.error(`Could not delete default user for project ${project.name}: ${err.message}`); } + const harborOperations = createHarborOperations(sqlClient); + + const harborResults = await harborOperations.deleteProject(project.name) + return 'success'; }; From 4e0c8a7cc5f36ac6c53b6af84d1ee83359f66000 Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Tue, 7 Apr 2020 10:22:44 -0500 Subject: [PATCH 025/280] Typo fix --- services/api/src/resources/project/harborSetup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/api/src/resources/project/harborSetup.js b/services/api/src/resources/project/harborSetup.js index 4a7eb64aa6..eb97b75a71 100644 --- a/services/api/src/resources/project/harborSetup.js +++ b/services/api/src/resources/project/harborSetup.js @@ -151,7 +151,7 @@ const createHarborOperations = (sqlClient /* : MariaSQL */) => ({ } catch (err) { logger.error(`Error while creating a webhook in the Harbor project for ${lagoonProjectName}, error: ${err}`) } - } + }, deleteProject: async (lagoonProjectName) => { // Delete harbor project From 94bf030c88cc9d0d4256ce991d53fde4b9407ca8 Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Tue, 7 Apr 2020 14:35:59 -0500 Subject: [PATCH 026/280] Adding logic to remove repositories before trying to remove harbor project. --- .../api/src/resources/project/harborSetup.js | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/services/api/src/resources/project/harborSetup.js b/services/api/src/resources/project/harborSetup.js index eb97b75a71..188ab52087 100644 --- a/services/api/src/resources/project/harborSetup.js +++ b/services/api/src/resources/project/harborSetup.js @@ -170,13 +170,36 @@ const createHarborOperations = (sqlClient /* : MariaSQL */) => ({ } logger.debug(`Harbor project id for ${lagoonProjectName}: ${harborProjectID}`) + // Check for existing repositories within the project + try { + const res = await harborClient.get(`search?name=${lagoonProjectName}`) + const harborRepos = [] + for (i = 0; i < res.repository.length; i++) { + if (res.repository[i].project_name == lagoonProjectName){ + harborRepos.push(res.repository[i]) + } + } + } catch (err) { + logger.error(`Unable to search for repositories within the harbor project "${lagoonProjectName}", error: ${err}`) + } + + // Delete any repositories within this project + try { + for (i = 0; i < harborRepos.length; i++) { + var res = await harborClient.delete(`repositories/${harborRepos[i].repository_name}`) + } + } catch (err) { + logger.error(`Unable to delete repositories within the harbor project "${lagoonProjectName}", error: ${err}`) + } + // Delete harbor project try { var res = await harborClient.delete(`projects/${harborProjectID}`); logger.debug(`Harbor project ${lagoonProjectName} deleted!`) } catch (err) { - // 400 means registry ID is invalid or registry is being used by policies - // 404 means registry doesn't exist + // 400 means the project id is invalid + // 404 means project doesn't exist + // 412 means project still contains repositories logger.info(`Unable to delete the harbor project "${lagoonProjectName}", error: ${err}`) } } From c641068340521953c24ad1508efb3b15e70dca70 Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Tue, 7 Apr 2020 23:26:32 -0500 Subject: [PATCH 027/280] Updating logic for project deletion from Harbor. --- services/api/src/resources/project/harborSetup.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/services/api/src/resources/project/harborSetup.js b/services/api/src/resources/project/harborSetup.js index 188ab52087..2411cc051d 100644 --- a/services/api/src/resources/project/harborSetup.js +++ b/services/api/src/resources/project/harborSetup.js @@ -61,7 +61,7 @@ const createHarborOperations = (sqlClient /* : MariaSQL */) => ({ if (err.statusCode == 404) { logger.error(`Unable to get the harbor project id of "${lagoonProjectName}", as it does not exist in harbor!`) } else { - logger.error(`Unable to get the harbor project id of "${lagoonProjectName}" !!`) + logger.error(`Unable to get the harbor project id of "${lagoonProjectName}", error: ${err}`) } } logger.debug(`Harbor project id for ${lagoonProjectName}: ${harborProjectID}`) @@ -87,7 +87,7 @@ const createHarborOperations = (sqlClient /* : MariaSQL */) => ({ if (err.statusCode == 409) { logger.warn(`Unable to create a robot account for harbor project "${lagoonProjectName}", as a robot account of the same name already exists!`) } else { - logger.warn(`Unable to create a robot account for harbor project "${lagoonProjectName}" !!`) + logger.error(`Unable to create a robot account for harbor project "${lagoonProjectName}", error: ${err}`) } } @@ -155,17 +155,20 @@ const createHarborOperations = (sqlClient /* : MariaSQL */) => ({ deleteProject: async (lagoonProjectName) => { // Delete harbor project - // Get new harbor project's id + // Get existing harbor project's id try { const res = await harborClient.get(`projects?name=${lagoonProjectName}`) var harborProjectID = res.body[0].project_id logger.debug(`Got the harbor project id for project ${lagoonProjectName} successfully!`) } catch (err) { if (err.statusCode == 404) { + // This case could come to pass if a project was created + // before we began using Harbor as our container registry logger.warn(`Unable to get the harbor project id of "${lagoonProjectName}", as it does not exist in harbor!`) return } else { logger.error(`Unable to get the harbor project id of "${lagoonProjectName}", error: ${err}`) + return } } logger.debug(`Harbor project id for ${lagoonProjectName}: ${harborProjectID}`) @@ -200,7 +203,7 @@ const createHarborOperations = (sqlClient /* : MariaSQL */) => ({ // 400 means the project id is invalid // 404 means project doesn't exist // 412 means project still contains repositories - logger.info(`Unable to delete the harbor project "${lagoonProjectName}", error: ${err}`) + logger.error(`Unable to delete the harbor project "${lagoonProjectName}", error: ${err}`) } } }) From bf8bcf76adb84930e9bfbebc95bd0a50677d878d Mon Sep 17 00:00:00 2001 From: Sean Hamlin Date: Fri, 10 Apr 2020 15:29:36 +1200 Subject: [PATCH 028/280] Code style. 2 spaces. --- images/varnish-drupal/drupal.vcl | 217 ++++++++++++++++--------------- 1 file changed, 111 insertions(+), 106 deletions(-) diff --git a/images/varnish-drupal/drupal.vcl b/images/varnish-drupal/drupal.vcl index b94c7b4551..4ca49e5ac5 100644 --- a/images/varnish-drupal/drupal.vcl +++ b/images/varnish-drupal/drupal.vcl @@ -14,10 +14,10 @@ backend default { # Allow purging from localhost # @TODO allow from openshift network acl purge { - "127.0.0.1"; - "10.0.0.0"/8; - "172.16.0.0"/12; - "192.168.0.0"/16; + "127.0.0.1"; + "10.0.0.0"/8; + "172.16.0.0"/12; + "192.168.0.0"/16; } sub vcl_init { @@ -31,52 +31,55 @@ sub vcl_init { # This configuration is optimized for Drupal hosting: # Respond to incoming requests. sub vcl_recv { - if (req.url ~ "^/varnish_status$") { + if (req.url ~ "^/varnish_status$") { return (synth(200,"OK")); } # set the backend, which should be used: set req.backend_hint = www_dir.backend("${VARNISH_BACKEND_HOST:-nginx}"); # Always set the forward ip. - if (req.restarts == 0) { - if (req.http.x-forwarded-for) { - set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip; - } else { - set req.http.X-Forwarded-For = client.ip; - } - } - - + if (req.restarts == 0) { + if (req.http.x-forwarded-for) { + set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip; + } + else { + set req.http.X-Forwarded-For = client.ip; + } + } if (req.http.X-LAGOON-VARNISH ) { - ## Pass all Requests which are handled via an upstream Varnish + # Pass all Requests which are handled via an upstream Varnish set req.http.X-LAGOON-VARNISH = "${HOSTNAME}-${LAGOON_GIT_BRANCH:-undef}-${LAGOON_PROJECT}, " + req.http.X-LAGOON-VARNISH; set req.http.X-LAGOON-VARNISH-BYPASS = "true"; - } else if (req.http.Fastly-FF) { - ## Pass all Requests which are handled via Fastly + } + else if (req.http.Fastly-FF) { + # Pass all Requests which are handled via Fastly set req.http.X-LAGOON-VARNISH = "${HOSTNAME}-${LAGOON_GIT_BRANCH:-undef}-${LAGOON_PROJECT}, fastly"; set req.http.X-LAGOON-VARNISH-BYPASS = "true"; set req.http.X-Forwarded-For = req.http.Fastly-Client-IP; - } else if (req.http.CF-RAY) { - ## Pass all Requests which are handled via CloudFlare + } + else if (req.http.CF-RAY) { + # Pass all Requests which are handled via CloudFlare set req.http.X-LAGOON-VARNISH = "${HOSTNAME}-${LAGOON_GIT_BRANCH:-undef}-${LAGOON_PROJECT}, cloudflare"; set req.http.X-LAGOON-VARNISH-BYPASS = "true"; set req.http.X-Forwarded-For = req.http.CF-Connecting-IP; - } else if (req.http.X-Pull) { - ## Pass all Requests which are handled via KeyCDN + } + else if (req.http.X-Pull) { + # Pass all Requests which are handled via KeyCDN set req.http.X-LAGOON-VARNISH = "${HOSTNAME}-${LAGOON_GIT_BRANCH:-undef}-${LAGOON_PROJECT}, keycdn"; set req.http.X-LAGOON-VARNISH-BYPASS = "true"; - } else { - ## We set a header to let a Varnish Chain know that it already has been varnishcached + } + else { + # We set a header to let a Varnish chain know that it already has been varnishcached set req.http.X-LAGOON-VARNISH = "${HOSTNAME}-${LAGOON_GIT_BRANCH:-undef}-${LAGOON_PROJECT}"; - ## Allow to bypass based on env variable `VARNISH_BYPASS` + # Allow to bypass based on env variable `VARNISH_BYPASS` set req.http.X-LAGOON-VARNISH-BYPASS = "${VARNISH_BYPASS:-false}"; } # Websockets are piped if (req.http.Upgrade ~ "(?i)websocket") { - return (pipe); + return (pipe); } if (req.http.X-LAGOON-VARNISH-BYPASS == "true" || req.http.X-LAGOON-VARNISH-BYPASS == "TRUE") { @@ -98,42 +101,43 @@ sub vcl_recv { # Bypass a cache hit (the request is still sent to the backend) if (req.method == "REFRESH") { - if (!client.ip ~ purge) { return (synth(405, "Not allowed")); } - set req.method = "GET"; - set req.hash_always_miss = true; + if (!client.ip ~ purge) { + return (synth(405, "Not allowed")); + } + set req.method = "GET"; + set req.hash_always_miss = true; } # Only allow BAN requests from IP addresses in the 'purge' ACL. if (req.method == "BAN" || req.method == "URIBAN" || req.method == "PURGE") { - # Only allow BAN from defined ACL - if (!client.ip ~ purge) { - return (synth(403, "Your IP is not allowed.")); - } - - # Only allows BAN if the Host Header has the style of with "${SERVICE_NAME:-varnish}:8080" or "${SERVICE_NAME:-varnish}". - # Such a request is only possible from within the Docker network, as a request from external goes trough the Kubernetes Router and for that needs a proper Host Header - if (!req.http.host ~ "^${SERVICE_NAME:-varnish}(:\d+)?$") { - return (synth(403, "Only allowed from within own network.")); - } + # Only allow BAN from defined ACL + if (!client.ip ~ purge) { + return (synth(403, "Your IP is not allowed.")); + } - if (req.method == "BAN") { - # Logic for the ban, using the Cache-Tags header. - if (req.http.Cache-Tags) { - ban("obj.http.Cache-Tags ~ " + req.http.Cache-Tags); - # Throw a synthetic page so the request won't go to the backend. - return (synth(200, "Ban added.")); - } - else { - return (synth(403, "Cache-Tags header missing.")); - } - } + # Only allows BAN if the Host Header has the style of with "${SERVICE_NAME:-varnish}:8080" or "${SERVICE_NAME:-varnish}". + # Such a request is only possible from within the Docker network, as a request from external goes trough the Kubernetes Router and for that needs a proper Host Header + if (!req.http.host ~ "^${SERVICE_NAME:-varnish}(:\d+)?$") { + return (synth(403, "Only allowed from within own network.")); + } - if (req.method == "URIBAN" || req.method == "PURGE") { - ban("req.url ~ " + req.url); + if (req.method == "BAN") { + # Logic for the ban, using the Cache-Tags header. + if (req.http.Cache-Tags) { + ban("obj.http.Cache-Tags ~ " + req.http.Cache-Tags); # Throw a synthetic page so the request won't go to the backend. return (synth(200, "Ban added.")); } + else { + return (synth(403, "Cache-Tags header missing.")); + } + } + if (req.method == "URIBAN" || req.method == "PURGE") { + ban("req.url ~ " + req.url); + # Throw a synthetic page so the request won't go to the backend. + return (synth(200, "Ban added.")); + } } # Non-RFC2616 or CONNECT which is weird, we pipe that @@ -152,18 +156,17 @@ sub vcl_recv { return (pass); } - # Any requests with Basic Auth are passed - if (req.http.Authorization || req.http.Authenticate) - { + # Any requests with Basic Authentication are passed. + if (req.http.Authorization || req.http.Authenticate) { return (pass); } - ## Pass requests which are from blackfire + # Blackfire requests are passed. if (req.http.X-Blackfire-Query) { return (pass); } - # Some URLs should never be cached + # Some URLs should never be cached. if (req.url ~ "^/status\.php$" || req.url ~ "^/update\.php$" || req.url ~ "^/admin([/?]|$).*$" || @@ -176,9 +179,8 @@ sub vcl_recv { return (pass); } - # Plupload likes to get piped - if (req.url ~ "^.*/plupload-handle-uploads.*$" - ) { + # Plupload likes to get piped. + if (req.url ~ "^.*/plupload-handle-uploads.*$") { return (pipe); } @@ -259,31 +261,32 @@ sub vcl_pipe { } sub vcl_hit { - if (obj.ttl >= 0s) { - # normal hit - return (deliver); + if (obj.ttl >= 0s) { + # normal hit + return (deliver); + } + # We have no fresh fish. Lets look at the stale ones. + if (std.healthy(req.backend_hint)) { + # Backend is healthy. If the object is not older then 30secs, deliver it to the client + # and automatically create a separate backend request to warm the cache for this request. + if (obj.ttl + 30s > 0s) { + set req.http.grace = "normal(limited)"; + return (deliver); + } else { + # No candidate for grace. Fetch a fresh object. + return (miss); } - # We have no fresh fish. Lets look at the stale ones. - if (std.healthy(req.backend_hint)) { - # Backend is healthy. If the object is not older then 30secs, deliver it to the client - # and automatically create a separate backend request to warm the cache for this request. - if (obj.ttl + 30s > 0s) { - set req.http.grace = "normal(limited)"; - return (deliver); - } else { - # No candidate for grace. Fetch a fresh object. - return(miss); - } + } + else { + # backend is sick - use full grace + if (obj.ttl + obj.grace > 0s) { + set req.http.grace = "full"; + return (deliver); } else { - # backend is sick - use full grace - if (obj.ttl + obj.grace > 0s) { - set req.http.grace = "full"; - return (deliver); - } else { - # no graced object. - return (miss); - } + # no graced object. + return (miss); } + } } sub vcl_backend_response { @@ -295,12 +298,12 @@ sub vcl_backend_response { set beresp.http.X-Host = bereq.http.host; # If the backend sends a X-LAGOON-VARNISH-BACKEND-BYPASS header we directly deliver - if(beresp.http.X-LAGOON-VARNISH-BACKEND-BYPASS == "TRUE") { + if (beresp.http.X-LAGOON-VARNISH-BACKEND-BYPASS == "TRUE") { return (deliver); } # Cache 404 and 403 for 10 seconds - if(beresp.status == 404 || beresp.status == 403) { + if (beresp.status == 404 || beresp.status == 403) { set beresp.ttl = 10s; return (deliver); } @@ -321,6 +324,7 @@ sub vcl_backend_response { set beresp.http.Cache-Control = "public, max-age=${VARNISH_ASSETS_TTL:-2628001}"; set beresp.http.Expires = "" + (now + beresp.ttl); } + # Disable buffering only for BigPipe responses if (beresp.http.Surrogate-Control ~ "BigPipe/1.0") { set beresp.do_stream = true; @@ -359,18 +363,19 @@ sub vcl_deliver { } sub vcl_hash { - hash_data(req.url); - if (req.http.host) { - hash_data(req.http.host); - } else { - hash_data(server.ip); - } - if (req.http.X-Forwarded-Proto) { - hash_data(req.http.X-Forwarded-Proto); - } - if (req.http.HTTPS) { - hash_data(req.http.HTTPS); - } + hash_data(req.url); + if (req.http.host) { + hash_data(req.http.host); + } + else { + hash_data(server.ip); + } + if (req.http.X-Forwarded-Proto) { + hash_data(req.http.X-Forwarded-Proto); + } + if (req.http.HTTPS) { + hash_data(req.http.HTTPS); + } return (lookup); } @@ -387,20 +392,20 @@ sub vcl_synth { # Create our synthetic response synthetic(""); return(deliver); -} + } return (deliver); } sub vcl_backend_error { - # Restart the request, when we have a backend server error, to try another backend. - # Restart max twice. - if (bereq.retries < 2) { - return(retry); - } + # Restart the request, when we have a backend server error, to try another backend. + # Restart max twice. + if (bereq.retries < 2) { + return(retry); + } - set beresp.http.Content-Type = "text/html; charset=utf-8"; - set beresp.http.Retry-After = "5"; - synthetic( {" + set beresp.http.Content-Type = "text/html; charset=utf-8"; + set beresp.http.Retry-After = "5"; + synthetic({" @@ -443,6 +448,6 @@ sub vcl_backend_error { -"} ); - return (deliver); +"}); + return (deliver); } From 4db220116b89047e35ea74ff23ea66256a7c77ab Mon Sep 17 00:00:00 2001 From: Sean Hamlin Date: Fri, 10 Apr 2020 15:30:10 +1200 Subject: [PATCH 029/280] Exclude file extensions that are likely to be large, from caching. --- images/varnish-drupal/drupal.vcl | 7 ++++++- images/varnish/default.vcl | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/images/varnish-drupal/drupal.vcl b/images/varnish-drupal/drupal.vcl index 4ca49e5ac5..aebb5d2113 100644 --- a/images/varnish-drupal/drupal.vcl +++ b/images/varnish-drupal/drupal.vcl @@ -151,7 +151,12 @@ sub vcl_recv { return (pipe); } - # We only try to cache GET and HEAD, other things are passed + # Large binary files are passed. + if (req.url ~ "\.(msi|exe|dmg|zip|tgz|gz|pkg)") { + return(pass); + } + + # We only try to cache GET and HEAD, other things are passed. if (req.method != "GET" && req.method != "HEAD") { return (pass); } diff --git a/images/varnish/default.vcl b/images/varnish/default.vcl index d5e2336927..6f47ff1651 100644 --- a/images/varnish/default.vcl +++ b/images/varnish/default.vcl @@ -43,6 +43,10 @@ sub vcl_recv { return (synth(200,"OK")); } + # Large binary files are passed. + if (req.url ~ "\.(msi|exe|dmg|zip|tgz|gz|pkg)") { + return(pass); + } } sub vcl_backend_response { From 67001ba902ca0afed8e25bca5499a7275e177485 Mon Sep 17 00:00:00 2001 From: Sean Hamlin Date: Fri, 10 Apr 2020 15:54:08 +1200 Subject: [PATCH 030/280] Files larger than 10MB get streamed. --- images/varnish-drupal/drupal.vcl | 8 +++++++- images/varnish/default.vcl | 25 +++++++++++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/images/varnish-drupal/drupal.vcl b/images/varnish-drupal/drupal.vcl index aebb5d2113..36435596b2 100644 --- a/images/varnish-drupal/drupal.vcl +++ b/images/varnish-drupal/drupal.vcl @@ -315,7 +315,6 @@ sub vcl_backend_response { # Don't allow static files to set cookies. if (bereq.url ~ "(?i)\.(css|js|jpg|jpeg|gif|ico|png|tiff|tif|img|tga|wmf|swf|html|htm|woff|woff2|mp4|ttf|eot|svg)(\?.*)?$") { - # beresp == Back-end response from the web server. unset beresp.http.set-cookie; unset beresp.http.Cache-Control; @@ -330,6 +329,13 @@ sub vcl_backend_response { set beresp.http.Expires = "" + (now + beresp.ttl); } + # Files larger than 10 MB get streamed. + if (beresp.http.Content-Length ~ "[0-9]{8,}") { + set beresp.do_stream = true; + set beresp.uncacheable = true; + set beresp.ttl = 0s; + } + # Disable buffering only for BigPipe responses if (beresp.http.Surrogate-Control ~ "BigPipe/1.0") { set beresp.do_stream = true; diff --git a/images/varnish/default.vcl b/images/varnish/default.vcl index 6f47ff1651..4c066ca262 100644 --- a/images/varnish/default.vcl +++ b/images/varnish/default.vcl @@ -50,15 +50,24 @@ sub vcl_recv { } sub vcl_backend_response { - # Happens after we have read the response headers from the backend. - # - # Here you clean the response headers, removing silly Set-Cookie headers - # and other mistakes your backend does. + # Happens after we have read the response headers from the backend. + # + # Here you clean the response headers, removing silly Set-Cookie headers + # and other mistakes your backend does. + + # Files larger than 10 MB get streamed. + if (beresp.http.Content-Length ~ "[0-9]{8,}") { + set beresp.do_stream = true; + set beresp.uncacheable = true; + set beresp.ttl = 0s; + } + + return (deliver); } sub vcl_deliver { - # Happens when we have all the pieces we need, and are about to send the - # response to the client. - # - # You can do accounting or modifying the final object here. + # Happens when we have all the pieces we need, and are about to send the + # response to the client. + # + # You can do accounting or modifying the final object here. } From 79d2e35352b8a3d966287cfae2cc95f7e67445e0 Mon Sep 17 00:00:00 2001 From: Bastian Widmer Date: Thu, 7 May 2020 01:39:35 +0200 Subject: [PATCH 031/280] First iteration of Lagoon Version Update Helper --- helpers/update-versions.yml | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 helpers/update-versions.yml diff --git a/helpers/update-versions.yml b/helpers/update-versions.yml new file mode 100644 index 0000000000..6cc40745bf --- /dev/null +++ b/helpers/update-versions.yml @@ -0,0 +1,58 @@ +# Lagoon Version Update Helper +# +# Helper to update Version inside Dockerfiles +# Update versions below in `vars` and execute locally +# +# ansible-playbook helpers/update-versions.yml +- name: update versions + hosts: 127.0.0.1 + connection: local + vars: + # Newrelic - https://docs.newrelic.com/docs/release-notes/agent-release-notes/php-release-notes/ + NEWRELIC_VERSION: '9.10.1.263' + # Composer - https://getcomposer.org/download/ + COMPOSER_VERSION: '1.10.6' + COMPOSER_HASH_SHA256: '29bdac1bda34d8902b9f9e4f5816de08879b8f3fafad901e4283519cdefbee7b' + # Drupal Console Launcher - https://github.com/hechoendrupal/drupal-console-launcher/releases + DRUPAL_CONSOLE_LAUNCHER_VERSION: 1.9.4 + DRUPAL_CONSOLE_LAUNCHER_SHA: b7759279668caf915b8e9f3352e88f18e4f20659 + # Drush - https://github.com/drush-ops/drush/releases + DRUSH_VERSION: 8.3.2 + # Drush Launcher Version - https://github.com/drush-ops/drush-launcher/releases + DRUSH_LAUNCHER_VERSION: 0.6.0 + tasks: + - name: update NEWRELIC_VERSION + lineinfile: + path: "{{ lookup('env', 'PWD') }}/images/php/fpm/Dockerfile" + regexp: 'ENV NEWRELIC_VERSION=' + line: 'ENV NEWRELIC_VERSION={{ NEWRELIC_VERSION }}' + - name: update COMPOSER_VERSION + lineinfile: + path: "{{ lookup('env', 'PWD') }}/images/php/cli/Dockerfile" + regexp: 'ENV COMPOSER_VERSION=' + line: 'ENV COMPOSER_VERSION={{ COMPOSER_VERSION }} \' + - name: update COMPOSER_HASH_SHA256 + lineinfile: + path: "{{ lookup('env', 'PWD') }}/images/php/cli/Dockerfile" + regexp: 'COMPOSER_HASH_SHA256=' + line: ' COMPOSER_HASH_SHA256={{ COMPOSER_HASH_SHA256 }}' + - name: update DRUPAL_CONSOLE_LAUNCHER_VERSION + lineinfile: + path: "{{ lookup('env', 'PWD') }}/images/php/cli-drupal/Dockerfile" + regexp: 'ENV DRUPAL_CONSOLE_LAUNCHER_VERSION=' + line: 'ENV DRUPAL_CONSOLE_LAUNCHER_VERSION={{ DRUPAL_CONSOLE_LAUNCHER_VERSION }} \' + - name: update DRUPAL_CONSOLE_LAUNCHER_SHA + lineinfile: + path: "{{ lookup('env', 'PWD') }}/images/php/cli-drupal/Dockerfile" + regexp: 'DRUPAL_CONSOLE_LAUNCHER_SHA=' + line: ' DRUPAL_CONSOLE_LAUNCHER_SHA={{ DRUPAL_CONSOLE_LAUNCHER_SHA }} \' + - name: update DRUSH_VERSION + lineinfile: + path: "{{ lookup('env', 'PWD') }}/images/php/cli-drupal/Dockerfile" + regexp: 'DRUSH_VERSION=' + line: ' DRUSH_VERSION={{ DRUSH_VERSION }} \' + - name: update DRUSH_LAUNCHER_VERSION + lineinfile: + path: "{{ lookup('env', 'PWD') }}/images/php/cli-drupal/Dockerfile" + regexp: 'DRUSH_LAUNCHER_VERSION=' + line: ' DRUSH_LAUNCHER_VERSION={{ DRUSH_LAUNCHER_VERSION }} \' From f296bda06adf06544b54f879e2ad4a11e66ece47 Mon Sep 17 00:00:00 2001 From: Sean Hamlin Date: Fri, 8 May 2020 21:38:43 +1200 Subject: [PATCH 032/280] Make the regex more specific to extensions. --- images/varnish-drupal/drupal.vcl | 2 +- images/varnish/default.vcl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/images/varnish-drupal/drupal.vcl b/images/varnish-drupal/drupal.vcl index 36435596b2..9138f8c9f4 100644 --- a/images/varnish-drupal/drupal.vcl +++ b/images/varnish-drupal/drupal.vcl @@ -152,7 +152,7 @@ sub vcl_recv { } # Large binary files are passed. - if (req.url ~ "\.(msi|exe|dmg|zip|tgz|gz|pkg)") { + if (req.url ~ "\.(msi|exe|dmg|zip|tgz|gz|pkg)$") { return(pass); } diff --git a/images/varnish/default.vcl b/images/varnish/default.vcl index 4c066ca262..7389e50fb0 100644 --- a/images/varnish/default.vcl +++ b/images/varnish/default.vcl @@ -44,7 +44,7 @@ sub vcl_recv { } # Large binary files are passed. - if (req.url ~ "\.(msi|exe|dmg|zip|tgz|gz|pkg)") { + if (req.url ~ "\.(msi|exe|dmg|zip|tgz|gz|pkg)$") { return(pass); } } From 3b998b9445c3f87a46869d11b6b43606a29b9ce6 Mon Sep 17 00:00:00 2001 From: Sean Hamlin Date: Sat, 9 May 2020 13:57:22 +1200 Subject: [PATCH 033/280] Cache the hit-for-pass for 2 minutes. --- images/varnish-drupal/drupal.vcl | 2 +- images/varnish/default.vcl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/images/varnish-drupal/drupal.vcl b/images/varnish-drupal/drupal.vcl index 9138f8c9f4..02d5e2692d 100644 --- a/images/varnish-drupal/drupal.vcl +++ b/images/varnish-drupal/drupal.vcl @@ -333,7 +333,7 @@ sub vcl_backend_response { if (beresp.http.Content-Length ~ "[0-9]{8,}") { set beresp.do_stream = true; set beresp.uncacheable = true; - set beresp.ttl = 0s; + set beresp.ttl = 120s; } # Disable buffering only for BigPipe responses diff --git a/images/varnish/default.vcl b/images/varnish/default.vcl index 7389e50fb0..6dcd0a8118 100644 --- a/images/varnish/default.vcl +++ b/images/varnish/default.vcl @@ -59,7 +59,7 @@ sub vcl_backend_response { if (beresp.http.Content-Length ~ "[0-9]{8,}") { set beresp.do_stream = true; set beresp.uncacheable = true; - set beresp.ttl = 0s; + set beresp.ttl = 120s; } return (deliver); From b3ad488312ad927414fc1dd960b61a839c184e5d Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 18 May 2020 16:15:18 +1000 Subject: [PATCH 034/280] dont use native cron for idler jobs, just run them in the pod --- .lagoon.yml | 9 --------- services/auto-idler/.lagoon.yml | 4 +++- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.lagoon.yml b/.lagoon.yml index 9e2b4e48dc..b5fbfed2f0 100644 --- a/.lagoon.yml +++ b/.lagoon.yml @@ -50,15 +50,6 @@ environments: rollouts: logs-db: statefulset logs-forwarder: statefulset - cronjobs: - - name: idle-clis - schedule: '*/15 * * * *' - command: /idle-clis.sh - service: auto-idler - - name: idle-services - schedule: '*/30 * * * *' - command: /idle-services.sh - service: auto-idler develop: types: logs-db: elasticsearch-cluster diff --git a/services/auto-idler/.lagoon.yml b/services/auto-idler/.lagoon.yml index 3f25655741..0384f6bad7 100644 --- a/services/auto-idler/.lagoon.yml +++ b/services/auto-idler/.lagoon.yml @@ -39,7 +39,9 @@ parameters: required: true - name: CRONJOBS description: Oneliner of Cronjobs - value: "" + value: |- + 30 * * * * /idle-clis.sh + 0 */4 * * * /idle-services.sh objects: - apiVersion: v1 kind: DeploymentConfig From f183f8976ef65299dd0507b07cdac5dd2c0fda6d Mon Sep 17 00:00:00 2001 From: xantrix Date: Thu, 7 May 2020 12:55:44 +0200 Subject: [PATCH 035/280] docs: Kubernetes installation Co-authored-by: Toby Bellwood --- docs/administering_lagoon/install_k8s.md | 125 +++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs/administering_lagoon/install_k8s.md diff --git a/docs/administering_lagoon/install_k8s.md b/docs/administering_lagoon/install_k8s.md new file mode 100644 index 0000000000..24ea63f34e --- /dev/null +++ b/docs/administering_lagoon/install_k8s.md @@ -0,0 +1,125 @@ +# Install local Kubernetes cluster for Lagoon + +Let's see how to install a local lightweight k8s cluster using +k3s by Rancher: [rancher/k3s](https://github.com/rancher/k3s) + +!!!hint + In order to have the best experience we recommend the following: + Linux or Mac OSX + 32 GB+ RAM total + 12 GB+ RAM allocated to Docker + 6+ cores allocated to Docker + SSD disk with 25GB+ free + +## Installation checklist +1. Make sure you have a clean state checking the following (use `-n` option for dry-run): + 1. Make sure no lagoon containers are running running `make kill`. + 2. Make sure to clean any old lagoon containers and volumes running `make down`. + 3. Now your `build` dir should be empty and `docker ps` should show no containers running. +2. Make sure to allow `172.17.0.1:5000` as insecure registry, check the [docker docs](https://docs.docker.com/registry/insecure/) for more information. + 1. Edit `insecure-registries` key in your `/etc/docker/daemon.json` and add `"insecure-registries":["172.17.0.1:5000"]` then restart docker service with `systemctl restart docker`. +3. Using `sysctl vm.max_map_count` check the value of `vm.max_map_count` is at least `262144` or set it is using `sysctl -w vm.max_map_count=262144`. We need to increase this value to avoid error [`max virtual memory areas is too low`](https://stackoverflow.com/questions/51445846/elasticsearch-max-virtual-memory-areas-vm-max-map-count-65530-is-too-low-inc/51448773#51448773) on `logs-db` Elasticsearch service. + +## Create a local k8s cluster +1. Now you can create a local k3s Kubernetes cluster running `make k3d` and see the following notable outputs: + * + ``` + INFO[0000] Creating cluster [k3s-lagoon] + INFO[0000] Creating server using docker.io/rancher/k3s:v1.17.0-k3s.1... + INFO[0008] SUCCESS: created cluster [k3s-lagoon] + ... + The push refers to repository [localhost:5000/lagoon/docker-host] + ... + The push refers to repository [localhost:5000/lagoon/kubectl-build-deploy-dind] + ... + Release "k8up" does not exist. Installing it now. + NAME: k8up + LAST DEPLOYED: Thu May 7 10:45:46 2020 + NAMESPACE: k8up + STATUS: deployed + REVISION: 1 + TEST SUITE: None + namespace/dbaas-operator created + "dbaas-operator" has been added to your repositories + Release "dbaas-operator" does not exist. Installing it now. + NAME: dbaas-operator + LAST DEPLOYED: Thu May 7 10:45:47 2020 + NAMESPACE: dbaas-operator + STATUS: deployed + REVISION: 1 + TEST SUITE: None + Release "mariadbprovider" does not exist. Installing it now. + coalesce.go:165: warning: skipped value for providers: Not a table. + NAME: mariadbprovider + LAST DEPLOYED: Thu May 7 10:45:48 2020 + NAMESPACE: dbaas-operator + STATUS: deployed + REVISION: 1 + TEST SUITE: None + namespace/lagoon created + Release "lagoon-remote" does not exist. Installing it now. + NAME: lagoon-remote + LAST DEPLOYED: Thu May 7 10:45:48 2020 + NAMESPACE: lagoon + STATUS: deployed + REVISION: 1 + TEST SUITE: None + + ``` +2. At the end of the script, using `docker ps` you should see an output like the following: + * + ``` + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 0d61e8ba168e rancher/k3s:v1.17.0-k3s.1 "/bin/k3s server --h…" 28 minutes ago Up 28 minutes 0.0.0.0:16643->16643/tcp, 0.0.0.0:18080->80/tcp, 0.0.0.0:18443->443/tcp k3d-k3s-lagoon-server + a7960981caaa lagoon/local-registry "/entrypoint.sh /etc…" 30 minutes ago Up 30 minutes 0.0.0.0:5000->5000/tcp lagoon_local-registry_1 + + ``` +2. `make k3d-kubeconfig` will print the `KUBECONFIG` env var you need to start using the cluster. + 1. Execute `export KUBECONFIG="$(./local-dev/k3d get-kubeconfig --name=$(cat k3d))"` inside the terminal. + 2. Now you should be able to use the cluster via an already installed `kubectl` or making a symbolic link to `/usr/local/bin/kubectl -> /your/path/amazee/lagoon/local-dev/kubectl` + 3. If you prefer to use something more visual you could install [k9s](https://k9scli.io/topics/install/) cli tool. + 4. Here the complete list of pods you should see with `kubectl get pod -A` + ``` + NAMESPACE NAME + kube-system local-path-provisioner + kube-system metrics-server + k8up k8up-operator + dbaas-operator kube-rbac-proxy,manager + kube-system coredns + lagoon docker-host + kube-system helm + kube-system nginx-ingress-default-backend + kube-system lb-port-80,lb-port-443 + kube-system nginx-ingress-controller + ``` + 5. Here the complete list of deployed helm [releases](https://helm.sh/docs/helm/helm_list/) you should see with `local-dev/helm/helm ls --all-namespaces`. + ``` + NAME NAMESPACE + dbaas-operator dbaas-operator + k8up k8up + lagoon-remote lagoon + mariadbprovider dbaas-operator + nginx kube-system + ``` + +## Deploy Lagoon on Kubernetes +1. TODO + +## Configure Installed Lagoon + +We have a fully running Kubernetes cluster. Now it's time to configure the first project inside of it. Follow the examples in [GraphQL API](graphql_api.md). + +## Clean up + +Clean up k3s cluster with `make k3d/stop`. + + +## Troubleshooting + +⚠ **Unable to connect to the server: x509: certificate signed by unknown authority** + +Rebuild the cluster via +``` +make k3d/stop +make k3d +``` From cc990b01de052df6bb0539a69b2dc6903a71cc38 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 26 May 2020 21:59:35 +0800 Subject: [PATCH 036/280] Bump logging-operator chart dependency --- charts/lagoon-logging/Chart.lock | 6 +++--- charts/lagoon-logging/Chart.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/charts/lagoon-logging/Chart.lock b/charts/lagoon-logging/Chart.lock index dfc19bef5c..34cf29d8f0 100644 --- a/charts/lagoon-logging/Chart.lock +++ b/charts/lagoon-logging/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com - version: 3.0.5 -digest: sha256:b5f1e93500944b39e9f49083594eaecdb4e584ec94dfcd8a38ef4c4835377e35 -generated: "2020-05-07T22:37:40.078678817+08:00" + version: 3.2.0 +digest: sha256:80ab25dccb717325b175db2187fbde6a3c608cf9f619ed88fe8de8a4aa239cef +generated: "2020-05-26T21:59:08.371456382+08:00" diff --git a/charts/lagoon-logging/Chart.yaml b/charts/lagoon-logging/Chart.yaml index 71155cb9f5..efe23b5122 100644 --- a/charts/lagoon-logging/Chart.yaml +++ b/charts/lagoon-logging/Chart.yaml @@ -23,4 +23,4 @@ appVersion: 0.1.0 dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com - version: ~3.0.5 + version: ~3.2.0 From 0db1c7bb3b41d1f0d7602efd2eeded756e08b82f Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 26 May 2020 22:03:28 +0800 Subject: [PATCH 037/280] Fix typo --- charts/lagoon-logging/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/lagoon-logging/README.md b/charts/lagoon-logging/README.md index 6fcb5088a2..1b167aed34 100644 --- a/charts/lagoon-logging/README.md +++ b/charts/lagoon-logging/README.md @@ -38,7 +38,7 @@ helm template --debug --namespace lagoon-logging -f ./lagoon-logging.values.yaml helm upgrade --dry-run --install --debug --create-namespace --namespace lagoon-logging -f ./lagoon-logging.values.yaml lagoon-logging lagoon-logging ``` -2. Run installation. +3. Run installation. ``` helm upgrade --install --debug --create-namespace --namespace lagoon-logging -f ./lagoon-logging.values.yaml lagoon-logging lagoon-logging From 13a46b6ec673720f982119065d1a0c020fb8f155 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 26 May 2020 22:06:20 +0800 Subject: [PATCH 038/280] Add upgrade command to README --- charts/lagoon-logging/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/charts/lagoon-logging/README.md b/charts/lagoon-logging/README.md index 1b167aed34..13e17f44ac 100644 --- a/charts/lagoon-logging/README.md +++ b/charts/lagoon-logging/README.md @@ -77,3 +77,11 @@ e.g. if `lagoon.sh/project: drupal-example` container-logs-drupal-example-* router-logs-drupal-example-* ``` + +## How to upgrade + +NOTE: If the `logging-operator` chart upgrade doesn't work, just uninstall the helm release and install it again. Logs won't be lost since fluentbit will send the contents of the log files once it is reinstalled. + +``` +helm upgrade --debug --namespace lagoon-logging --reuse-values lagoon-logging lagoon-logging +``` From 600d88a261ec4a04b822d4b3de4eefe07ea0b09d Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 26 May 2020 22:11:03 +0800 Subject: [PATCH 039/280] Define PodSecurityContext for OpenShift compatibility --- charts/lagoon-logging/templates/logging.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/charts/lagoon-logging/templates/logging.yaml b/charts/lagoon-logging/templates/logging.yaml index 62c6424c2d..f3e9f73a78 100644 --- a/charts/lagoon-logging/templates/logging.yaml +++ b/charts/lagoon-logging/templates/logging.yaml @@ -6,6 +6,9 @@ metadata: labels: {{- include "lagoon-logging.labels" . | nindent 4 }} spec: - fluentd: {} + fluentd: + security: + podSecurityContext: + runAsUser: 100 fluentbit: {} controlNamespace: {{ .Release.Namespace | quote }} From 60cd28b2d73a5325ac8f4e0f03bb3a268e762d37 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 26 May 2020 22:58:45 +0800 Subject: [PATCH 040/280] Document extra permissions required for OpenShift --- charts/lagoon-logging/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/charts/lagoon-logging/README.md b/charts/lagoon-logging/README.md index 13e17f44ac..62b17c7f5a 100644 --- a/charts/lagoon-logging/README.md +++ b/charts/lagoon-logging/README.md @@ -44,6 +44,22 @@ helm upgrade --dry-run --install --debug --create-namespace --namespace lagoon-l helm upgrade --install --debug --create-namespace --namespace lagoon-logging -f ./lagoon-logging.values.yaml lagoon-logging lagoon-logging ``` +**OpenShift only** + +Give the various serviceaccounts permissions required: +``` +oc project lagoon-logging + +# fluentd statefulset serviceaccount (logging-operator chart) +oc adm policy add-scc-to-user nonroot -z lagoon-logging-fluentd + +# fluentbit daemonset serviceaccount (logging-operator chart) +oc adm policy add-scc-to-user hostaccess -z lagoon-logging-fluentbit + +# logs-dispatcher statefulset serviceaccount (lagoon-logging chart) +oc adm policy add-scc-to-user anyuid -z lagoon-logging-logs-dispatcher +``` + ## View logs ### For namespaces without a lagoon.sh/project label From 514a8091710fe59552376f7413afa95833190c61 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 27 May 2020 18:52:46 +0800 Subject: [PATCH 041/280] Bump chart version --- charts/lagoon-logging/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/lagoon-logging/Chart.yaml b/charts/lagoon-logging/Chart.yaml index efe23b5122..bd90d08a54 100644 --- a/charts/lagoon-logging/Chart.yaml +++ b/charts/lagoon-logging/Chart.yaml @@ -12,7 +12,7 @@ type: application # time you make changes to the chart and its templates, including the app # version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.2.0 # This is the version number of the application being deployed. This version # number should be incremented each time you make changes to the application. From a18574ab8858b97aa5cba4900a439e64eac57e00 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 27 May 2020 21:45:23 +0800 Subject: [PATCH 042/280] Generate package for lagoon-logging chart 0.2.0 Also update index.yaml --- charts/index.yaml | 23 +++++++++++++++++++---- charts/lagoon-logging-0.2.0.tgz | Bin 0 -> 106621 bytes 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 charts/lagoon-logging-0.2.0.tgz diff --git a/charts/index.yaml b/charts/index.yaml index 82a4943898..bf5badc833 100644 --- a/charts/index.yaml +++ b/charts/index.yaml @@ -3,7 +3,22 @@ entries: lagoon-logging: - apiVersion: v2 appVersion: 0.1.0 - created: "2020-05-20T21:04:11.988795-04:00" + created: "2020-05-27T21:44:37.427234974+08:00" + dependencies: + - name: logging-operator + repository: https://kubernetes-charts.banzaicloud.com + version: ~3.2.0 + description: | + A Helm chart for Kubernetes which installs the lagoon container and router logs collection system. + digest: 94c4a3b92dad2f23f61a750d3b9b6e69084c93a6775a0051ce990c3528e90f25 + name: lagoon-logging + type: application + urls: + - lagoon-logging-0.2.0.tgz + version: 0.2.0 + - apiVersion: v2 + appVersion: 0.1.0 + created: "2020-05-27T21:44:37.420526997+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -19,7 +34,7 @@ entries: lagoon-remote: - apiVersion: v2 appVersion: 1.4.0 - created: "2020-05-20T21:04:11.990249-04:00" + created: "2020-05-27T21:44:37.428425112+08:00" description: A Helm chart to run a lagoon-remote digest: 96bc41bc9985cd6a7fbd85a32affea3bbbabdf4baa0cd829e7e3d33fb975ceeb name: lagoon-remote @@ -29,7 +44,7 @@ entries: version: 0.1.3 - apiVersion: v2 appVersion: 1.4.0 - created: "2020-05-20T21:04:11.989691-04:00" + created: "2020-05-27T21:44:37.427949322+08:00" description: A Helm chart to run a lagoon-remote digest: 5756a3fbb46a11f2f43fdcadb41d709d90c70208b90fa0257d48dcacc4df3040 name: lagoon-remote @@ -37,4 +52,4 @@ entries: urls: - lagoon-remote-0.1.2.tgz version: 0.1.2 -generated: "2020-05-20T21:04:11.982298-04:00" +generated: "2020-05-27T21:44:37.415688985+08:00" diff --git a/charts/lagoon-logging-0.2.0.tgz b/charts/lagoon-logging-0.2.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..56c9dd0a4d12aa8c775f9ca87d5bc0f339773def GIT binary patch literal 106621 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 T%_%JaaC(~SD2)Y(g8=^n;#D5u literal 0 HcmV?d00001 From 1600eee7a90c59a4f3d41b2356a106fed2eb78cf Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 29 May 2020 11:12:16 +1000 Subject: [PATCH 043/280] add active/standby to kubernetes --- Makefile | 8 +- .../build-deploy-docker-compose.sh | 129 +++++++ .../custom-ingress/templates/ingress.yaml | 1 + .../helmcharts/custom-ingress/values.yaml | 3 +- .../03-populate-api-data-kubernetes.gql | 53 ++- node-packages/commons/src/openshiftApi.js | 2 +- services/kubernetesbuilddeploy/src/index.js | 18 +- .../src/handlers/ingressMigration.ts | 317 ++++++++++++++++++ services/kubernetesmisc/src/index.ts | 5 + tests/tests/active-standby-kubernetes.yaml | 13 + ...dby.yaml => active-standby-openshift.yaml} | 0 11 files changed, 543 insertions(+), 6 deletions(-) create mode 100644 services/kubernetesmisc/src/handlers/ingressMigration.ts create mode 100644 tests/tests/active-standby-kubernetes.yaml rename tests/tests/{active-standby.yaml => active-standby-openshift.yaml} (100%) diff --git a/Makefile b/Makefile index 4e9a8e022a..d36e7a7e96 100644 --- a/Makefile +++ b/Makefile @@ -530,7 +530,8 @@ build-list: # Define list of all tests all-k8s-tests-list:= features-kubernetes \ nginx \ - drupal + drupal \ + active-standby-kubernetes all-k8s-tests = $(foreach image,$(all-k8s-tests-list),k8s-tests/$(image)) # Run all k8s tests @@ -577,7 +578,7 @@ all-openshift-tests-list:= features-openshift \ bitbucket \ nginx \ elasticsearch \ - active-standby + active-standby-openshift all-openshift-tests = $(foreach image,$(all-openshift-tests-list),openshift-tests/$(image)) .PHONY: openshift-tests @@ -1028,6 +1029,9 @@ endif local-dev/kubectl create namespace k8up; \ local-dev/helm/helm repo add appuio https://charts.appuio.ch; \ local-dev/helm/helm upgrade --install -n k8up k8up appuio/k8up; \ + local-dev/kubectl create namespace dioscuri; \ + local-dev/helm/helm repo add dioscuri https://raw.githubusercontent.com/amazeeio/dioscuri/ingress/charts ; \ + local-dev/helm/helm upgrade --install -n dioscuri dioscuri dioscuri/dioscuri ; \ local-dev/kubectl create namespace dbaas-operator; \ local-dev/helm/helm repo add dbaas-operator https://raw.githubusercontent.com/amazeeio/dbaas-operator/master/charts ; \ local-dev/helm/helm upgrade --install -n dbaas-operator dbaas-operator dbaas-operator/dbaas-operator ; \ diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index c743443bb1..8d201b2887 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -459,6 +459,119 @@ TEMPLATE_PARAMETERS=() ### CUSTOM ROUTES FROM .lagoon.yml ############################################## +ROUTES_SERVICE_COUNTER=0 +# we need to check for production routes for active/standby if they are defined, as these will get migrated between environments as required +if [ "${ENVIRONMENT_TYPE}" == "production" ]; then + if [ "${BRANCH//./\\.}" == "${ACTIVE_ENVIRONMENT}" ]; then + if [ -n "$(cat .lagoon.yml | shyaml keys production_routes.active.routes.$ROUTES_SERVICE_COUNTER 2> /dev/null)" ]; then + while [ -n "$(cat .lagoon.yml | shyaml keys production_routes.active.routes.$ROUTES_SERVICE_COUNTER 2> /dev/null)" ]; do + ROUTES_SERVICE=$(cat .lagoon.yml | shyaml keys production_routes.active.routes.$ROUTES_SERVICE_COUNTER) + + ROUTE_DOMAIN_COUNTER=0 + while [ -n "$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER 2> /dev/null)" ]; do + # Routes can either be a key (when the have additional settings) or just a value + if cat .lagoon.yml | shyaml keys production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER &> /dev/null; then + ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml keys production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) + # Route Domains include dots, which need to be esacped via `\.` in order to use them within shyaml + ROUTE_DOMAIN_ESCAPED=$(cat .lagoon.yml | shyaml keys production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER | sed 's/\./\\./g') + ROUTE_TLS_ACME=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.tls-acme true) + ROUTE_MIGRATE=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.migrate true) + ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) + ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) + else + # Only a value given, assuming some defaults + ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) + ROUTE_TLS_ACME=true + ROUTE_MIGRATE=true + ROUTE_INSECURE=Redirect + ROUTE_HSTS=null + fi + + touch /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml + echo "$ROUTE_ANNOTATIONS" | yq p - annotations > /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml + + # The very first found route is set as MAIN_CUSTOM_ROUTE + if [ -z "${MAIN_CUSTOM_ROUTE+x}" ]; then + MAIN_CUSTOM_ROUTE=$ROUTE_DOMAIN + fi + + ROUTE_SERVICE=$ROUTES_SERVICE + + cat /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml + + helm template ${ROUTE_DOMAIN} \ + /kubectl-build-deploy/helmcharts/custom-ingress \ + --set host="${ROUTE_DOMAIN}" \ + --set service="${ROUTE_SERVICE}" \ + --set tls_acme="${ROUTE_TLS_ACME}" \ + --set insecure="${ROUTE_INSECURE}" \ + --set hsts="${ROUTE_HSTS}" \ + --set routeMigrate="${ROUTE_MIGRATE}" \ + -f /kubectl-build-deploy/values.yaml -f /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml "${HELM_ARGUMENTS[@]}" | outputToYaml + + let ROUTE_DOMAIN_COUNTER=ROUTE_DOMAIN_COUNTER+1 + done + + let ROUTES_SERVICE_COUNTER=ROUTES_SERVICE_COUNTER+1 + done + fi + fi + if [ "${BRANCH//./\\.}" == "${STANDBY_ENVIRONMENT}" ]; then + if [ -n "$(cat .lagoon.yml | shyaml keys production_routes.standby.routes.$ROUTES_SERVICE_COUNTER 2> /dev/null)" ]; then + while [ -n "$(cat .lagoon.yml | shyaml keys production_routes.standby.routes.$ROUTES_SERVICE_COUNTER 2> /dev/null)" ]; do + ROUTES_SERVICE=$(cat .lagoon.yml | shyaml keys production_routes.standby.routes.$ROUTES_SERVICE_COUNTER) + + ROUTE_DOMAIN_COUNTER=0 + while [ -n "$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER 2> /dev/null)" ]; do + # Routes can either be a key (when the have additional settings) or just a value + if cat .lagoon.yml | shyaml keys production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER &> /dev/null; then + ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml keys production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) + # Route Domains include dots, which need to be esacped via `\.` in order to use them within shyaml + ROUTE_DOMAIN_ESCAPED=$(cat .lagoon.yml | shyaml keys production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER | sed 's/\./\\./g') + ROUTE_TLS_ACME=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.tls-acme true) + ROUTE_MIGRATE=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.migrate true) + ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) + ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) + else + # Only a value given, assuming some defaults + ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) + ROUTE_TLS_ACME=true + ROUTE_MIGRATE=true + ROUTE_INSECURE=Redirect + ROUTE_HSTS=null + fi + + touch /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml + echo "$ROUTE_ANNOTATIONS" | yq p - annotations > /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml + + # The very first found route is set as MAIN_CUSTOM_ROUTE + if [ -z "${MAIN_CUSTOM_ROUTE+x}" ]; then + MAIN_CUSTOM_ROUTE=$ROUTE_DOMAIN + fi + + ROUTE_SERVICE=$ROUTES_SERVICE + + cat /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml + + helm template ${ROUTE_DOMAIN} \ + /kubectl-build-deploy/helmcharts/custom-ingress \ + --set host="${ROUTE_DOMAIN}" \ + --set service="${ROUTE_SERVICE}" \ + --set tls_acme="${ROUTE_TLS_ACME}" \ + --set insecure="${ROUTE_INSECURE}" \ + --set hsts="${ROUTE_HSTS}" \ + --set routeMigrate="${ROUTE_MIGRATE}" \ + -f /kubectl-build-deploy/values.yaml -f /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml "${HELM_ARGUMENTS[@]}" | outputToYaml + + let ROUTE_DOMAIN_COUNTER=ROUTE_DOMAIN_COUNTER+1 + done + + let ROUTES_SERVICE_COUNTER=ROUTES_SERVICE_COUNTER+1 + done + fi + fi +fi + # Two while loops as we have multiple services that want routes and each service has multiple routes ROUTES_SERVICE_COUNTER=0 if [ -n "$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER 2> /dev/null)" ]; then @@ -473,6 +586,7 @@ if [ -n "$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\. # Route Domains include dots, which need to be esacped via `\.` in order to use them within shyaml ROUTE_DOMAIN_ESCAPED=$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER | sed 's/\./\\./g') ROUTE_TLS_ACME=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.tls-acme true) + ROUTE_MIGRATE=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.migrate false) ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) ROUTE_ANNOTATIONS=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.annotations {}) @@ -480,6 +594,7 @@ if [ -n "$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\. # Only a value given, assuming some defaults ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) ROUTE_TLS_ACME=true + ROUTE_MIGRATE=false ROUTE_INSECURE=Redirect ROUTE_HSTS=null ROUTE_ANNOTATIONS="{}" @@ -504,6 +619,7 @@ if [ -n "$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\. --set tls_acme="${ROUTE_TLS_ACME}" \ --set insecure="${ROUTE_INSECURE}" \ --set hsts="${ROUTE_HSTS}" \ + --set routeMigrate="${ROUTE_MIGRATE}" \ -f /kubectl-build-deploy/values.yaml -f /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml "${HELM_ARGUMENTS[@]}" | outputToYaml let ROUTE_DOMAIN_COUNTER=ROUTE_DOMAIN_COUNTER+1 @@ -523,6 +639,7 @@ else # Route Domains include dots, which need to be esacped via `\.` in order to use them within shyaml ROUTE_DOMAIN_ESCAPED=$(cat .lagoon.yml | shyaml keys environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER | sed 's/\./\\./g') ROUTE_TLS_ACME=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.tls-acme true) + ROUTE_MIGRATE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.migrate false) ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) ROUTE_ANNOTATIONS=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.annotations {}) @@ -530,6 +647,7 @@ else # Only a value given, assuming some defaults ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) ROUTE_TLS_ACME=true + ROUTE_MIGRATE=false ROUTE_INSECURE=Redirect ROUTE_HSTS=null ROUTE_ANNOTATIONS="{}" @@ -554,6 +672,7 @@ else --set tls_acme="${ROUTE_TLS_ACME}" \ --set insecure="${ROUTE_INSECURE}" \ --set hsts="${ROUTE_HSTS}" \ + --set routeMigrate="${ROUTE_MIGRATE}" \ -f /kubectl-build-deploy/values.yaml -f /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml "${HELM_ARGUMENTS[@]}" | outputToYaml let ROUTE_DOMAIN_COUNTER=ROUTE_DOMAIN_COUNTER+1 @@ -635,6 +754,16 @@ fi # Load all routes with correct schema and comma separated ROUTES=$(kubectl -n ${NAMESPACE} get ingress --sort-by='{.metadata.name}' -l "acme.openshift.io/exposer!=true" -o=go-template --template='{{range $indexItems, $ingress := .items}}{{if $indexItems}},{{end}}{{$tls := .spec.tls}}{{range $indexRule, $rule := .spec.rules}}{{if $indexRule}},{{end}}{{if $tls}}https://{{else}}http://{{end}}{{.host}}{{end}}{{end}}') +# Active / Standby routes +ACTIVE_ROUTES="" +STANDBY_ROUTES="" +if [ "${BRANCH//./\\.}" == "${ACTIVE_ENVIRONMENT}" ]; then +ACTIVE_ROUTES=$(kubectl -n ${NAMESPACE} get ingress --sort-by='{.metadata.name}' -l "dioscuri.amazee.io/migrate=true" -o=go-template --template='{{range $indexItems, $ingress := .items}}{{if $indexItems}},{{end}}{{$tls := .spec.tls}}{{range $indexRule, $rule := .spec.rules}}{{if $indexRule}},{{end}}{{if $tls}}https://{{else}}http://{{end}}{{.host}}{{end}}{{end}}') +fi +if [ "${BRANCH//./\\.}" == "${STANDBY_ENVIRONMENT}" ]; then +STANDBY_ROUTES=$(kubectl -n ${NAMESPACE} get ingress --sort-by='{.metadata.name}' -l "dioscuri.amazee.io/migrate=true" -o=go-template --template='{{range $indexItems, $ingress := .items}}{{if $indexItems}},{{end}}{{$tls := .spec.tls}}{{range $indexRule, $rule := .spec.rules}}{{if $indexRule}},{{end}}{{if $tls}}https://{{else}}http://{{end}}{{.host}}{{end}}{{end}}') +fi + # Get list of autogenerated routes AUTOGENERATED_ROUTES=$(kubectl -n ${NAMESPACE} get ingress --sort-by='{.metadata.name}' -l "lagoon/autogenerated=true" -o=go-template --template='{{range $indexItems, $ingress := .items}}{{if $indexItems}},{{end}}{{$tls := .spec.tls}}{{range $indexRule, $rule := .spec.rules}}{{if $indexRule}},{{end}}{{if $tls}}https://{{else}}http://{{end}}{{.host}}{{end}}{{end}}') diff --git a/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/templates/ingress.yaml b/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/templates/ingress.yaml index e26a8dac9c..157e46e0b7 100644 --- a/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/templates/ingress.yaml +++ b/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/templates/ingress.yaml @@ -8,6 +8,7 @@ metadata: name: {{ include "custom-ingress.fullname" . }} labels: lagoon/autogenerated: "false" + dioscuri.amazee.io/migrate: {{ .Values.routeMigrate | quote }} {{- include "custom-ingress.labels" . | nindent 4 }} annotations: # force-ssl-redirect handling diff --git a/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/values.yaml b/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/values.yaml index 1190022f2d..d262a11e8f 100644 --- a/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/values.yaml +++ b/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/values.yaml @@ -7,4 +7,5 @@ hsts: 'null' tls_acme: true insecure: Allow service: '' -annotations: {} \ No newline at end of file +annotations: {} +routeMigrate: false \ No newline at end of file diff --git a/local-dev/api-data/03-populate-api-data-kubernetes.gql b/local-dev/api-data/03-populate-api-data-kubernetes.gql index e47b814673..1cc64742ff 100644 --- a/local-dev/api-data/03-populate-api-data-kubernetes.gql +++ b/local-dev/api-data/03-populate-api-data-kubernetes.gql @@ -9,7 +9,7 @@ mutation PopulateApi { routerPattern: "${project}.${environment}.172.17.0.1.xip.io" sshHost: "172.17.0.1" sshPort: "2020" - token: "eyJhbGciOiJSUzI1NiIsImtpZCI6Im1zRTZVZkdubzliaHR4d09sWkFBUm9lWnd3QjkzdkVQSHc1OEMzd1BvX1kifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJsYWdvb24iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoia3ViZXJuZXRlc2J1aWxkZGVwbG95LXRva2VuLXc4Mm45Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Imt1YmVybmV0ZXNidWlsZGRlcGxveSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjliYzQ2MjUxLWEwYjktNDU2Yi1hOGM0LTY1MjI0MDQxMmRmYyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpsYWdvb246a3ViZXJuZXRlc2J1aWxkZGVwbG95In0.mzptDZaVbOht73rb-afsiON2bHoiynN84lVuLkjqPWbib5xwA9OCyLht-Y4WO4RhOFj0JKPrHToBYAhn9Y1ikLqcVsiv2yhQGHlGA_bugcYkcrTjgdkjAZIZvd63qR6Jl8G00pWJQtgB7y2z9lw36XM4iarxlt9WtpDlFi2tRnaJgnpgwIjIjKah--H1_hg5r-GTkABbK79kdyZBrR4hY9C1kZAsCMO1YEnXSq1F41f-sd2dlNoohSmwGxntNN3Yiaemw9UyM6aqWy7L41_qoOHCg8it4HlVzFcXOQAxNTYi3wq5BSDwGTfusev9xnoMyqtcxlMs8KF3yDrPCjdpiw" # make-kubernetes-token + token: "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9URmloeU9MbTVsN0xrbU1jSDl5N1BwRFpaYzBqVU1WRDQ4VDRIZDZQdU0ifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJsYWdvb24iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoia3ViZXJuZXRlc2J1aWxkZGVwbG95LXRva2VuLWh2d2ttIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Imt1YmVybmV0ZXNidWlsZGRlcGxveSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImRjMGYyMTYwLTI4N2ItNGE2Yy1hMzBhLWQ2NjExOWNkY2EwYyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpsYWdvb246a3ViZXJuZXRlc2J1aWxkZGVwbG95In0.L9ZmyhQZwTp-_I8fIpjSS448JFBI5uDao68dora6Pva5op3LEkP9GJ768VLYFxpbpUl6SHT5W0OtD-sYoLYGPPniegPTRKWu2enm6mBKemvKKg_57QD_cdE2mqllHFHWsFWRKl_NVIZ1yuYSq8q8e-IZD33Th0szcnUB3XJofTcYSJQI6ZmvOQL7rBsYb9IAygAewDvAJs6ZL9K0WtW3ALoCOQgIIPpS87fmxjWjrbvLQf6x0qZO4AUhuk9N5QV3aQNno6x3tB-W2GSFkSbq9tTVPbiWWFeRxyiE-1A2T-cOFX-aEbzO8rYwVhGiNF9Xpz3q1fmiVuoGcCtXn4Lrmw" # make-kubernetes-token } ) { id @@ -1119,4 +1119,55 @@ mutation PopulateApi { ) { id } + + CiActiveStandby: addProject( + input: { + id: 1024 + name: "ci-active-standby-k8s" + openshift: 1002 + gitUrl: "ssh://git@192.168.42.1:2222/git/active-standby.git" + productionEnvironment:"master-a" + standbyProductionEnvironment:"master-b" + activeSystemsTask: "lagoon_kubernetesJob" + activeSystemsMisc: "lagoon_kubernetesMisc" + activeSystemsDeploy: "lagoon_kubernetesBuildDeploy" + activeSystemsRemove: "lagoon_kubernetesRemove" + privateKey: "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----" + } + ) { + id + } + CiActiveStandbyGroup3: addGroupsToProject( + input: { + project: { + name: "ci-active-standby-k8s" + } + groups: [ + { + name: "ci-group" + } + ] + } + ) { + id + } + CiActiveStandbyRocketChat: addNotificationToProject( + input: { + project: "ci-active-standby-k8s" + notificationType: ROCKETCHAT + notificationName: "amazeeio--lagoon-local-ci" + } + ) { + id + } + CiActiveStandbyEmail: addNotificationToProject( + input: { + project: "ci-active-standby-k8s" + notificationType: EMAIL + notificationName: "local-email-testing" + } + ) { + id + } + } diff --git a/node-packages/commons/src/openshiftApi.js b/node-packages/commons/src/openshiftApi.js index 169fc76702..27927b9522 100644 --- a/node-packages/commons/src/openshiftApi.js +++ b/node-packages/commons/src/openshiftApi.js @@ -20,7 +20,7 @@ class RouteMigration extends ApiGroup { path: 'apis/dioscuri.amazee.io', version: options.version || 'v1', groupResources: [], - namespaceResources: ['routemigrates'], + namespaceResources: ['routemigrates','ingressmigrates'], }); super(options); } diff --git a/services/kubernetesbuilddeploy/src/index.js b/services/kubernetesbuilddeploy/src/index.js index 319a94f970..f24ee83e49 100644 --- a/services/kubernetesbuilddeploy/src/index.js +++ b/services/kubernetesbuilddeploy/src/index.js @@ -55,7 +55,13 @@ const messageConsumer = async msg => { environmentName = environmentName.concat('-' + hash) } - var environmentType = branch === projectOpenShift.productionEnvironment ? 'production' : 'development'; + var environmentType = 'development' + if ( + projectOpenShift.productionEnvironment === environmentName + || projectOpenShift.standbyProductionEnvironment === environmentName + ) { + environmentType = 'production' + } var gitSha = sha var projectId = projectOpenShift.id var openshiftConsole = projectOpenShift.openshift.consoleUrl.replace(/\/$/, ""); @@ -65,6 +71,8 @@ const messageConsumer = async msg => { var openshiftProjectUser = projectOpenShift.openshift.projectUser || "" var deployPrivateKey = projectOpenShift.privateKey var gitUrl = projectOpenShift.gitUrl + var projectProductionEnvironment = projectOpenShift.productionEnvironment + var projectStandbyEnvironment = projectOpenShift.standbyProductionEnvironment var subfolder = projectOpenShift.subfolder || "" var routerPattern = projectOpenShift.openshift.routerPattern ? projectOpenShift.openshift.routerPattern.replace('${environment}',environmentName).replace('${project}', projectName) : "" var prHeadBranch = headBranch || "" @@ -213,6 +221,14 @@ const messageConsumer = async msg => { "name": "ENVIRONMENT_TYPE", "value": environmentType }, + { + "name": "ACTIVE_ENVIRONMENT", + "value": projectProductionEnvironment + }, + { + "name": "STANDBY_ENVIRONMENT", + "value": projectStandbyEnvironment + }, { "name": "KUBERNETES", "value": openshiftName diff --git a/services/kubernetesmisc/src/handlers/ingressMigration.ts b/services/kubernetesmisc/src/handlers/ingressMigration.ts new file mode 100644 index 0000000000..2cd7bb44d0 --- /dev/null +++ b/services/kubernetesmisc/src/handlers/ingressMigration.ts @@ -0,0 +1,317 @@ +// import { promisify } from 'util'; +// import KubernetesClient from 'kubernetes-client'; +// import R from 'ramda'; +// import { logger } from '@lagoon/commons/dist/local-logging'; +// import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +// import { +// getOpenShiftInfoForProject, +// updateProject, +// updateTask, +// } from '@lagoon/commons/dist/api'; +// import { RouteMigration } from '@lagoon/commons/dist/openshiftApi'; +// const convertDateFormat = R.init; +const promisify = require("util").promisify; +const R = require("ramda"); +const {logger} = require("@lagoon/commons/src/local-logging"); +const {sendToLagoonLogs} = require("@lagoon/commons/src/logs"); +const { + getOpenShiftInfoForProject, + updateProject, + updateTask, +} = require("@lagoon/commons/src/api"); +const {RouteMigration} = require("@lagoon/commons/src/openshiftApi"); +const convertDateFormat = R.init; + +import Api, { ClientConfiguration } from 'kubernetes-client'; +const Client = Api.Client1_13; + +const getConfig = (url, token) => ({ + url, + insecureSkipTlsVerify: true, + auth: { + bearer: token + } +}); + +const pause = duration => new Promise(res => setTimeout(res, duration)); +const retry = (retries, fn, delay = 1000) => + fn().catch( + err => + retries > 1 + ? pause(delay).then(() => retry(retries - 1, fn, delay)) + : Promise.reject(err) + ); + +export async function ingressMigration (data) { + const { projectName, productionEnvironment, standbyProductionEnvironment, task } = data; + + const result = await getOpenShiftInfoForProject(projectName); + const projectOpenShift = result.project; + const ocsafety = string => + string.toLocaleLowerCase().replace(/[^0-9a-z-]/g, '-'); + + try { + var safeActiveProductionEnvironment = ocsafety(productionEnvironment); + var safeStandbyProductionEnvironment = ocsafety(standbyProductionEnvironment); + var safeProjectName = ocsafety(projectName); + var openshiftConsole = projectOpenShift.openshift.consoleUrl.replace( + /\/$/, + '' + ); + var openshiftToken = projectOpenShift.openshift.token || ''; + var openshiftProject = projectOpenShift.openshiftProjectPattern + ? projectOpenShift.openshiftProjectPattern + .replace('${branch}', safeActiveProductionEnvironment) + .replace('${project}', safeProjectName) + : `${safeProjectName}-${safeActiveProductionEnvironment}`; + // create the destination openshift project name + var destinationOpenshiftProject = projectOpenShift.openshiftProjectPattern + ? projectOpenShift.openshiftProjectPattern + .replace('${branch}', safeStandbyProductionEnvironment) + .replace('${project}', safeProjectName) + : `${safeProjectName}-${safeStandbyProductionEnvironment}`; + } catch (error) { + logger.error(`Error while loading information for project ${projectName}`); + logger.error(error); + throw error; + } + + // define the routemigration. the annotation being set to true is what actually triggers the switch + const migrateRoutes = (openshiftProject, destinationOpenshiftProject) => { + let config = { + apiVersion: 'dioscuri.amazee.io/v1', + kind: 'IngressMigrate', + metadata: { + name: openshiftProject, + annotations: { + 'dioscuri.amazee.io/migrate':'true' + } + }, + spec: { + destinationNamespace: destinationOpenshiftProject, + activeEnvironment: safeActiveProductionEnvironment, + }, + }; + + return config; + }; + + // Kubernetes API Object - needed as some API calls are done to the Kubernetes API part of OpenShift and + // the OpenShift API does not support them. + const dioscuri: any = new RouteMigration({ + url: openshiftConsole, + insecureSkipTlsVerify: true, + auth: { + bearer: openshiftToken + } + }); + + const config: ClientConfiguration = getConfig(openshiftConsole, openshiftToken); + const client = new Client({ config }); + + const routeMigratesGet = promisify( + dioscuri.ns(openshiftProject).ingressmigrates.get + ); + + const routeMigrateDelete = async name => { + const deleteFn = promisify(dioscuri.ns(openshiftProject).ingressmigrates(openshiftProject).delete); + return deleteFn({ + body: {} + }); + }; + + const hasNoRouteMigrate = () => + new Promise(async (resolve, reject) => { + const routeMigrates = await routeMigratesGet(); + if (routeMigrates.items.length === 0) { + logger.info(`${openshiftProject}: RouteMigrate deleted`); + resolve(); + } else { + logger.info( + `${openshiftProject}: RouteMigrate not deleted yet, will try again in 2sec` + ); + reject(); + } + }); + + const projectExists = async (client, namespace) => { + const namespaces = await client.api.v1.namespaces(namespace).get(); + if ( + namespaces.statusCode !== 200 && + namespaces.body.metadata.name !== namespace + ) { + return false; + } + + return true; + }; + + if (!(await projectExists(client, openshiftProject))) { + logger.error(`Project ${openshiftProject} does not exist, bailing`); + return; + } + if (!(await projectExists(client, destinationOpenshiftProject))) { + logger.error(`Project ${destinationOpenshiftProject} does not exist, bailing`); + return; + } + + // check if there is already a route migrate resource, delete it if there is + try { + const routeMigrates = await routeMigratesGet(); + for (let routeMigrate of routeMigrates.items) { + await routeMigrateDelete(routeMigrate.metadata.name); + logger.info( + `${openshiftProject}: Deleting RouteMigrate ${routeMigrate.metadata.name}` + ); + } + // RouteMigrates are deleted quickly, but we still have to wait before we attempt to create the new one + try { + await retry(10, hasNoRouteMigrate, 2000); + } catch (err) { + throw new Error( + `${openshiftProject}: RouteMigrate not deleted` + ); + } + } catch (err) { + logger.info(`${openshiftProject}: RouteMigrate doesn't exist`); // proceed if it doesn't exist + } + + // add the routemigrate resource + try { + const routeMigratePost = promisify( + dioscuri.ns(openshiftProject).ingressmigrates.post + ); + await routeMigratePost({ + body: migrateRoutes(openshiftProject, destinationOpenshiftProject) + }); + logger.verbose(`${openshiftProject}: RouteMigrate resource created`); + } catch (err) { + logger.error(err); + throw new Error(); + } + + sendToLagoonLogs( + 'info', + projectName, + '', + 'task:misc-openshift:route:migrate', + data, + `*[${projectName}]* Route Migration between environments *${destinationOpenshiftProject}* started` + ); + + const routeMigrateGet = promisify( + dioscuri.ns(openshiftProject).ingressmigrates(openshiftProject).get + ); + + // this will check the resource in openshift, then updates the task in the api + const updateActiveStandbyTask = () => { + return (new Promise(async (resolve, reject) => { + let exitResolve = false; + const routeMigrateStatus = await routeMigrateGet(); + if (routeMigrateStatus === undefined || routeMigrateStatus.status === undefined || routeMigrateStatus.status.conditions === undefined) { + logger.info(`${openshiftProject}: active/standby switch not ready, will try again in 2sec, RETRY`); + } else { + for (let i = 0; i < routeMigrateStatus.status.conditions.length; i++) { + switch (routeMigrateStatus.status.conditions[i].type ) { + case 'started': + // update the task to started + var created = convertDateFormat(routeMigrateStatus.status.conditions[i].lastTransitionTime) + await updateTask(task.id, { + status: 'ACTIVE', + created: created, + }); + break; + case 'failed': + // update the task to failed + var created = convertDateFormat(routeMigrateStatus.status.conditions[i].lastTransitionTime) + await updateTask(task.id, { + status: 'FAILED', + completed: created, + }); + var condition: any = new Object(); + // send a log off with the status information + condition.condition = routeMigrateStatus.status.conditions[i].condition + condition.activeRoutes = routeMigrateStatus.spec.ingress.activeIngress + condition.standbyRoutes = routeMigrateStatus.spec.ingress.standbyIngress + var conditionStr= JSON.stringify(condition); + await saveTaskLog( + 'active-standby-switch', + projectOpenShift.name, + 'failed', + task.uuid, + conditionStr, + ); + logger.info(`${openshiftProject}: active/standby switch failed`); + exitResolve = true; + break; + case 'completed': + // swap the active/standby in lagoon by updating the project + const response = await updateProject(projectOpenShift.id, { + productionEnvironment: safeStandbyProductionEnvironment, + standbyProductionEnvironment: safeActiveProductionEnvironment, + productionRoutes: routeMigrateStatus.spec.ingress.activeIngress, + standbyRoutes: routeMigrateStatus.spec.ingress.standbyIngress, + }); + // update the task to completed + var created = convertDateFormat(routeMigrateStatus.status.conditions[i].lastTransitionTime) + await updateTask(task.id, { + status: 'SUCCEEDED', + completed: created, + }); + // send a log off with the status information + var condition: any = new Object(); + condition.condition = routeMigrateStatus.status.conditions[i].condition + condition.activeRoutes = routeMigrateStatus.spec.ingress.activeIngress + condition.standbyRoutes = routeMigrateStatus.spec.ingress.standbyIngress + var conditionStr= JSON.stringify(condition); + await saveTaskLog( + 'active-standby-switch', + projectOpenShift.name, + 'succeeded', + task.uuid, + conditionStr, + ); + logger.info(`${openshiftProject}: active/standby switch completed`); + exitResolve = true; + break; + } + } + } + // handle the exit here + if (exitResolve == true) { + resolve(); + } else { + logger.info(`${openshiftProject}: active/standby switch not ready, will try again in 2sec, REJECT`); + reject(); + } + })); + } + + try { + // actually run the task that updates the task + await retry(10, updateActiveStandbyTask, 2000); + } catch (err) { + throw new Error( + `${openshiftProject}: active/standby task is taking too long ${err}` + ); + } +} + +const saveTaskLog = async (jobName, projectName, status, uid, log) => { + const meta = { + jobName, + jobStatus: status, + remoteId: uid + }; + + sendToLagoonLogs( + 'info', + projectName, + '', + `task:misc-kubernetes:route:migrate:${jobName}`, + meta, + log + ); +}; + +export default ingressMigration; \ No newline at end of file diff --git a/services/kubernetesmisc/src/index.ts b/services/kubernetesmisc/src/index.ts index b792f0c6ec..ed714c4ddc 100644 --- a/services/kubernetesmisc/src/index.ts +++ b/services/kubernetesmisc/src/index.ts @@ -4,6 +4,7 @@ import { consumeTasks, initSendToLagoonTasks } from '@lagoon/commons/src/tasks'; import resticRestore from './handlers/resticRestore'; import kubernetesBuildCancel from "./handlers/kubernetesBuildCancel"; +import ingressMigration from "./handlers/ingressMigration"; initSendToLagoonLogs(); initSendToLagoonTasks(); @@ -25,6 +26,10 @@ const messageConsumer = async msg => { kubernetesBuildCancel(data); break; + case 'kubernetes:route:migrate': + ingressMigration(data); + break; + default: const meta = { msg: JSON.parse(msg.content.toString()), diff --git a/tests/tests/active-standby-kubernetes.yaml b/tests/tests/active-standby-kubernetes.yaml new file mode 100644 index 0000000000..881b637095 --- /dev/null +++ b/tests/tests/active-standby-kubernetes.yaml @@ -0,0 +1,13 @@ +--- + +- include: features/api-token.yaml + vars: + testname: "API TOKEN" + +- include: active-standby/active-standby.yaml + vars: + testname: "ACTIVE_STANDBY" + git_repo_name: active-standby.git + project: ci-active-standby-k8s + branch: master-a + standby_branch: master-b \ No newline at end of file diff --git a/tests/tests/active-standby.yaml b/tests/tests/active-standby-openshift.yaml similarity index 100% rename from tests/tests/active-standby.yaml rename to tests/tests/active-standby-openshift.yaml From ccb9e0b21d970707c80c20d541b529c3f8e4ed7b Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Fri, 29 May 2020 13:10:43 -0400 Subject: [PATCH 044/280] Billing Invoice UI Tweaks --- .../components/BillingGroupInvoice/index.js | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/services/ui/src/components/BillingGroupInvoice/index.js b/services/ui/src/components/BillingGroupInvoice/index.js index 58dab72bb4..328a1b8d99 100644 --- a/services/ui/src/components/BillingGroupInvoice/index.js +++ b/services/ui/src/components/BillingGroupInvoice/index.js @@ -34,11 +34,11 @@ const Invoice = ({ cost, language }) => { setLang(value); } - + return (

- +

Invoice

@@ -55,7 +55,7 @@ const Invoice = ({ cost, language }) => { - +
@@ -64,28 +64,29 @@ const Invoice = ({ cost, language }) => {
{ lang === LANGS.ENGLISH ? `Unit Price` : `Einzelpreis` }
{ lang === LANGS.ENGLISH ? `Amount ${cost.currency}` : `Preis in ${cost.currency}` }
- +
- { lang === LANGS.ENGLISH ? + { lang === LANGS.ENGLISH ?
Monthly Hosting Fee for { cost.availability } Availability Environment
PHP CMS Bundle: {currencyChar} {cost.environmentCostDescription.prod.unitPrice} per h
- : + :
Monatliche Hostinggebühr im { cost.availability } Availability Environment
PHP CMS Bundle: {currencyChar} {cost.environmentCostDescription.prod.unitPrice} pro Stunde
} + Production Environment(s):
{cost.environmentCostDescription.prod.description.projects.map(({name, hours}, index) => (
{name} - {hours} { lang === LANGS.ENGLISH ? `h` : `Std.` }
)) }
- - { lang === LANGS.ENGLISH ? + + { lang === LANGS.ENGLISH ?
- Total hours: {cost.environmentCostDescription.prod.quantity.toFixed(2).toLocaleString()} h + Total hours: {cost.environmentCostDescription.prod.quantity} h
:
@@ -101,30 +102,31 @@ const Invoice = ({ cost, language }) => {
- { lang === LANGS.ENGLISH ? + { lang === LANGS.ENGLISH ?
Monthly Hits Fee for { cost.availability } Availability Environment
- : + :
Monatliche Gebühren für Hits im { cost.availability } Availability Environment
} + { lang === LANGS.ENGLISH ? `Hits per Production Environment:` : `Hits pro Production Environment:` }
{cost.hitCostDescription.description.projects.map(({name, hits}, index) => (
{name} - {hits.toLocaleString()}
)) }
- { lang === LANGS.ENGLISH ? + { lang === LANGS.ENGLISH ?
Combined Hits: {cost.hitCostDescription.description.total.toLocaleString()}
- : + :
Hits Total: {cost.hitCostDescription.description.total.toLocaleString()}
} - +
1.00
{cost.hitCost.toFixed(2)}
@@ -134,12 +136,12 @@ const Invoice = ({ cost, language }) => {
- { lang === LANGS.ENGLISH ? + { lang === LANGS.ENGLISH ?
Additional Storage Fee
Storage per GB/day: {currencyChar} {cost.storageCostDescription.unitPrice}
- : + :
Zusätzliche Storagegebühren
Storage GB/Tag: {currencyChar} {cost.storageCostDescription.unitPrice}
@@ -151,7 +153,7 @@ const Invoice = ({ cost, language }) => { {cost.storageCostDescription.description.projects.map(({name, storage}, index) => (
{name} - {storage.toFixed(2)} GB
)) }
- { lang === LANGS.ENGLISH ? + { lang === LANGS.ENGLISH ?
Total Storage: {cost.storageCostDescription.quantity.toFixed(2).toLocaleString()} GB
Included Storage: {cost.storageCostDescription.description.included.toFixed(2).toLocaleString()} GB
@@ -165,7 +167,7 @@ const Invoice = ({ cost, language }) => {
}
-
{cost.storageCostDescription.description.additional.toFixed(2).toLocaleString()}
+
{cost.storageCostDescription.description.qty.toFixed(2).toLocaleString()}
{cost.storageCostDescription.unitPrice}
{cost.storageCost.toFixed(2)}
@@ -173,7 +175,7 @@ const Invoice = ({ cost, language }) => {
- { lang === LANGS.ENGLISH ? + { lang === LANGS.ENGLISH ?
Additional Development Environments
DEV Environment: {currencyChar} {cost.environmentCostDescription.dev.unitPrice} per hour
@@ -188,22 +190,24 @@ const Invoice = ({ cost, language }) => {
{ cost.environmentCostDescription.dev.description.projects.map(({name, hours, additional, included}, index) => ( -
- {name} - {hours} { lang === LANGS.ENGLISH ? `h` : `Std.` } -
Included hours - {included} { lang === LANGS.ENGLISH ? `h` : `Std.` }
- { additional !== 0 &&
{ lang === LANGS.ENGLISH ? `Additional hours` : `Zusätzliche Stunden` } - {additional} { lang === LANGS.ENGLISH ? `h` : `Std.` }
} -
) - ) + additional > 0 && +
+ {name} - {hours} { lang === LANGS.ENGLISH ? `h` : `Std.` } +
Included hours - {included} { lang === LANGS.ENGLISH ? `h` : `Std.` }
+ { additional !== 0 &&
{ lang === LANGS.ENGLISH ? `Additional hours` : `Zusätzliche Stunden` } - {additional} { lang === LANGS.ENGLISH ? `h` : `Std.` }
} +
+ ) + ) }
- { lang === LANGS.ENGLISH ? + { lang === LANGS.ENGLISH ?
- Total additional hours: {cost.environmentCostDescription.dev.quantity.toFixed(2).toLocaleString()} h + Total additional hours: {cost.environmentCostDescription.dev.quantity} h
:
- Total: {cost.environmentCostDescription.dev.quantity.toFixed(2).toLocaleString()} Std. + Total: {cost.environmentCostDescription.dev.quantity} Std.
} @@ -223,7 +227,7 @@ const Invoice = ({ cost, language }) => {
} - { + { cost.modifiers.map( ({ id, discountFixed, discountPercentage, extraFixed, extraPercentage, customerComments }, index) => (
@@ -252,7 +256,7 @@ const Invoice = ({ cost, language }) => {
{cost.total.toFixed(2)}
- +
From 8063cf60a8f1e9faa61be34bf5ef1180c9dda339 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Fri, 29 May 2020 13:11:37 -0400 Subject: [PATCH 045/280] Adding in QTY to billingCalculations --- services/api/src/resources/billing/billingCalculations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/api/src/resources/billing/billingCalculations.ts b/services/api/src/resources/billing/billingCalculations.ts index 45312c3c85..3e68af9ac8 100644 --- a/services/api/src/resources/billing/billingCalculations.ts +++ b/services/api/src/resources/billing/billingCalculations.ts @@ -186,7 +186,8 @@ export const storageCost = ({ projects, currency }: IBillingGroup) => { const description = { projects: projects.map(({name, storageDays}) => ({name, storage: storageDays/days})), included: freeGBDays/days, - additional: storageToBill/days + additional: storageToBill/days, + qty: storageToBill } return storageDays > freeGBDays From c7c3d02f2edbfbd6a47c3ccd3214a1122687a9c1 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Mon, 1 Jun 2020 17:14:34 -0400 Subject: [PATCH 046/280] Drag N Drop Modifiers Reordering --- services/ui/package.json | 5 +- .../components/BillingGroupInvoice/index.js | 26 +- .../BillingModifiers/AddBillingModifier.js | 8 +- .../BillingModifiers/AllBillingModifiers.js | 350 ++++++++++-------- .../ui/src/lib/fragment/BillingModifier.js | 3 +- services/ui/src/pages/admin/billing.js | 25 +- 6 files changed, 243 insertions(+), 174 deletions(-) diff --git a/services/ui/package.json b/services/ui/package.json index a6da1b02f6..a407902934 100644 --- a/services/ui/package.json +++ b/services/ui/package.json @@ -14,7 +14,9 @@ "serve-storybook": "storybook-server -s ./src" }, "dependencies": { - "@apollo/react-hooks": "^3.1.3", + "@apollo/react-hooks": "^3.1.5", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", "@zeit/next-css": "^1.0.1", "apollo-cache-inmemory": "^1.3.9", "apollo-client": "^2.4.5", @@ -37,6 +39,7 @@ "ramda": "^0.25.0", "react": "^16.8.4", "react-apollo": "^2.1.11", + "react-beautiful-dnd": "^13.0.0", "react-copy-to-clipboard": "^5.0.1", "react-dom": "^16.8.4", "react-highlight-words": "^0.14.0", diff --git a/services/ui/src/components/BillingGroupInvoice/index.js b/services/ui/src/components/BillingGroupInvoice/index.js index 328a1b8d99..764d17ba74 100644 --- a/services/ui/src/components/BillingGroupInvoice/index.js +++ b/services/ui/src/components/BillingGroupInvoice/index.js @@ -67,20 +67,23 @@ const Invoice = ({ cost, language }) => {
- { lang === LANGS.ENGLISH ? -
- Monthly Hosting Fee for { cost.availability } Availability Environment
- PHP CMS Bundle: {currencyChar} {cost.environmentCostDescription.prod.unitPrice} per h
-
- :
- Monatliche Hostinggebühr im { cost.availability } Availability Environment
- PHP CMS Bundle: {currencyChar} {cost.environmentCostDescription.prod.unitPrice} pro Stunde
+ + { lang === LANGS.ENGLISH ? +
+ Monthly Hosting Fee for { cost.availability } Availability Environment
+ PHP CMS Bundle: {currencyChar} {cost.environmentCostDescription.prod.unitPrice} per h
+
+ : +
+ Monatliche Hostinggebühr im { cost.availability } Availability Environment
+ PHP CMS Bundle: {currencyChar} {cost.environmentCostDescription.prod.unitPrice} pro Stunde
+
+ }
- } - Production Environment(s):
+ Production Environment{cost.environmentCostDescription.prod.description.projects.count > 1 && 's'}: {cost.environmentCostDescription.prod.description.projects.map(({name, hours}, index) => (
{name} - {hours} { lang === LANGS.ENGLISH ? `h` : `Std.` }
)) }
@@ -112,8 +115,8 @@ const Invoice = ({ cost, language }) => {
} - { lang === LANGS.ENGLISH ? `Hits per Production Environment:` : `Hits pro Production Environment:` }
+ { lang === LANGS.ENGLISH ? `Hits per Production Environment:` : `Hits pro Production Environment:` } {cost.hitCostDescription.description.projects.map(({name, hits}, index) => (
{name} - {hits.toLocaleString()}
)) }
@@ -140,6 +143,7 @@ const Invoice = ({ cost, language }) => {
Additional Storage Fee
Storage per GB/day: {currencyChar} {cost.storageCostDescription.unitPrice}
+ Average Storage per Environment per day:
:
diff --git a/services/ui/src/components/BillingModifiers/AddBillingModifier.js b/services/ui/src/components/BillingModifiers/AddBillingModifier.js index b3f63308fa..8040e24039 100644 --- a/services/ui/src/components/BillingModifiers/AddBillingModifier.js +++ b/services/ui/src/components/BillingModifiers/AddBillingModifier.js @@ -16,10 +16,10 @@ const AddBillingModifier = ({ group, month }) => { {(addBillingModifier, { loading, called, error, data }) => { diff --git a/services/ui/src/components/BillingModifiers/AllBillingModifiers.js b/services/ui/src/components/BillingModifiers/AllBillingModifiers.js index 2d4001037e..dd225590df 100644 --- a/services/ui/src/components/BillingModifiers/AllBillingModifiers.js +++ b/services/ui/src/components/BillingModifiers/AllBillingModifiers.js @@ -1,11 +1,18 @@ import * as R from 'ramda'; -import React from 'react'; +import React, { useState } from 'react'; +import { graphql, compose, withApollo, Mutation, Query } from 'react-apollo'; +import gql from "graphql-tag"; +import { useMutation, useQuery } from '@apollo/react-hooks'; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import { useApolloClient } from "@apollo/react-hooks"; + +import styled from "@emotion/styled"; import css from 'styled-jsx/css'; import Button from '../Button'; -import { Mutation, Query } from 'react-apollo'; -import AllBillingModifiersQuery from 'lib/query/AllBillingModifiers'; +import UpdateBillingModifierMutation from 'lib/mutation/UpdateBillingModifier'; import DeleteBillingModifierMutation from 'lib/mutation/DeleteBillingModifier'; +import AllBillingModifiersQuery from 'lib/query/AllBillingModifiers'; import BillingGroupCostsQuery from 'lib/query/BillingGroupCosts'; import withQueryLoading from 'lib/withQueryLoading'; @@ -14,192 +21,229 @@ import withQueryError from 'lib/withQueryError'; import { bp, color, fontSize } from 'lib/variables'; import { json } from 'body-parser'; - -const AllBillingModifiers = ({group, modifiers, month}) => ( - -
- -

Billing Modifiers

- -
- {!modifiers.length && ( -
No Billing Modifiers
- )} - {modifiers.map(({ id, group, name, startDate, endDate, customerComments, adminComments, weight, discountFixed, discountPercentage, extraFixed, extraPercentage }) => ( -
- +const grid = 8; + +const ModifierItem = styled.div` + width: 100%; + border: 1px solid grey; + margin-bottom: ${grid}px; + padding: ${grid}px; +`; + +const Modifier = ({modifier, index}) => { + const { id, group, name, startDate, endDate, customerComments, adminComments, weight, discountFixed, discountPercentage, extraFixed, extraPercentage } = modifier; + return( + + {provided => ( + + +
{discountFixed !== 0 ? `- ${discountFixed}` : ''} {discountPercentage !== 0 ? `- ${discountPercentage}%` : ''} {extraFixed !== 0 ? `+ ${extraFixed}` : ''} {extraPercentage !== 0 ? `+ ${extraPercentage}%` : ''}
-
{startDate.replace('00:00:00', '')} - {endDate.replace('00:00:00', '')}
- -
-
- Customer Comments: {customerComments} -
-
- Admin Comments: {adminComments} -
+
Customer Comments: {customerComments}
+
Admin Comments: {adminComments}
- {( - deleteBillingModifier, - { loading, called, error, data } - ) => { - if (error) { - return
{error.message}
; - } - - if (called) { - return
Deleting Billing Modifier...
; - } - - return ( - - ); - }} -
+ mutation={DeleteBillingModifierMutation} + // refetchQueries={[ + // { query: AllBillingModifiersQuery, variables: { input: { name: group.name } } }, + // { query: BillingGroupCostsQuery, variables: { input: { name: group.name }, month }} + // ]} + > + {( + deleteBillingModifier, + { loading, called, error, data } + ) => { + if (error) { + return
{error.message}
; + } + + if (called) { + return
Deleting Billing Modifier...
; + } + + return ( + + ); + }} +
- ) - )} -
- + + + )} + + + ); +} + +const ModifierList = React.memo(function ModifierList({ modifiers }) { + return modifiers.map((modifier, index) => ()); +}); + + +const reorder = (list, startIndex, endIndex) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result.map((item, index) => ({...item, weight: index})); +}; + +const AllBillingModifiers = ({group, modifiers, month}) => { + + const client = useApolloClient(); + + const [updateModifier] = useMutation( + UpdateBillingModifierMutation, + { + update(cache, { data: { updateBillingModifier } }){ + const variables = { input: { name: group } }; + const { allBillingModifiers } = cache.readQuery({ query: AllBillingModifiersQuery, variables}); + const { id, weight } = updateBillingModifier; + + const idx = allBillingModifiers.findIndex(({id}) => id === id ); + + if(allBillingModifiers[idx].weight !== weight){ + const data = { allBillingModifiers: allBillingModifiers.map(obj => id === obj.id ? updateBillingModifier : obj) }; + cache.writeQuery({ query: AllBillingModifiersQuery, variables, data }); } + } + } + ); - .modifier-value { - font-weight: bold; - } - .comments { - padding-top: 15px; - margin-right: 100px; - } + const onDragEnd = (result) => { + if (!result.destination) { + return; + } - .delete { - position: absolute; - top: 15px; - right: 15px; - } + if (result.destination.index === result.source.index) { + return; + } - .data-table { - background-color: ${color.white}; - border: 1px solid ${color.lightestGrey}; - border-radius: 3px; - box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.03); - - .data-none { - border: 1px solid ${color.white}; - border-bottom: 1px solid ${color.lightestGrey}; - border-radius: 3px; - line-height: 1.5rem; - padding: 8px 0 7px 0; - text-align: center; - } + const reorderedModifiers = reorder( + modifiers, + result.source.index, + result.destination.index + ); - .data-row { - border: 1px solid ${color.white}; - border-bottom: 1px solid ${color.lightestGrey}; - border-radius: 0; - line-height: 1.5rem; - display: block; - position: relative; - padding: 8px 0 7px 0; - - - & > div { - padding-left: 20px; - @media ${bp.wideDown} { - padding-right: 40px; - } - } - &:hover { - border: 1px solid ${color.brightBlue}; - } + reorderedModifiers.forEach(modifier => { + const {id, weight} = modifier; - &:first-child { - border-top-left-radius: 3px; - border-top-right-radius: 3px; + const optimisticResponse = { + updateBillingModifier: { + ...modifier, + __typename: "BillingModifier", + group: { + ...modifier.group, + type: "billing", + __typename: "BillingGroup" } + } + }; - &:last-child { - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; - } + const variables = { input: { id, patch: { weight }} }; + + updateModifier({variables, optimisticResponse }) + + }); + + + const variables = { input: { name: group } }; + const data = { allBillingModifiers: reorderedModifiers }; + client.writeQuery({ query: AllBillingModifiersQuery, variables, data }); + + } // end onDragEnd + + return( +
+ +

Billing Modifiers

+ + {!modifiers.length && ( +
No Billing Modifiers
+ )} + + + + {(provided, snapshot) => ( +
+ + {provided.placeholder} +
+ )} +
+
+ + +
-); + ); + +} export default AllBillingModifiers; diff --git a/services/ui/src/lib/fragment/BillingModifier.js b/services/ui/src/lib/fragment/BillingModifier.js index e6ab3c62cb..ba8a04f7ff 100644 --- a/services/ui/src/lib/fragment/BillingModifier.js +++ b/services/ui/src/lib/fragment/BillingModifier.js @@ -11,6 +11,7 @@ export default gql` extraFixed, extraPercentage, customerComments, - adminComments + adminComments + weight } `; \ No newline at end of file diff --git a/services/ui/src/pages/admin/billing.js b/services/ui/src/pages/admin/billing.js index c6c7b71e3f..d9ce9c18f3 100644 --- a/services/ui/src/pages/admin/billing.js +++ b/services/ui/src/pages/admin/billing.js @@ -158,6 +158,17 @@ export const PageBillingGroup = ({ router }) => { {R.compose(withQueryLoading, withQueryError)( ({ data: { costs } }) => { + + + if (costs.projects.count === 0) { + return (

No Projects

) + } + + const isAvailabilityEqual = costs.projects.reduce((acc, proj) => (proj.availability !== costs.projects[0].availability ? false : true), true); + if (!isAvailabilityEqual){ + return(

All projects in billing group do not have the same availability.

) + } + return( <>
@@ -172,12 +183,14 @@ export const PageBillingGroup = ({ router }) => {
- { + { + {R.compose(withQueryLoading, withQueryError)( - ({ data: { allBillingModifiers: modifiers } }) => + ({ data: { allBillingModifiers: modifiers } }) => )} - } - + + } +
@@ -209,6 +222,10 @@ export const PageBillingGroup = ({ router }) => { } } + .error { + margin: 1rem; + } + .barChart-wrapper { display: flex; flex-direction: column; From 154e203fb0e1f56c1d651f4973bd3cbe9a30f364 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 2 Jun 2020 09:03:53 +1000 Subject: [PATCH 047/280] merge conflicts --- local-dev/api-data/03-populate-api-data-kubernetes.gql | 4 ---- 1 file changed, 4 deletions(-) diff --git a/local-dev/api-data/03-populate-api-data-kubernetes.gql b/local-dev/api-data/03-populate-api-data-kubernetes.gql index c742ed8e9b..3020fd9fa3 100644 --- a/local-dev/api-data/03-populate-api-data-kubernetes.gql +++ b/local-dev/api-data/03-populate-api-data-kubernetes.gql @@ -9,11 +9,7 @@ mutation PopulateApi { routerPattern: "${project}.${environment}.172.17.0.1.xip.io" sshHost: "172.17.0.1" sshPort: "2020" -<<<<<<< HEAD - token: "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9URmloeU9MbTVsN0xrbU1jSDl5N1BwRFpaYzBqVU1WRDQ4VDRIZDZQdU0ifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJsYWdvb24iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoia3ViZXJuZXRlc2J1aWxkZGVwbG95LXRva2VuLWh2d2ttIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Imt1YmVybmV0ZXNidWlsZGRlcGxveSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImRjMGYyMTYwLTI4N2ItNGE2Yy1hMzBhLWQ2NjExOWNkY2EwYyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpsYWdvb246a3ViZXJuZXRlc2J1aWxkZGVwbG95In0.L9ZmyhQZwTp-_I8fIpjSS448JFBI5uDao68dora6Pva5op3LEkP9GJ768VLYFxpbpUl6SHT5W0OtD-sYoLYGPPniegPTRKWu2enm6mBKemvKKg_57QD_cdE2mqllHFHWsFWRKl_NVIZ1yuYSq8q8e-IZD33Th0szcnUB3XJofTcYSJQI6ZmvOQL7rBsYb9IAygAewDvAJs6ZL9K0WtW3ALoCOQgIIPpS87fmxjWjrbvLQf6x0qZO4AUhuk9N5QV3aQNno6x3tB-W2GSFkSbq9tTVPbiWWFeRxyiE-1A2T-cOFX-aEbzO8rYwVhGiNF9Xpz3q1fmiVuoGcCtXn4Lrmw" # make-kubernetes-token -======= token: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNMaTF3aC1PR2dSeElnMlJXdG5kOUF5d1N1X1JQVGZhQUo0WHBYcEVMUUEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJsYWdvb24iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoia3ViZXJuZXRlc2J1aWxkZGVwbG95LXRva2VuLWY1OGQ1Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Imt1YmVybmV0ZXNidWlsZGRlcGxveSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImEyN2NiOTBhLTQ0MDAtNDljYy04Yzc1LWQ4NTAyZmRmMDhhNCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpsYWdvb246a3ViZXJuZXRlc2J1aWxkZGVwbG95In0.dvmjV1_XXt__2juq392y2M1LyfFUGZz6D3do5ywkLy7zXz1IUv5Qf9dPAk4t3NP4xjtbhR_jJc8pWnVSP1Z2d1tZqj-XYJJvAf6YHXv8L4ram-MObz0x0zyCVK8w_bbzwWCEySM92eUI_TtpPCxFyEhJE7XJ--NppzBq-LaQ8eb8WvOrR5YHJAej1_HrtyQ_4Lm2DAm8dug-ptuAYQ8X4CWbXq-L_dmQnt-LgnKCCbDnvxo8XIJ_LQeMewtwtWmwOXscwGGM2Gq3C-9ykrnI1hd42GE_otLRBHTKLZfJ34eIYXsxXoMIXllxZp4O-NSIzEtafoaOL6BOQkSitAADZQ" # make-kubernetes-token ->>>>>>> upstream/master } ) { id From 37914f61365f74184c8c737da3ca3617f28fff63 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Mon, 1 Jun 2020 20:54:05 -0400 Subject: [PATCH 048/280] add, edit, delete plus drag/drop rearrange --- .../components/BillingGroupInvoice/index.js | 32 ++--- .../BillingModifiers/AddBillingModifier.js | 39 +----- .../BillingModifiers/AllBillingModifiers.js | 96 ++++++------- .../BillingModifiers/BillingModifierForm.js | 130 ++++++++++++++++-- services/ui/src/pages/admin/billing.js | 10 +- 5 files changed, 197 insertions(+), 110 deletions(-) diff --git a/services/ui/src/components/BillingGroupInvoice/index.js b/services/ui/src/components/BillingGroupInvoice/index.js index 764d17ba74..13f1a77f23 100644 --- a/services/ui/src/components/BillingGroupInvoice/index.js +++ b/services/ui/src/components/BillingGroupInvoice/index.js @@ -37,25 +37,19 @@ const Invoice = ({ cost, language }) => { return (
- - - -

Invoice

-
- - -
-
- - -
- - - - - +
+

Invoice

+
+ + +
+
+ + +
+
diff --git a/services/ui/src/components/BillingModifiers/AddBillingModifier.js b/services/ui/src/components/BillingModifiers/AddBillingModifier.js index 8040e24039..6ca8c4c463 100644 --- a/services/ui/src/components/BillingModifiers/AddBillingModifier.js +++ b/services/ui/src/components/BillingModifiers/AddBillingModifier.js @@ -1,45 +1,20 @@ +import * as R from 'ramda'; import React from 'react'; import css from 'styled-jsx/css'; -import { Mutation } from 'react-apollo'; - import { color } from 'lib/variables'; -import AddBillingModifierMutation from '../../lib/mutation/AddBillingModifier'; -import AllBillingModifiersQuery from 'lib/query/AllBillingModifiers'; -import BillingGroupCostsQuery from 'lib/query/BillingGroupCosts'; + import BillingModifierForm from "./BillingModifierForm"; -const AddBillingModifier = ({ group, month }) => { +const AddBillingModifier = ({ group, month, editBillingModifier }) => { return(
- - {(addBillingModifier, { loading, called, error, data }) => { - - const addBillingModifierHandler = (input) => { - addBillingModifier({ variables: { input } }); - }; - - if (!error && called && loading) { - return
Adding Billing Modifier...
; - } - - return ( -
-

Add Billing Modifier

- { error ?
{error.message.replace('GraphQL error:', '').trim()}
: "" } - -
- ); - }} -
+
+

{R.isEmpty(editBillingModifier) ? 'Add' : 'Edit' } Billing Modifier

+ +
@@ -121,8 +123,8 @@ const Modifier = ({modifier, index}) => { ); } -const ModifierList = React.memo(function ModifierList({ modifiers }) { - return modifiers.map((modifier, index) => ()); +const ModifierList = React.memo(function ModifierList({ modifiers, editHandler }) { + return modifiers.map((modifier, index) => ()); }); @@ -134,7 +136,7 @@ const reorder = (list, startIndex, endIndex) => { return result.map((item, index) => ({...item, weight: index})); }; -const AllBillingModifiers = ({group, modifiers, month}) => { +const AllBillingModifiers = ({group, modifiers, month, editHandler}) => { const client = useApolloClient(); @@ -159,6 +161,9 @@ const AllBillingModifiers = ({group, modifiers, month}) => { const onDragEnd = (result) => { + + editHandler({}); + if (!result.destination) { return; } @@ -191,8 +196,7 @@ const AllBillingModifiers = ({group, modifiers, month}) => { const variables = { input: { id, patch: { weight }} }; - updateModifier({variables, optimisticResponse }) - + updateModifier({variables, optimisticResponse }); }); @@ -215,7 +219,7 @@ const AllBillingModifiers = ({group, modifiers, month}) => { {(provided, snapshot) => (
- + {provided.placeholder}
)} diff --git a/services/ui/src/components/BillingModifiers/BillingModifierForm.js b/services/ui/src/components/BillingModifiers/BillingModifierForm.js index c280c2a160..91b8b6cf7c 100644 --- a/services/ui/src/components/BillingModifiers/BillingModifierForm.js +++ b/services/ui/src/components/BillingModifiers/BillingModifierForm.js @@ -1,7 +1,15 @@ +import * as R from 'ramda'; import React, { useState } from 'react'; import css from 'styled-jsx/css'; import Button from 'components/Button'; +import { useMutation, useQuery } from '@apollo/react-hooks'; + +import AddBillingModifierMutation from '../../lib/mutation/AddBillingModifier'; +import UpdateBillingModifierMutation from 'lib/mutation/UpdateBillingModifier'; +import AllBillingModifiersQuery from 'lib/query/AllBillingModifiers'; +import BillingGroupCostsQuery from 'lib/query/BillingGroupCosts'; + import moment from 'moment'; import { enGB } from 'date-fns/locale' @@ -9,16 +17,54 @@ import { DateRangePicker, START_DATE, END_DATE } from 'react-nice-dates' import 'react-nice-dates/build/style.css' -const BillingModifierForm = ({group, submitHandler}) => { +const BillingModifierForm = ({group, editBillingModifier}) => { + + const getModifierType = ({discountFixed, discountPercentage, extraFixed, extraPercentage}) => { + if(discountFixed !== 0){ + return 'discountFixed' + } + + if (discountPercentage !== 0) { + return 'discountPercentage' + } + + if(extraFixed !== 0){ + return 'extraFixed' + } + + if(extraPercentage !== 0){ + return 'extraPercentage' + } + return ''; + } + + const getModifierValue = ({discountFixed, discountPercentage, extraFixed, extraPercentage}) => { + if(discountFixed !== 0){ + return discountFixed + } + + if (discountPercentage !== 0) { + return discountPercentage + } + + if(extraFixed !== 0){ + return extraFixed + } + + if(extraPercentage !== 0){ + return extraPercentage + } + return ''; + } const defaultValues = { - startDate: '', - endDate: '', - modifierType: 'discountFixed', - modifierValue: '', - customerComments: '', - adminComments: '', - weight: 0, + startDate: !R.isEmpty(editBillingModifier) ? editBillingModifier.startDate : '', + endDate: !R.isEmpty(editBillingModifier) ? editBillingModifier.endDate : '', + modifierType: !R.isEmpty(editBillingModifier) ? getModifierType(editBillingModifier) : 'discountFixed', + modifierValue: !R.isEmpty(editBillingModifier) ? getModifierValue(editBillingModifier) : '', + customerComments: !R.isEmpty(editBillingModifier) ? editBillingModifier.customerComments : '', + adminComments: !R.isEmpty(editBillingModifier) ? editBillingModifier.adminComments : '', + weight: !R.isEmpty(editBillingModifier) ? editBillingModifier.weight : 0, }; const [values, setValues] = useState(defaultValues); @@ -27,9 +73,40 @@ const BillingModifierForm = ({group, submitHandler}) => { setValues({...values, [name]: value}); } + const [addBillingModifier] = useMutation( + AddBillingModifierMutation, + { + update(cache, { data: { addBillingModifier } }){ + + const variables = { input: { name: group } }; + const { allBillingModifiers } = cache.readQuery({ query: AllBillingModifiersQuery, variables}); + const data = { allBillingModifiers: [...allBillingModifiers, {...addBillingModifier}] }; - const isFormValid = values.startDate !== '' && values.endDate !== '' && values.modifierType && values.modifierValue && values.adminComments !== ''; + cache.writeQuery({ query: AllBillingModifiersQuery, variables, data }); + } + } + ); + const [updateModifier] = useMutation( + UpdateBillingModifierMutation, + { + update(cache, { data: { updateBillingModifier } }){ + const variables = { input: { name: group } }; + const { allBillingModifiers } = cache.readQuery({ query: AllBillingModifiersQuery, variables}); + const { id, weight } = updateBillingModifier; + + const idx = allBillingModifiers.findIndex(({id}) => id === id ); + + if(allBillingModifiers[idx].weight !== weight){ + const data = { allBillingModifiers: allBillingModifiers.map(obj => id === obj.id ? updateBillingModifier : obj) }; + cache.writeQuery({ query: AllBillingModifiersQuery, variables, data }); + } + + } + } + ); + + const isFormValid = values.startDate !== '' && values.endDate !== '' && values.modifierType && values.modifierValue && values.adminComments !== ''; const formSubmitHandler = () => { const variables = { @@ -45,9 +122,37 @@ const BillingModifierForm = ({group, submitHandler}) => { weight: values.weight !== 0 ? parseInt(values.weight): 0 }; - submitHandler(variables) + // const optimisticResponse = { + // addBillingModifier: { + // ...variables, + // __typename: "BillingModifier", + // } + // }; + + if(R.isEmpty(editBillingModifier)){ + addBillingModifier({ variables: { input: {...variables } } }) + }else{ + + const optimisticResponse = { + updateBillingModifier: { + ...editBillingModifier, + ...variables, + __typename: "BillingModifier", + group: { + ...editBillingModifier.group, + type:'billing', + __typename: "BillingGroup" + } + } + }; + + const editVariables = { input: { id: editBillingModifier.id, patch: { ...variables }} }; + updateModifier({variables: editVariables, optimisticResponse }); + } } + + return (
@@ -60,6 +165,7 @@ const BillingModifierForm = ({group, submitHandler}) => { className={'input' + (focus === START_DATE ? ' -focused' : '')} placeholder='Start date (YYYY-MM-DD)' onChange={handleChange} + value={values.startDate} /> { className={'input' + (focus === END_DATE ? ' -focused' : '')} placeholder='End date (YYYY-MM-DD)' onChange={handleChange} + value={values.endDate} />
@@ -81,6 +188,7 @@ const BillingModifierForm = ({group, submitHandler}) => { aria-labelledby="modifierType" label='Modifier Type' className="modifierInput" + value={values.modifierType} > {[ {name: 'Discount: Fixed', value: 'discountFixed'}, @@ -152,7 +260,7 @@ const BillingModifierForm = ({group, submitHandler}) => {
- +
+
+ ); +} + /** * Displays a billingGroupCost page, given the billingGroupCost name. */ @@ -65,11 +157,19 @@ export const PageBillingGroup = ({ router }) => { const [editModifier, setEditModifier] = useState({ }); useEffect(() => { - const result = queries.map(query => { - if (query && query.data && query.data.costs) { - return (query.data.costs) + const result = queries.map(({loading, error, data}) => { + if (error) { + return {error}; + } + + if (loading){ + return {loading}; + } + + if (data && data.costs) { + return (data.costs) } - return ({total: 0}); + return {}; }); // for (let i = 0; i <= 5; i++) { @@ -114,7 +214,22 @@ export const PageBillingGroup = ({ router }) => { {auth => { - if (adminAuthChecker(auth)) { + if (!adminAuthChecker(auth)) { + return (
Seems that you do not have permissions to access this resource.
); + } + + if (costs.length > 0 && costs[0].loading){ + return
Loading...
+ } + + if (costs.length > 0 && costs[0].error){ + return costs[0].error.message.includes("Projects must have the same availability") + ? + : (
{costs[0].error.message}
); + } + + if (costs.length > 0 && costs[0]){ + console.log(costs[0]) return (
@@ -159,63 +274,39 @@ export const PageBillingGroup = ({ router }) => {
- - - - {R.compose(withQueryLoading, withQueryError)( - ({ data: { costs } }) => { - - - if (costs.projects.count === 0) { - return (

No Projects

) - } - - const isAvailabilityEqual = costs.projects.reduce((acc, proj) => (proj.availability !== costs.projects[0].availability ? false : true), true); - if (!isAvailabilityEqual){ - return(

All projects in billing group do not have the same availability.

) - } - - return( - <> -
-
-
- -
- - -
- -
-
-
- { - - {R.compose(withQueryLoading, withQueryError)( - ({ data: { allBillingModifiers: modifiers } }) => - )} - - } - -
-
-
- -
- - ); + +
+
+
+ +
+ + +
+ +
+
+
+ { + + {R.compose(withQueryLoading, withQueryError)( + ({ data: { allBillingModifiers: modifiers } }) => + )} + } - )} - - + +
+
+
+ +
); - } - - return (
Seems that you do not have permissions to access this resource.
); + } }} + diff --git a/services/ui/src/components/Problems/Accordion/index.js b/services/ui/src/components/Problems/Accordion/index.js new file mode 100644 index 0000000000..2902af3f44 --- /dev/null +++ b/services/ui/src/components/Problems/Accordion/index.js @@ -0,0 +1,47 @@ +import React, { useState, Fragment } from "react"; +import PropTypes from "prop-types"; +import moment from 'moment'; + +const Accordion = ({ children, defaultValue = true, className = "", onToggle, heading }) => { + const [visibility, setVisibility] = useState(defaultValue); + return ( +
+
{ + setVisibility(!visibility); + if (onToggle) onToggle(!visibility); + }}> +
{heading.identifier}
+
{heading.service}
+
{heading.associatedPackage || 'UNSET'}
+
{heading.source}
+
{heading.severity}
+
{heading.severityScore}
+
+ + {visibility ? {children} : null} + +
+ ); +}; + +Accordion.propTypes = { + className: PropTypes.string, + children: PropTypes.any.isRequired, + onToggle: PropTypes.func, +}; + +export default Accordion; \ No newline at end of file diff --git a/services/ui/src/components/Problems/index.js b/services/ui/src/components/Problems/index.js new file mode 100644 index 0000000000..959ba4dbb4 --- /dev/null +++ b/services/ui/src/components/Problems/index.js @@ -0,0 +1,317 @@ +import React, { useState, useEffect } from 'react'; +import { bp, color, fontSize } from 'lib/variables'; +import useSortableData from './sortedItems'; +import Accordion from './Accordion'; + +const Problems = ({ problems }) => { + const { sortedItems, requestSort, getClassNamesFor } = useSortableData(problems); + + const [currentItems, setCurrentItems] = useState(sortedItems); + const [problemTerm, setProblemTerm] = useState(''); + const [hasFilter, setHasFilter] = React.useState(false); + + const handleProblemFilterChange = (event) => { + setHasFilter(false); + + if (event.target.value !== null || event.target.value !== '') { + setCurrentItems(sortedItems); + setHasFilter(true); + } + setProblemTerm(event.target.value); + }; + + const handleSort = (key) => { + if (hasFilter) { + const results = filterResults(); + setCurrentItems(results); + } + else { + setCurrentItems(sortedItems); + } + + return requestSort(key); + }; + + const filterResults = () => { + const lowercasedFilter = problemTerm.toLowerCase(); + + return sortedItems.filter(item => { + if (problemTerm == null || problemTerm === '') { + setHasFilter(false); + return problems; + } + + return Object.keys(item).some(key => { + if (item[key] !== null) { + return item[key].toString().toLowerCase().includes(lowercasedFilter); + } + }); + }); + }; + + useEffect(() => { + const results = filterResults(); + setCurrentItems(results); + }, [problemTerm]); + + return ( +
+
+ +
+
+ + + + + + +
+
+ {!currentItems.length &&
No Problems
} + {currentItems.map((problem) => { + return ( +
+ {problem.description && problem.description.length > 0 && (
+ +
{problem.description}
+
)} + {problem.version && problem.version.length > 0 && (
+ +
{problem.version}
+
)} + {problem.fixedVersion && problem.fixedVersion.length > 0 && (
+ +
{problem.fixedVersion}
+
)} + {problem.links && problem.links.length > 0 && (
+ + +
)} +
+
Raw Data:
+
+ {Object.entries(JSON.parse(problem.data)).map(([a, b]) => { + if(b) { + return ( +
+ +
{b}
+
+ ); + } + })} +
+
+
+
+ ); + })} +
+ +
+ ); +}; + +export default Problems; diff --git a/services/ui/src/components/Problems/index.stories.js b/services/ui/src/components/Problems/index.stories.js new file mode 100644 index 0000000000..6a5591f537 --- /dev/null +++ b/services/ui/src/components/Problems/index.stories.js @@ -0,0 +1,20 @@ +import React from 'react'; +import Problems from './index'; +import faker from 'faker/locale/en'; +import mocks, { generator } from 'api/src/mocks'; +import {MockList} from "graphql-tools"; + +export default { + component: Problems, + title: 'Components/Problems', +} + +let temp = mocks.ProblemMutation(mocks.Problem); + +export const Default = () => ( + +); + +export const NoProblems = () => ( + +); diff --git a/services/ui/src/components/Problems/sortedItems.js b/services/ui/src/components/Problems/sortedItems.js new file mode 100644 index 0000000000..013a459711 --- /dev/null +++ b/services/ui/src/components/Problems/sortedItems.js @@ -0,0 +1,62 @@ +import React, {useState} from "react"; +import moment from 'moment'; +import hash from 'object-hash'; + +const useSortableData = (initialItems) => { + const initialConfig = {key: 'identifier', direction: 'ascending'}; + const [sortConfig, setSortConfig] = React.useState(initialConfig); + const [currentItems, setCurrentItems] = useState(initialItems); + + const getClassNamesFor = (name) => { + if (!sortConfig) { + return; + } + + return sortConfig.key === name ? sortConfig.direction : undefined; + }; + + const sortedItems = React.useMemo(() => { + let sortableItems = [...currentItems]; + + if (sortConfig !== null) { + sortableItems.sort((a, b) => { + let aParsed = sortConfig.key === 'created' ? new moment(a[sortConfig.key]).format('YYYYMMDD') + : (a[sortConfig.key] ? a[sortConfig.key].toString().toLowerCase().trim() : null); + let bParsed = sortConfig.key === 'created' ? new moment(b[sortConfig.key]).format('YYYYMMDD') + : (b[sortConfig.key] ? b[sortConfig.key].toString().toLowerCase().trim() : null); + + if (aParsed < bParsed) { + return sortConfig.direction === 'ascending' ? -1 : 1; + } + if (aParsed > bParsed) { + return sortConfig.direction === 'ascending' ? 1 : -1; + } + + return 0; + }); + } + + return sortableItems; + }, [currentItems, sortConfig]); + + if (hash(sortedItems) !== hash(currentItems)) { + setCurrentItems(sortedItems); + } + + const requestSort = (key) => { + let direction = 'ascending'; + + if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { + direction = 'descending'; + } + + setCurrentItems(sortedItems); + setSortConfig({ key, direction }); + + return { sortedItems: currentItems }; + }; + + return { sortedItems: currentItems, getClassNamesFor, requestSort }; +}; + +export default useSortableData; \ No newline at end of file diff --git a/services/ui/src/components/link/Problems.js b/services/ui/src/components/link/Problems.js new file mode 100644 index 0000000000..bc102849fa --- /dev/null +++ b/services/ui/src/components/link/Problems.js @@ -0,0 +1,30 @@ +import Link from 'next/link'; + +export const getLinkData = (environmentSlug, projectSlug) => ({ + urlObject: { + pathname: '/problems', + query: { openshiftProjectName: environmentSlug } + }, + asPath: `/projects/${projectSlug}/${environmentSlug}/problems` +}); + +/** + * Links to the problems page given the project name and the openshift project name. + */ +const ProblemsLink = ({ + environmentSlug, + projectSlug, + children, + className = null, + prefetch = false +}) => { + const linkData = getLinkData(environmentSlug, projectSlug); + + return ( + + {children} + + ); +}; + +export default ProblemsLink; diff --git a/services/ui/src/components/link/Problems.stories.js b/services/ui/src/components/link/Problems.stories.js new file mode 100644 index 0000000000..f2226ba537 --- /dev/null +++ b/services/ui/src/components/link/Problems.stories.js @@ -0,0 +1,20 @@ +import React from 'react'; +import mocks, { seed } from 'api/src/mocks'; +import ProblemsLink from './Problems'; + +export default { + component: ProblemsLink, + title: 'Components/link/ProblemsLink', +}; + +seed(); +const environment = mocks.Environment(); + +export const Default = () => ( + + Backups link + +); diff --git a/services/ui/src/lib/fragment/Problem.js b/services/ui/src/lib/fragment/Problem.js new file mode 100644 index 0000000000..fad587c399 --- /dev/null +++ b/services/ui/src/lib/fragment/Problem.js @@ -0,0 +1,20 @@ +import gql from 'graphql-tag'; + +export default gql` + fragment problemFields on Problem { + id + identifier + data + severity + source + service + created + deleted + severityScore + associatedPackage + description + version + fixedVersion + links + } +`; diff --git a/services/ui/src/lib/query/EnvironmentByOpenshiftProjectName.js b/services/ui/src/lib/query/EnvironmentByOpenshiftProjectName.js index e488dc32b5..36ad926c38 100644 --- a/services/ui/src/lib/query/EnvironmentByOpenshiftProjectName.js +++ b/services/ui/src/lib/query/EnvironmentByOpenshiftProjectName.js @@ -20,6 +20,7 @@ export default gql` standbyRoutes productionEnvironment standbyProductionEnvironment + problemsUi } } } diff --git a/services/ui/src/lib/query/EnvironmentWithBackups.js b/services/ui/src/lib/query/EnvironmentWithBackups.js index 4aea9a7f22..241f4c182e 100644 --- a/services/ui/src/lib/query/EnvironmentWithBackups.js +++ b/services/ui/src/lib/query/EnvironmentWithBackups.js @@ -12,6 +12,7 @@ export default gql` project { id name + problemsUi } backups { ...backupFields diff --git a/services/ui/src/lib/query/EnvironmentWithDeployment.js b/services/ui/src/lib/query/EnvironmentWithDeployment.js index 0327eb008d..40b92111d1 100644 --- a/services/ui/src/lib/query/EnvironmentWithDeployment.js +++ b/services/ui/src/lib/query/EnvironmentWithDeployment.js @@ -12,6 +12,7 @@ export default gql` project { id name + problemsUi } deployments(name: $deploymentName) { ...deploymentFields diff --git a/services/ui/src/lib/query/EnvironmentWithDeployments.js b/services/ui/src/lib/query/EnvironmentWithDeployments.js index 40582ac13a..cb8cf26708 100644 --- a/services/ui/src/lib/query/EnvironmentWithDeployments.js +++ b/services/ui/src/lib/query/EnvironmentWithDeployments.js @@ -12,6 +12,7 @@ export default gql` project { id name + problemsUi } deployType deployBaseRef diff --git a/services/ui/src/lib/query/EnvironmentWithProblems.js b/services/ui/src/lib/query/EnvironmentWithProblems.js new file mode 100644 index 0000000000..0a2d570cb8 --- /dev/null +++ b/services/ui/src/lib/query/EnvironmentWithProblems.js @@ -0,0 +1,23 @@ +import gql from 'graphql-tag'; +import ProblemsFragment from 'lib/fragment/Problem'; + +export default gql` + query getEnvironment($openshiftProjectName: String!) { + environment: environmentByOpenshiftProjectName( + openshiftProjectName: $openshiftProjectName + ) { + id + name + openshiftProjectName + project { + id + name + problemsUi + } + problems { + ...problemFields + } + } + } + ${ProblemsFragment} +`; diff --git a/services/ui/src/lib/query/EnvironmentWithTask.js b/services/ui/src/lib/query/EnvironmentWithTask.js index 744c45bc5b..0ffdbd5e01 100644 --- a/services/ui/src/lib/query/EnvironmentWithTask.js +++ b/services/ui/src/lib/query/EnvironmentWithTask.js @@ -17,6 +17,7 @@ export default gql` project { id name + problemsUi } services { id diff --git a/services/ui/src/lib/query/EnvironmentWithTasks.js b/services/ui/src/lib/query/EnvironmentWithTasks.js index 3bb9dd5576..82702a8365 100644 --- a/services/ui/src/lib/query/EnvironmentWithTasks.js +++ b/services/ui/src/lib/query/EnvironmentWithTasks.js @@ -17,6 +17,7 @@ export default gql` project { id name + problemsUi } services { id diff --git a/services/ui/src/lib/query/ProjectByName.js b/services/ui/src/lib/query/ProjectByName.js index 01366d1730..b5f58e3870 100644 --- a/services/ui/src/lib/query/ProjectByName.js +++ b/services/ui/src/lib/query/ProjectByName.js @@ -25,6 +25,7 @@ export default gql` name productionEnvironment standbyProductionEnvironment + problemsUi } } } diff --git a/services/ui/src/pages/problems.js b/services/ui/src/pages/problems.js new file mode 100644 index 0000000000..9688d6a17e --- /dev/null +++ b/services/ui/src/pages/problems.js @@ -0,0 +1,77 @@ +import React from 'react'; +import * as R from 'ramda'; +import { withRouter } from 'next/router'; +import Head from 'next/head'; +import { Query } from 'react-apollo'; +import MainLayout from 'layouts/MainLayout'; +import EnvironmentWithProblemsQuery from 'lib/query/EnvironmentWithProblems'; +import Breadcrumbs from 'components/Breadcrumbs'; +import ProjectBreadcrumb from 'components/Breadcrumbs/Project'; +import EnvironmentBreadcrumb from 'components/Breadcrumbs/Environment'; +import NavTabs from 'components/NavTabs'; +import Problems from 'components/Problems'; +import withQueryLoading from 'lib/withQueryLoading'; +import withQueryError from 'lib/withQueryError'; +import { withEnvironmentRequired } from 'lib/withDataRequired'; +import { bp, color } from 'lib/variables'; + +/** + * Displays the problems page, given the name of an openshift project. + */ +export const PageProblems = ({ router }) => ( + <> + + {`${router.query.openshiftProjectName} | Problems`} + + + {R.compose( + withQueryLoading, + withQueryError, + withEnvironmentRequired + )(({ data: { environment } }) => { + + return ( + + + + + +
+ +
+ +
+
+ +
+ ); + })} +
+ +); + +export default withRouter(PageProblems); diff --git a/services/ui/src/static/images/problems-active.svg b/services/ui/src/static/images/problems-active.svg new file mode 100644 index 0000000000..d772807c41 --- /dev/null +++ b/services/ui/src/static/images/problems-active.svg @@ -0,0 +1 @@ +tasks diff --git a/services/ui/src/static/images/problems.svg b/services/ui/src/static/images/problems.svg new file mode 100644 index 0000000000..35e630c54b --- /dev/null +++ b/services/ui/src/static/images/problems.svg @@ -0,0 +1 @@ +tasks \ No newline at end of file diff --git a/services/webhook-handler/src/extractWebhookData.ts b/services/webhook-handler/src/extractWebhookData.ts index 6455cada50..c96d3dc6b9 100644 --- a/services/webhook-handler/src/extractWebhookData.ts +++ b/services/webhook-handler/src/extractWebhookData.ts @@ -84,6 +84,14 @@ export function extractWebhookData(req: IncomingMessage, body: string): WebhookR webhooktype = 'resticbackup'; event = 'restore:finished'; uuid = uuid4(); + } else if (bodyObj.type && bodyObj.type == 'scanningCompleted') { + webhooktype = 'problems'; + event = 'harbor:scanningcompleted'; + uuid = uuid4(); + } else if (bodyObj.lagoonInfo) { + webhooktype = 'problems'; + event = 'drutiny:resultset'; + uuid = uuid4(); } else { throw new Error('No supported event header found on POST request'); } diff --git a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts new file mode 100644 index 0000000000..e1709c89cf --- /dev/null +++ b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts @@ -0,0 +1,237 @@ +// @flow + +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +import { + getVulnerabilitiesPayloadFromHarbor, +} from '@lagoon/commons/dist/harborApi'; +import * as R from 'ramda'; +import uuid4 from 'uuid4'; + +import { + getProjectByName, + getEnvironmentByName, + getProblemHarborScanMatches, +} from '@lagoon/commons/dist/api'; + +const HARBOR_WEBHOOK_SUCCESSFUL_SCAN = "Success"; + +const DEFAULT_REPO_DETAILS_REGEX = "^(?.+)\/(?.+)\/(?.+)$"; + +const DEFAULT_REPO_DETAILS_MATCHER = { + defaultProjectName: "", + defaultEnvironmentName: "", + defaultServiceName: "", + regex: DEFAULT_REPO_DETAILS_REGEX, +}; + + export async function harborScanningCompleted( + WebhookRequestData, + channelWrapperWebhooks +) { + const { webhooktype, event, uuid, body } = WebhookRequestData; + const HARBOR_WEBHOOK_SUCCESSFUL_SCAN = "Success"; + + try { + let { + resources, + repository, + scanOverview, + lagoonProjectName, + lagoonEnvironmentName, + lagoonServiceName, + harborScanId, + } = await validateAndTransformIncomingWebhookdata(body); + + if(scanOverview.scan_status !== HARBOR_WEBHOOK_SUCCESSFUL_SCAN) { + sendToLagoonLogs( + 'error', + '', + uuid, + `${webhooktype}:${event}:unhandled`, + { data: body }, + `Received a scan report of status "${scanOverview.scan_status}" - ignoring` + ); + + return; + } + + let vulnerabilities = await getVulnerabilitiesFromHarbor(harborScanId); + + let { id: lagoonProjectId } = await getProjectByName(lagoonProjectName); + + let { environmentByName: environmentDetails } = await getEnvironmentByName( + lagoonEnvironmentName, + lagoonProjectId + ); + + let messageBody = { + lagoonProjectId, + lagoonProjectName, + lagoonEnvironmentId: environmentDetails.id, + lagoonEnvironmentName: environmentDetails.name, + lagoonServiceName, + vulnerabilities, + }; + + const webhookData = generateWebhookData( + WebhookRequestData.giturl, + 'problems', + 'harbor:scanningresultfetched', + messageBody + ); + + const buffer = new Buffer(JSON.stringify(webhookData)); + + await channelWrapperWebhooks.publish(`lagoon-webhooks`, '', buffer, { + persistent: true, + }); + + } catch (error) { + sendToLagoonLogs( + 'error', + '', + uuid, + `${webhooktype}:${event}:unhandled`, + { data: body }, + `Could not fetch Harbor scan results, reason: ${error}` + ); + } +} + +/** + * This function will take an incoming Harbor webhook and decompose it + * into a more useable format + * + * @param {*} rawData + */ +const validateAndTransformIncomingWebhookdata = async (rawData) => { + let { resources, repository } = rawData.event_data; + + if (!repository.repo_full_name) { + throw generateError( + 'InvalidHarborInput', + 'Unable to find repo_full_name in body.event_data.repository' + ); + } + + // scan_overview is tricky because the property doesn't have an obvious name. + // We convert it to an array of objects with the old property as a member + let scanOverviewArray = R.toPairs(resources[0].scan_overview).map((e) => { + let obj = e[1]; + obj.scan_key = e[0]; + return obj; + }); + + let harborScanPatternMatchers = await getProblemHarborScanMatches(); + + let { + lagoonProjectName, + lagoonEnvironmentName, + lagoonServiceName, + } = matchRepositoryAgainstPatterns(repository.repo_full_name, harborScanPatternMatchers.allProblemHarborScanMatchers); + + return { + resources, + repository, + scanOverview: scanOverviewArray.pop(), + lagoonProjectName, + lagoonEnvironmentName, + lagoonServiceName, + harborScanId: repository.repo_full_name, + }; +}; + +const generateError = (name, message) => { + let e = new Error(message); + e.name = name; + return e; +}; + +const matchRepositoryAgainstPatterns = (repoFullName, matchPatterns = []) => { + const matchingRes = matchPatterns.filter((e) => generateRegex(e.regex).test(repoFullName)); + + if(matchingRes.length > 1) { + const stringifyMatchingRes = matchingRes.reduce((prevRetString, e) => `${e.regex},${prevRetString}`, ''); + throw generateError("InvalidHarborConfiguration", + `We have multiple matching regexes for '${repoFullName}'` + ); + } else if (matchingRes.length == 0 && !generateRegex(DEFAULT_REPO_DETAILS_MATCHER.regex).test(repoFullName)) { + throw generateError("HarborError", + `We have no matching regexes, including default, for '${repoFullName}'` + ); + } + + const matchPatternDetails = matchingRes.pop() || DEFAULT_REPO_DETAILS_MATCHER; + + const { + lagoonProjectName = matchPatternDetails.defaultProjectName, + lagoonEnvironmentName = matchPatternDetails.defaultEnvironmentName, + lagoonServiceName = matchPatternDetails.defaultServiceName, + } = extractRepositoryDetailsGivenRegex(repoFullName, matchPatternDetails.regex); + + return {lagoonProjectName, lagoonEnvironmentName, lagoonServiceName}; +} + +const generateRegex = R.memoizeWith(R.identity, re => new RegExp(re)); + +const extractRepositoryDetailsGivenRegex = (repoFullName, pattern = DEFAULT_REPO_DETAILS_REGEX) => { + const re = generateRegex(pattern); + const match = re.exec(repoFullName); + return match.groups; +} + +const generateWebhookData = ( + webhookGiturl, + webhooktype, + event, + body, + id = null +) => { + return { + webhooktype: webhooktype, + event: event, + giturl: webhookGiturl, + uuid: id ? id : uuid4(), + body: body, + }; +}; + +const extractVulnerabilities = (harborScanResponse) => { + for (let [key, value] of Object.entries(harborScanResponse)) { + let potentialStore: any = value; + if (potentialStore.hasOwnProperty('vulnerabilities')) { + return potentialStore.vulnerabilities; + } + } + throw new ProblemsHarborConnectionError( + "Scan response from Harbor does not contain a 'vulnerabilities' key" + ); +}; + +const getVulnerabilitiesFromHarbor = async (scanId) => { + let harborPayload = null; + try { + harborPayload = await getVulnerabilitiesPayloadFromHarbor( + scanId + ); + } catch (error) { + throw error; + } + + return extractVulnerabilities(harborPayload); +}; + +class ProblemsHarborConnectionError extends Error { + constructor(message) { + super(message); + this.name = 'problems-harborConnectionError'; + } +} + +class ProblemsInvalidWebhookData extends Error { + constructor(message) { + super(message); + this.name = 'problems-invalidWebhookData'; + } +} + diff --git a/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts b/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts new file mode 100644 index 0000000000..61404cbb88 --- /dev/null +++ b/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts @@ -0,0 +1,156 @@ +// @flow + +import { + addProblem, + deleteProblemsFromSource, + getProblemsforProjectEnvironment, +} from'@lagoon/commons/dist/api'; +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +const DRUTINY_VULNERABILITY_SOURCE_BASE = 'Drutiny'; +const DRUTINY_SERVICE_NAME = 'cli'; +const DRUTINY_PACKAGE_NAME = '' +import { + getProjectByName, + getEnvironmentByName, +} from '@lagoon/commons/dist/api'; +import { generateProblemsWebhookEventName } from "./webhookHelpers"; + +const ERROR_STATES = ["error", "failure"]; +const SEVERITY_LEVELS = [ + "NONE", + "UNKNOWN", + "NEGLIGIBLE", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL" +]; +const DEFAULT_SEVERITY_LEVEL = "NEGLIGIBLE"; + +export async function processDrutinyResultset( + WebhookRequestData, + channelWrapperWebhooks +) { + + const { webhooktype, event, uuid, body } = WebhookRequestData; + const { lagoonInfo, results, profile: drutinyProfile } = body; + + try { + const lagoonProjectName = + lagoonInfo.LAGOON_DRUTINY_PROJECT_NAME || + lagoonInfo.LAGOON_PROJECT || + lagoonInfo.LAGOON_SAFE_PROJECT; + + if (!lagoonProjectName) { + throw new Error('No project name passed in Drutiny results'); + } + + const lagoonEnvironmentName = + lagoonInfo.LAGOON_DRUTINY_ENVIRONMENT_NAME || + lagoonInfo.LAGOON_ENVIRONMENT || + lagoonInfo.LAGOON_GIT_BRANCH; + + if (!lagoonEnvironmentName) { + throw new Error('No environment name passed in Drutiny results'); + } + + const { id: lagoonProjectId } = await getProjectByName( + lagoonProjectName + ); + + const { + environmentByName: environmentDetails, + } = await getEnvironmentByName(lagoonEnvironmentName, lagoonProjectId); + + const lagoonEnvironmentId = environmentDetails.id; + const lagoonServiceName = DRUTINY_SERVICE_NAME; + const drutinyVulnerabilitySource = `${DRUTINY_VULNERABILITY_SOURCE_BASE}-${drutinyProfile}`; + + //Let's get the existing problems before removing them ... + const existingProblemSet = ( + await getProblemsforProjectEnvironment( + lagoonEnvironmentName, + lagoonProjectId + ) + ) + .filter((e) => e.service == lagoonServiceName) + .reduce((prev, current) => prev.concat([current.identifier]), []); + + await deleteProblemsFromSource( + environmentDetails.id, + drutinyVulnerabilitySource, + DRUTINY_SERVICE_NAME + ); + + if (results) { + results + .filter((e) => ERROR_STATES.includes(e.type)) + .forEach((element) => { + addProblem({ + environment: lagoonEnvironmentId, + identifier: element.name, + severity: convertSeverityLevels(element.severity), + source: drutinyVulnerabilitySource, + description: element.description, + data: JSON.stringify(element), + service: DRUTINY_SERVICE_NAME, + severityScore: null, + associatedPackage: 'Drupal', + version: null, + fixedVersion: null, + links: null, + }) + .then(() => { + sendToLagoonLogs( + 'info', + lagoonProjectName, + uuid, + generateProblemsWebhookEventName({ + source: 'drutiny', + severity: convertSeverityLevels(element.severity), + isNew: !existingProblemSet.includes(element.name) + }), + { + lagoonProjectId, + lagoonProjectName, + lagoonEnvironmentId, + lagoonEnvironmentName, + lagoonServiceName, + severity: convertSeverityLevels(element.severity), + vulnerability: element, + }, + `New problem found for ${lagoonProjectName}:${lagoonEnvironmentName}:${lagoonServiceName}. Severity: ${element.severity}. Description: ${element.description}` + ); + }) + .catch((error) => + sendToLagoonLogs( + 'error', + '', + uuid, + `${webhooktype}:${event}:problem_insert_error`, + { data: body }, + `Error inserting problem id ${element.id} for ${lagoonProjectId}:${environmentDetails.id} -- ${error.message}` + ) + ); + }); + } + } catch (error) { + sendToLagoonLogs( + 'error', + '', + uuid, + `${webhooktype}:${event}:unhandled`, + { data: body }, + `Could not process incoming Drutiny scan results, reason: ${error}` + ); + } +} + +const convertSeverityLevels = (level) => { + level = level.toUpperCase(); + if(SEVERITY_LEVELS.includes(level)) { + return level; + } + + return DEFAULT_SEVERITY_LEVEL; +} diff --git a/services/webhooks2tasks/src/handlers/problems/processHarborVulnerabilityList.ts b/services/webhooks2tasks/src/handlers/problems/processHarborVulnerabilityList.ts new file mode 100644 index 0000000000..930594b9a6 --- /dev/null +++ b/services/webhooks2tasks/src/handlers/problems/processHarborVulnerabilityList.ts @@ -0,0 +1,93 @@ +// @flow + +import { addProblem, + deleteProblemsFromSource, + getProblemsforProjectEnvironment, +} from '@lagoon/commons/dist/api'; +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +import { generateProblemsWebhookEventName } from "./webhookHelpers"; +const HARBOR_VULNERABILITY_SOURCE = 'Harbor'; + + +export async function processHarborVulnerabilityList( + WebhookRequestData, + channelWrapperWebhooks +) { + const { webhooktype, event, uuid, body } = WebhookRequestData; + + const { + lagoonProjectId, + lagoonProjectName, + lagoonEnvironmentId, + lagoonEnvironmentName, + lagoonServiceName, + vulnerabilities, + } = body; + + //Let's get the existing problems before removing them ... + const existingProblemSet = ( + await getProblemsforProjectEnvironment( + lagoonEnvironmentName, + lagoonProjectId + ) + ) + .filter((e) => e.service == lagoonServiceName) + .reduce((prev, current) => prev.concat([current.identifier]), []); + + await deleteProblemsFromSource( + lagoonEnvironmentId, + HARBOR_VULNERABILITY_SOURCE, + lagoonServiceName + ); + + if (vulnerabilities) { + vulnerabilities.forEach((element) => { + addProblem({ + environment: lagoonEnvironmentId, + identifier: element.id, + severity: element.severity.toUpperCase(), + severityScore: null, + source: HARBOR_VULNERABILITY_SOURCE, + description: element.description, + links: element.links.pop(), + data: JSON.stringify(element), + version: element.version, + fixedVersion: element.fix_version, + service: lagoonServiceName, + associatedPackage: element.package, + }) + .then(() => { + sendToLagoonLogs( + 'info', + lagoonProjectName, + uuid, + generateProblemsWebhookEventName({ + source: 'harbor', + severity: element.severity.toUpperCase(), + isNew: !existingProblemSet.includes(element.id), + }), + { + lagoonProjectId, + lagoonProjectName, + lagoonEnvironmentId, + lagoonEnvironmentName, + lagoonServiceName, + severity: element.severity.toUpperCase(), + vulnerability: element, + }, + `New problem found for ${lagoonProjectName}:${lagoonEnvironmentName}:${lagoonServiceName}. Severity: ${element.severity}. Description: ${element.description}` + ); + }) + .catch((error) => + sendToLagoonLogs( + 'error', + '', + uuid, + `${webhooktype}:${event}:problem_insert_error`, + { data: body }, + `Error inserting problem id ${element.id} for ${lagoonProjectId}:${lagoonEnvironmentId} -- ${error.message}` + ) + ); + }); + } + } \ No newline at end of file diff --git a/services/webhooks2tasks/src/handlers/problems/webhookHelpers.js b/services/webhooks2tasks/src/handlers/problems/webhookHelpers.js new file mode 100644 index 0000000000..edaa092af9 --- /dev/null +++ b/services/webhooks2tasks/src/handlers/problems/webhookHelpers.js @@ -0,0 +1,15 @@ +const generateProblemsWebhookEventName = ({ + source, + severity, + isSummaryData = false, + isNew = true, +}) => { + const prefix = 'problem'; + const eventType = isNew ? 'insert' : 'update'; + const dataType = isSummaryData ? 'summary' : 'item'; + return `${prefix}:${eventType}:${source}:${dataType}:${severity}`; +}; + +module.exports = { + generateProblemsWebhookEventName +}; \ No newline at end of file diff --git a/services/webhooks2tasks/src/handlers/problems/webhookHelpers.ts b/services/webhooks2tasks/src/handlers/problems/webhookHelpers.ts new file mode 100644 index 0000000000..d101ff562c --- /dev/null +++ b/services/webhooks2tasks/src/handlers/problems/webhookHelpers.ts @@ -0,0 +1,11 @@ +export const generateProblemsWebhookEventName = ({ + source, + severity, + isSummaryData = false, + isNew = true, +}) => { + const prefix = 'problem'; + const eventType = isNew ? 'insert' : 'update'; + const dataType = isSummaryData ? 'summary' : 'item'; + return `${prefix}:${eventType}:${source}:${dataType}:${severity}`; +}; \ No newline at end of file diff --git a/services/webhooks2tasks/src/processQueue.ts b/services/webhooks2tasks/src/processQueue.ts index 3a4abad44c..34ce759a2e 100644 --- a/services/webhooks2tasks/src/processQueue.ts +++ b/services/webhooks2tasks/src/processQueue.ts @@ -3,7 +3,7 @@ import { ConsumeMessage } from 'amqplib'; import { processProjects } from './webhooks/projects'; import { processDataSync } from './webhooks/dataSync'; import { processBackup } from './webhooks/backup'; - +import { processProblems } from './webhooks/problems'; import { WebhookRequestData } from './types'; export async function processQueue (rabbitMsg: ConsumeMessage, channelWrapperWebhooks: ChannelWrapper): Promise { @@ -21,6 +21,8 @@ export async function processQueue (rabbitMsg: ConsumeMessage, channelWrapperWeb processDataSync(rabbitMsg, channelWrapperWebhooks); } else if (webhooktype == 'resticbackup') { processBackup(rabbitMsg, channelWrapperWebhooks); + } else if (webhooktype == 'problems') { + processProblems(rabbitMsg, channelWrapperWebhooks); } else { processProjects(rabbitMsg, channelWrapperWebhooks); diff --git a/services/webhooks2tasks/src/webhooks/problems.ts b/services/webhooks2tasks/src/webhooks/problems.ts new file mode 100644 index 0000000000..458754a916 --- /dev/null +++ b/services/webhooks2tasks/src/webhooks/problems.ts @@ -0,0 +1,76 @@ +// @flow + + +import uuid4 from 'uuid4'; +import { logger } from '@lagoon/commons/dist/local-logging'; +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +import { harborScanningCompleted } from '../handlers/problems/harborScanningCompleted'; +import { processHarborVulnerabilityList } from '../handlers/problems/processHarborVulnerabilityList'; +import { processDrutinyResultset } from '../handlers/problems/processDrutinyResults'; + + +import { + WebhookRequestData, + Project +} from '../types'; + + +export async function processProblems( + rabbitMsg, + channelWrapperWebhooks + ): Promise { + const webhook: WebhookRequestData = JSON.parse(rabbitMsg.content.toString()); + const { + webhooktype, + event + } = webhook; + + switch(webhook.event) { + case 'harbor:scanningcompleted' : + await handle(harborScanningCompleted, webhook, `${webhooktype}:${event}`, channelWrapperWebhooks); + break + case 'harbor:scanningresultfetched' : + await handle(processHarborVulnerabilityList, webhook, `${webhooktype}:${event}`, channelWrapperWebhooks); + break; + case 'drutiny:resultset' : + await handle(processDrutinyResultset, webhook, `${webhooktype}:${event}`, channelWrapperWebhooks); + break; + } + channelWrapperWebhooks.ack(rabbitMsg); +}; + +async function handle(handler, webhook: WebhookRequestData, fullEvent: string, channelWrapperWebhooks) { + const { + uuid + } = webhook; + + logger.info(`Handling ${fullEvent}`, { + uuid + }); + + try { + await handler(webhook, channelWrapperWebhooks); + } catch (error) { + logger.error(`Error handling ${fullEvent}`); + logger.error(error); + } +} + +async function unhandled(webhook: WebhookRequestData, fullEvent: string) { + const { + uuid + } = webhook; + + const meta = { + fullEvent: fullEvent + }; + sendToLagoonLogs( + 'info', + '', + uuid, + `unhandledWebhook`, + meta, + `Unhandled webhook ${fullEvent}` + ); + return; +} \ No newline at end of file From bf7f9d148481fa788ae5405e465106b1baf1fa66 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Fri, 19 Jun 2020 14:49:29 -0400 Subject: [PATCH 130/280] disable deleting of harbor projects for now --- services/api/src/resources/project/resolvers.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/api/src/resources/project/resolvers.ts b/services/api/src/resources/project/resolvers.ts index 1c97ed0b90..abcbc1f0f4 100644 --- a/services/api/src/resources/project/resolvers.ts +++ b/services/api/src/resources/project/resolvers.ts @@ -475,9 +475,10 @@ export const deleteProject: ResolverFn = async ( logger.error(`Could not delete default user for project ${project.name}: ${err.message}`); } - const harborOperations = createHarborOperations(sqlClient); + // @TODO discuss if we want to delete projects in harbor or not + //const harborOperations = createHarborOperations(sqlClient); - const harborResults = await harborOperations.deleteProject(project.name) + //const harborResults = await harborOperations.deleteProject(project.name) return 'success'; }; From 4438e32128ff46a54d116fafdac2c14bf76e9984 Mon Sep 17 00:00:00 2001 From: Tyler Ward Date: Fri, 19 Jun 2020 13:27:29 -0700 Subject: [PATCH 131/280] Add support for stakater/ingressmonitorcontroller (#1872) --- docs/using_lagoon/lagoon_yml.md | 14 +++++-- .../build-deploy-docker-compose.sh | 41 ++++++++----------- .../custom-ingress/templates/ingress.yaml | 11 ++++- .../helmcharts/custom-ingress/values.yaml | 7 +++- .../build-deploy-docker-compose.sh | 14 +++++++ .../openshift-templates/route.yml | 20 +++++++++ .../scripts/exec-openshift-create-route.sh | 7 +++- node-packages/commons/src/api.ts | 8 ++++ .../docker-entrypoint-initdb.d/00-tables.sql | 19 +++++---- .../01-migrations.sql | 19 +++++++++ .../03-procedures.sql | 23 ++++++----- services/api/src/models/group.ts | 2 + services/api/src/resources/group/resolvers.ts | 6 ++- .../api/src/resources/openshift/resolvers.ts | 3 +- services/api/src/typeDefs.js | 6 +++ services/kubernetesbuilddeploy/src/index.ts | 23 +++++++++++ services/openshiftbuilddeploy/src/index.ts | 24 ++++++++++- 17 files changed, 194 insertions(+), 53 deletions(-) diff --git a/docs/using_lagoon/lagoon_yml.md b/docs/using_lagoon/lagoon_yml.md index 06ef55ee23..92aee984f0 100644 --- a/docs/using_lagoon/lagoon_yml.md +++ b/docs/using_lagoon/lagoon_yml.md @@ -140,10 +140,10 @@ Environment names match your deployed branches or pull requests. This allows for #### `environments.[name].monitoring_urls` -At the end of a deploy, Lagoon will check this field for any URLs which you have specified to add to the API for the purpose of monitoring. The default value for this field is the first route for a project. It is useful for adding specific paths of a project to the API, for consumption by a monitoring service. +!!!danger + This feature will be removed in an upcoming release of Lagoon. Please use the newer [`monitoring-path` method](lagoon_yml.md#monitoring-a-specific-path) on your specific route. -!!!hint - Please note, Lagoon does not provide any direct integration to a monitoring service, this just adds the URLs to the API. On amazee.io, we take the `monitoring_urls` and add them to our StatusCake account. +At the end of a deploy, Lagoon will check this field for any URLs which you have specified to add to the API for the purpose of monitoring. The default value for this field is the first route for a project. It is useful for adding specific paths of a project to the API, for consumption by a monitoring service. #### `environments.[name].routes` @@ -176,6 +176,14 @@ In the `"www.example.com"` example repeated below, we see two more options \(als hsts: max-age=31536000 ``` +#### Monitoring a specific path +When [UptimeRobot](https://uptimerobot.com/) is configured for your cluster (OpenShift or Kubernetes), Lagoon will inject annotations to each route/ingress for use by the `stakater/IngressControllerMonitor`. The default action is to monitor the homepage of the route. If you have a specific route to be monitored, this can be overriden by adding a `monitoring-path` to your route specification. A common use is to set up a path for monitoring which bypasses caching to give a more real-time monitoring of your site. + +``` + - "www.example.com": + monitoring-path: "/bypass-cache" +``` + #### Ingress annotations (Redirects) !!!hint diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 587e502a9f..d8d6a7bd25 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -464,6 +464,13 @@ TEMPLATE_PARAMETERS=() ### CUSTOM ROUTES FROM .lagoon.yml ############################################## +# set some monitoring defaults +if [ "${ENVIRONMENT_TYPE}" == "production" ]; then + MONITORING_ENABLED="true" +else + MONITORING_ENABLED="false" +fi + # Two while loops as we have multiple services that want routes and each service has multiple routes ROUTES_SERVICE_COUNTER=0 if [ -n "$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER 2> /dev/null)" ]; then @@ -480,6 +487,7 @@ if [ -n "$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\. ROUTE_TLS_ACME=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.tls-acme true) ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) + MONITORING_PATH=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.monitoring-path "") ROUTE_ANNOTATIONS=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.annotations {}) else # Only a value given, assuming some defaults @@ -509,6 +517,10 @@ if [ -n "$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\. --set tls_acme="${ROUTE_TLS_ACME}" \ --set insecure="${ROUTE_INSECURE}" \ --set hsts="${ROUTE_HSTS}" \ + --set ingressmonitorcontroller.enabled="${MONITORING_ENABLED}" \ + --set ingressmonitorcontroller.path="${MONITORING_PATH}" \ + --set ingressmonitorcontroller.alertContacts="${MONITORING_ALERTCONTACT}" \ + --set ingressmonitorcontroller.statuspageId="${MONITORING_STATUSPAGEID}" \ -f /kubectl-build-deploy/values.yaml -f /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml "${HELM_ARGUMENTS[@]}" > $YAML_FOLDER/${ROUTE_DOMAIN}.yaml let ROUTE_DOMAIN_COUNTER=ROUTE_DOMAIN_COUNTER+1 @@ -530,6 +542,7 @@ else ROUTE_TLS_ACME=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.tls-acme true) ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) + MONITORING_PATH=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.monitoring-path "") ROUTE_ANNOTATIONS=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.annotations {}) else # Only a value given, assuming some defaults @@ -559,6 +572,10 @@ else --set tls_acme="${ROUTE_TLS_ACME}" \ --set insecure="${ROUTE_INSECURE}" \ --set hsts="${ROUTE_HSTS}" \ + --set ingressmonitorcontroller.enabled="${MONITORING_ENABLED}" \ + --set ingressmonitorcontroller.path="${MONITORING_PATH}" \ + --set ingressmonitorcontroller.alertContacts="${MONITORING_ALERTCONTACT}" \ + --set ingressmonitorcontroller.statuspageId="${MONITORING_STATUSPAGEID}" \ -f /kubectl-build-deploy/values.yaml -f /kubectl-build-deploy/${ROUTE_DOMAIN}-values.yaml "${HELM_ARGUMENTS[@]}" > $YAML_FOLDER/${ROUTE_DOMAIN}.yaml let ROUTE_DOMAIN_COUNTER=ROUTE_DOMAIN_COUNTER+1 @@ -605,20 +622,6 @@ if [ "$(ls -A $YAML_FOLDER/)" ]; then kubectl apply --insecure-skip-tls-verify -n ${NAMESPACE} -f $YAML_FOLDER/ fi -############################################## -### CUSTOM MONITORING_URLS FROM .lagoon.yml -############################################## -URL_COUNTER=0 -while [ -n "$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.monitoring_urls.$URL_COUNTER 2> /dev/null)" ]; do - MONITORING_URL="$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.monitoring_urls.$URL_COUNTER)" - if [[ $URL_COUNTER > 0 ]]; then - MONITORING_URLS="${MONITORING_URLS}, ${MONITORING_URL}" - else - MONITORING_URLS="${MONITORING_URL}" - fi - let URL_COUNTER=URL_COUNTER+1 -done - ############################################## ### PROJECT WIDE ENV VARIABLES ############################################## @@ -642,22 +645,14 @@ ROUTES=$(kubectl -n ${NAMESPACE} get ingress --sort-by='{.metadata.name}' -l "ac # Get list of autogenerated routes AUTOGENERATED_ROUTES=$(kubectl -n ${NAMESPACE} get ingress --sort-by='{.metadata.name}' -l "lagoon.sh/autogenerated=true" -o=go-template --template='{{range $indexItems, $ingress := .items}}{{if $indexItems}},{{end}}{{$tls := .spec.tls}}{{range $indexRule, $rule := .spec.rules}}{{if $indexRule}},{{end}}{{if $tls}}https://{{else}}http://{{end}}{{.host}}{{end}}{{end}}') -# If no MONITORING_URLS were specified, fall back to the ROUTE of the project -if [ -z "$MONITORING_URLS"]; then - echo "No monitoring_urls provided, using ROUTE" - MONITORING_URLS="${ROUTE}" -fi - yq write -i /kubectl-build-deploy/values.yaml 'route' "$ROUTE" yq write -i /kubectl-build-deploy/values.yaml 'routes' "$ROUTES" yq write -i /kubectl-build-deploy/values.yaml 'autogeneratedRoutes' "$AUTOGENERATED_ROUTES" -yq write -i /kubectl-build-deploy/values.yaml 'monitoringUrls' "$MONITORING_URLS" echo -e "\ LAGOON_ROUTE=${ROUTE}\n\ LAGOON_ROUTES=${ROUTES}\n\ LAGOON_AUTOGENERATED_ROUTES=${AUTOGENERATED_ROUTES}\n\ -LAGOON_MONITORING_URLS=${MONITORING_URLS}\n\ " >> /kubectl-build-deploy/values.env # Generate a Config Map with project wide env variables @@ -1046,4 +1041,4 @@ if [ "${LAGOON_POSTROLLOUT_DISABLED}" != "true" ]; then done else echo "post-rollout tasks are currently disabled LAGOON_POSTROLLOUT_DISABLED is set to true" -fi \ No newline at end of file +fi diff --git a/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/templates/ingress.yaml b/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/templates/ingress.yaml index 06489fb824..aeeb5df15b 100644 --- a/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/templates/ingress.yaml +++ b/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/templates/ingress.yaml @@ -21,6 +21,15 @@ metadata: nginx.ingress.kubernetes.io/ssl-redirect: "true" ingress.kubernetes.io/ssl-redirect: "true" {{- end }} + monitor.stakater.com/enabled: "{{ .Values.ingressmonitorcontroller.enabled }}" + uptimerobot.monitor.stakater.com/interval: "{{ .Values.ingressmonitorcontroller.interval }}" + uptimerobot.monitor.stakater.com/alert-contacts: "{{ .Values.ingressmonitorcontroller.alertContacts }}" + {{- if .Values.ingressmonitorcontroller.path }} + monitor.stakater.com/overridePath: "{{ .Values.ingressmonitorcontroller.path }}" + {{- end }} + {{- if .Values.ingressmonitorcontroller.statuspageId }} + uptimerobot.monitor.stakater.com/status-pages: "{{ .Values.ingressmonitorcontroller.statuspageId }}" + {{- end }} # HSTS Handling {{- if .Values.hsts}} # haproxy.router.openshift.io/hsts_header: {{ .Values.route_hsts }} @@ -42,5 +51,3 @@ spec: - backend: serviceName: {{ .Values.service }} servicePort: http - - diff --git a/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/values.yaml b/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/values.yaml index 1190022f2d..6f18985974 100644 --- a/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/values.yaml +++ b/images/kubectl-build-deploy-dind/helmcharts/custom-ingress/values.yaml @@ -7,4 +7,9 @@ hsts: 'null' tls_acme: true insecure: Allow service: '' -annotations: {} \ No newline at end of file +annotations: {} + +ingressmonitorcontroller: + enabled: 'false' + interval: '60' + alertContacts: 'unconfigured' diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index 3144102fbe..47d8291921 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -505,7 +505,15 @@ TEMPLATE_PARAMETERS=() ### CUSTOM ROUTES FROM .lagoon.yml ############################################## +if [ "${ENVIRONMENT_TYPE}" == "production" ]; then + MONITORING_ENABLED="true" +else + MONITORING_ENABLED="false" +fi +MONITORING_INTERVAL=60 + ROUTES_SERVICE_COUNTER=0 + # we need to check for production routes for active/standby if they are defined, as these will get migrated between environments as required if [ "${ENVIRONMENT_TYPE}" == "production" ]; then if [ "${BRANCH//./\\.}" == "${ACTIVE_ENVIRONMENT}" ]; then @@ -524,6 +532,7 @@ if [ "${ENVIRONMENT_TYPE}" == "production" ]; then ROUTE_MIGRATE=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.migrate true) ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) + MONITORING_PATH=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.monitoring-path "") else # Only a value given, assuming some defaults ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml get-value production_routes.active.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) @@ -565,6 +574,7 @@ if [ "${ENVIRONMENT_TYPE}" == "production" ]; then ROUTE_MIGRATE=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.migrate true) ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) + MONITORING_PATH=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.monitoring-path "") else # Only a value given, assuming some defaults ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml get-value production_routes.standby.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) @@ -609,6 +619,7 @@ if [ -n "$(cat .lagoon.yml | shyaml keys ${PROJECT}.environments.${BRANCH//./\\. ROUTE_MIGRATE=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.migrate false) ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) + MONITORING_PATH=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.monitoring-path "") else # Only a value given, assuming some defaults ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml get-value ${PROJECT}.environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) @@ -647,6 +658,7 @@ else ROUTE_MIGRATE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.migrate false) ROUTE_INSECURE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.insecure Redirect) ROUTE_HSTS=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.hsts null) + MONITORING_PATH=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER.$ROUTE_DOMAIN_ESCAPED.monitoring-path "") else # Only a value given, assuming some defaults ROUTE_DOMAIN=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.routes.$ROUTES_SERVICE_COUNTER.$ROUTES_SERVICE.$ROUTE_DOMAIN_COUNTER) @@ -707,8 +719,10 @@ fi ############################################## ### CUSTOM MONITORING_URLS FROM .lagoon.yml ############################################## +# @DEPRECATED - to be removed with Lagoon 2.0 URL_COUNTER=0 while [ -n "$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.monitoring_urls.$URL_COUNTER 2> /dev/null)" ]; do + echo "DEPRECATION WARNING: 'monitoring_urls' is being moved to a per-route 'monitoring-path', please update your route" MONITORING_URL="$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.monitoring_urls.$URL_COUNTER)" if [[ $URL_COUNTER > 0 ]]; then MONITORING_URLS="${MONITORING_URLS}, ${MONITORING_URL}" diff --git a/images/oc-build-deploy-dind/openshift-templates/route.yml b/images/oc-build-deploy-dind/openshift-templates/route.yml index 2f3c1e04d6..1f8ecca4ea 100644 --- a/images/oc-build-deploy-dind/openshift-templates/route.yml +++ b/images/oc-build-deploy-dind/openshift-templates/route.yml @@ -40,6 +40,21 @@ parameters: - name: ROUTE_MIGRATE description: Setting to determine if this route should be migratable for active/standby purposes required: true + - name: MONITORING_ENABLED + description: Default to monitoring disabled, only enabled on production routes + value: "false" + - name: MONITORING_INTERVAL + description: Frequency of checks by monitoring + value: "" + - name: MONITOR_ALERTCONTACTS + description: Alertcontacts to associate to this monitor + value: "" + - name: MONITORING_PATH + description: Path for monitoring of this route + value: "" + - name: MONITORING_STATUSPAGEID + description: Uptime Robot status page ID + value: "" objects: - apiVersion: v1 kind: Route @@ -48,6 +63,11 @@ objects: haproxy.router.openshift.io/disable_cookies: 'true' haproxy.router.openshift.io/hsts_header: '${ROUTE_HSTS}' kubernetes.io/tls-acme: '${ROUTE_TLS_ACME}' + monitor.stakater.com/enabled: '${MONITORING_ENABLED}' + uptimerobot.monitor.stakater.com/interval: '${MONITORING_INTERVAL}' + uptimerobot.monitor.stakater.com/alert-contacts: '${MONITOR_ALERTCONTACTS}' + monitor.stakater.com/overridePath: '${MONITORING_PATH}' + uptimerobot.monitor.stakater.com/status-pages: '${MONITORING_STATUSPAGEID}' creationTimestamp: null labels: branch: ${SAFE_BRANCH} diff --git a/images/oc-build-deploy-dind/scripts/exec-openshift-create-route.sh b/images/oc-build-deploy-dind/scripts/exec-openshift-create-route.sh index 013045e6a3..a243302e2f 100644 --- a/images/oc-build-deploy-dind/scripts/exec-openshift-create-route.sh +++ b/images/oc-build-deploy-dind/scripts/exec-openshift-create-route.sh @@ -2,7 +2,7 @@ # TODO: find out why we are using the if/else and if it's still needed for kubernetes if oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} get route "$ROUTE_DOMAIN" &> /dev/null; then - oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} patch route "$ROUTE_DOMAIN" -p "{\"metadata\":{\"labels\":{\"dioscuri.amazee.io/migrate\": \"${ROUTE_MIGRATE}\"},\"annotations\":{\"kubernetes.io/tls-acme\":\"${ROUTE_TLS_ACME}\",\"haproxy.router.openshift.io/hsts_header\":\"${ROUTE_HSTS}\"}},\"spec\":{\"to\":{\"name\":\"${ROUTE_SERVICE}\"},\"tls\":{\"insecureEdgeTerminationPolicy\":\"${ROUTE_INSECURE}\"}}}" + oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} patch route "$ROUTE_DOMAIN" -p "{\"metadata\":{\"labels\":{\"dioscuri.amazee.io/migrate\":\"${ROUTE_MIGRATE}\"},\"annotations\":{\"kubernetes.io/tls-acme\":\"${ROUTE_TLS_ACME}\",\"haproxy.router.openshift.io/hsts_header\":\"${ROUTE_HSTS}\",\"monitor.stakater.com/enabled\":\"${MONITORING_ENABLED}\",\"uptimerobot.monitor.stakater.com/interval\":\"${MONITORING_INTERVAL}\",\"uptimerobot.monitor.stakater.com/alert-contacts\":\"${MONITORING_ALERTCONTACT}\",\"monitor.stakater.com/overridePath\":\"${MONITORING_PATH}\",\"uptimerobot.monitor.stakater.com/status-pages\":\"${MONITORING_STATUSPAGEID}\"}},\"spec\":{\"to\":{\"name\":\"${ROUTE_SERVICE}\"},\"tls\":{\"insecureEdgeTerminationPolicy\":\"${ROUTE_INSECURE}\"}}}" else oc process --local -o yaml --insecure-skip-tls-verify \ -n ${OPENSHIFT_PROJECT} \ @@ -19,5 +19,10 @@ else -p ROUTE_INSECURE="${ROUTE_INSECURE}" \ -p ROUTE_HSTS="${ROUTE_HSTS}" \ -p ROUTE_MIGRATE="${ROUTE_MIGRATE}" \ + -p MONITORING_ENABLED="${MONITORING_ENABLED}" \ + -p MONITOR_ALERTCONTACTS="${MONITOR_ALERTCONTACTS}" \ + -p MONITORING_PATH="${MONITORING_PATH}" \ + -p MONITORING_INTERVAL="${MONITORING_INTERVAL}" \ + -p MONITORING_STATUSPAGEID="${MONITORING_STATUSPAGEID}" \ | outputToYaml fi diff --git a/node-packages/commons/src/api.ts b/node-packages/commons/src/api.ts index 7c9aa85074..494913d359 100644 --- a/node-packages/commons/src/api.ts +++ b/node-packages/commons/src/api.ts @@ -1000,7 +1000,9 @@ export const getOpenShiftInfoForProject = (project: string): Promise => token projectUser routerPattern + monitoringConfig } + availability gitUrl privateKey subfolder @@ -1016,6 +1018,12 @@ export const getOpenShiftInfoForProject = (project: string): Promise => value scope } + groups { + ... on BillingGroup { + type + uptimeRobotStatusPageId + } + } } } `); diff --git a/services/api-db/docker-entrypoint-initdb.d/00-tables.sql b/services/api-db/docker-entrypoint-initdb.d/00-tables.sql index 4288a8e5a4..9d5979fea9 100644 --- a/services/api-db/docker-entrypoint-initdb.d/00-tables.sql +++ b/services/api-db/docker-entrypoint-initdb.d/00-tables.sql @@ -29,15 +29,16 @@ CREATE TABLE IF NOT EXISTS customer ( ); CREATE TABLE IF NOT EXISTS openshift ( - id int NOT NULL auto_increment PRIMARY KEY, - name varchar(50) UNIQUE, - console_url varchar(300), - token varchar(2000), - router_pattern varchar(300), - project_user varchar(100), - ssh_host varchar(300), - ssh_port varchar(50), - created timestamp DEFAULT CURRENT_TIMESTAMP + id int NOT NULL auto_increment PRIMARY KEY, + name varchar(50) UNIQUE, + console_url varchar(300), + token varchar(2000), + router_pattern varchar(300), + project_user varchar(100), + ssh_host varchar(300), + ssh_port varchar(50), + monitoring_config varchar(2048), + created timestamp DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS notification_microsoftteams ( diff --git a/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql b/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql index 0d4b2f50ae..14f8c34eba 100644 --- a/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql +++ b/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql @@ -1045,6 +1045,24 @@ CREATE OR REPLACE PROCEDURE END; $$ +CREATE OR REPLACE PROCEDURE + add_monitoring_config_to_openshift() + + BEGIN + IF NOT EXISTS ( + SELECT NULL + FROM INFORMATION_SCHEMA.COLUMNS + WHERE + table_name = 'openshift' + AND table_schema = 'infrastructure' + AND column_name = 'monitoring_config' + ) THEN + ALTER TABLE `openshift` + ADD `monitoring_config` varchar(2048); + END IF; + END; +$$ + CREATE OR REPLACE PROCEDURE add_additional_harbor_scan_fields_to_environment_problem() @@ -1168,6 +1186,7 @@ CALL convert_user_ssh_key_usid_to_char(); CALL add_private_key_to_project(); CALL add_index_for_environment_backup_environment(); CALL add_enum_email_microsoftteams_to_type_in_project_notification(); +CALL add_monitoring_config_to_openshift(); CALL add_standby_production_environment_to_project(); CALL add_standby_routes_to_project(); CALL add_production_routes_to_project(); diff --git a/services/api-db/docker-entrypoint-initdb.d/03-procedures.sql b/services/api-db/docker-entrypoint-initdb.d/03-procedures.sql index 9d721f2a01..ff1c39b6bc 100644 --- a/services/api-db/docker-entrypoint-initdb.d/03-procedures.sql +++ b/services/api-db/docker-entrypoint-initdb.d/03-procedures.sql @@ -285,14 +285,15 @@ $$ CREATE OR REPLACE PROCEDURE CreateOpenshift ( - IN id int, - IN name varchar(50), - IN console_url varchar(300), - IN token varchar(2000), - IN router_pattern varchar(300), - IN project_user varchar(100), - IN ssh_host varchar(300), - IN ssh_port varchar(50) + IN id int, + IN name varchar(50), + IN console_url varchar(300), + IN token varchar(2000), + IN router_pattern varchar(300), + IN project_user varchar(100), + IN ssh_host varchar(300), + IN ssh_port varchar(50), + IN monitoring_config varchar(2048) ) BEGIN DECLARE new_oid int; @@ -309,7 +310,8 @@ CREATE OR REPLACE PROCEDURE router_pattern, project_user, ssh_host, - ssh_port + ssh_port, + monitoring_config ) VALUES ( id, name, @@ -318,7 +320,8 @@ CREATE OR REPLACE PROCEDURE router_pattern, project_user, ssh_host, - ssh_port + ssh_port, + monitoring_config ); IF (id = 0) THEN diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index 617a523113..d184e63865 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -41,6 +41,7 @@ export interface BillingGroup extends Group { currency?: string; billingSoftware?: string; type?: string; + uptimeRobotStatusPageId?: string; } interface GroupMembership { @@ -111,6 +112,7 @@ export const Group = (clients) => { type: attributeKVOrNull('type', keycloakGroup), currency: attributeKVOrNull('currency', keycloakGroup), billingSoftware: attributeKVOrNull('billingSoftware', keycloakGroup), + uptimeRobotStatusPageId: attributeKVOrNull('uptimeRobotStatusPageId', keycloakGroup), path: keycloakGroup.path, attributes: keycloakGroup.attributes, subGroups: keycloakGroup.subGroups, diff --git a/services/api/src/resources/group/resolvers.ts b/services/api/src/resources/group/resolvers.ts index 985919b3e8..0a76449645 100644 --- a/services/api/src/resources/group/resolvers.ts +++ b/services/api/src/resources/group/resolvers.ts @@ -340,7 +340,7 @@ export const addGroupsToProject: ResolverFn = async ( export const addBillingGroup: ResolverFn = async ( _root, - { input: { name, currency, billingSoftware } }, + { input: { name, currency, billingSoftware, uptimeRobotStatusPageId } }, { models, hasPermission }, ) => { await hasPermission('group', 'add'); @@ -358,6 +358,7 @@ export const addBillingGroup: ResolverFn = async ( attributes: { type: ['billing'], currency: [currency], + uptimeRobotStatusPageId: [uptimeRobotStatusPageId], ...(billingSoftware ? { billingSoftware: [billingSoftware] } : {}), }, }); @@ -377,11 +378,12 @@ export const updateBillingGroup: ResolverFn = async ( throw new Error('Input patch requires at least 1 attribute'); } - const { name, currency, billingSoftware } = patch; + const { name, currency, billingSoftware, uptimeRobotStatusPageId } = patch; const updatedAttributes = { ...attributes, type: ['billing'], ...(currency ? { currency: [currency] } : {}), + ...(uptimeRobotStatusPageId ? {uptimeRobotStatusPageId: [uptimeRobotStatusPageId] }: {}), ...(billingSoftware ? { billingSoftware: [billingSoftware] } : {}), }; diff --git a/services/api/src/resources/openshift/resolvers.ts b/services/api/src/resources/openshift/resolvers.ts index 9bec4b8f3a..3e40957b93 100644 --- a/services/api/src/resources/openshift/resolvers.ts +++ b/services/api/src/resources/openshift/resolvers.ts @@ -33,7 +33,8 @@ export const addOpenshift: ResolverFn = async ( ${input.routerPattern ? ':router_pattern' : 'NULL'}, ${input.projectUser ? ':project_user' : 'NULL'}, ${input.sshHost ? ':ssh_host' : 'NULL'}, - ${input.sshPort ? ':ssh_port' : 'NULL'} + ${input.sshPort ? ':ssh_port' : 'NULL'}, + ${input.monitoringConfig ? ':monitoring_config' : 'NULL'} ); `, ); diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 9877574399..111eff0b26 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -251,6 +251,7 @@ const typeDefs = gql` currency: String billingSoftware: String modifiers: [BillingModifier] + uptimeRobotStatusPageId: String } type Openshift { @@ -263,6 +264,7 @@ const typeDefs = gql` sshHost: String sshPort: String created: String + monitoringConfig: JSON } type NotificationMicrosoftTeams { @@ -940,6 +942,7 @@ const typeDefs = gql` projectUser: String sshHost: String sshPort: String + monitoringConfig: JSON } input DeleteOpenshiftInput { @@ -1062,6 +1065,7 @@ const typeDefs = gql` projectUser: String sshHost: String sshPort: String + monitoringConfig: JSON } input UpdateOpenshiftInput { @@ -1322,6 +1326,7 @@ const typeDefs = gql` name: String! currency: Currency! billingSoftware: String + uptimeRobotStatusPageId: String } input ProjectBillingGroupInput { @@ -1333,6 +1338,7 @@ const typeDefs = gql` name: String! currency: Currency billingSoftware: String + uptimeRobotStatusPageId: String } input UpdateBillingGroupInput { diff --git a/services/kubernetesbuilddeploy/src/index.ts b/services/kubernetesbuilddeploy/src/index.ts index 59ba91e1ae..cc09ba5ff5 100644 --- a/services/kubernetesbuilddeploy/src/index.ts +++ b/services/kubernetesbuilddeploy/src/index.ts @@ -78,6 +78,16 @@ const messageConsumer = async msg => { var openshiftPromoteSourceProject = promoteSourceEnvironment ? `${projectName}-${ocsafety(promoteSourceEnvironment)}` : "" // A secret which is the same across all Environments of this Lagoon Project var projectSecret = crypto.createHash('sha256').update(`${projectName}-${jwtSecret}`).digest('hex'); + var alertContactHA = "" + var alertContactSA = "" + var monitoringConfig = JSON.parse(projectOpenShift.openshift.monitoringConfig) || "invalid" + if (monitoringConfig != "invalid"){ + alertContactHA = monitoringConfig.uptimerobot.alertContactHA || "" + alertContactSA = monitoringConfig.uptimerobot.alertContactSA || "" + } + var availability = projectOpenShift.availability || "STANDARD" + const billingGroup = projectOpenShift.groups.find(i => i.type == "billing" ) || "" + var uptimeRobotStatusPageId = billingGroup.uptimeRobotStatusPageId || "" } catch(error) { logger.error(`Error while loading information for project ${projectName}`) logger.error(error) @@ -265,6 +275,19 @@ const messageConsumer = async msg => { if (!R.isEmpty(environment.envVariables)) { jobconfig.spec.template.spec.containers[0].env.push({"name": "LAGOON_ENVIRONMENT_VARIABLES", "value": JSON.stringify(environment.envVariables)}) } + if (alertContactHA != undefined && alertContactSA != undefined){ + if (availability == "HIGH") { + jobconfig.spec.template.spec.containers[0].env.push({"name": "MONITORING_ALERTCONTACT","value": alertContactHA}) + } else { + jobconfig.spec.template.spec.containers[0].env.push({"name": "MONITORING_ALERTCONTACT","value": alertContactSA}) + } + } else { + jobconfig.spec.template.spec.containers[0].env.push({"name": "MONITORING_ALERTCONTACT","value": "unconfigured"}) + } + if (uptimeRobotStatusPageId){ + jobconfig.spec.template.spec.containers[0].env.push({"name": "MONITORING_STATUSPAGEID","value": uptimeRobotStatusPageId}) + } + return jobconfig } diff --git a/services/openshiftbuilddeploy/src/index.ts b/services/openshiftbuilddeploy/src/index.ts index df2aebdee7..313938950f 100644 --- a/services/openshiftbuilddeploy/src/index.ts +++ b/services/openshiftbuilddeploy/src/index.ts @@ -85,8 +85,18 @@ const messageConsumer = async msg => { var graphqlEnvironmentType = environmentType.toUpperCase() var graphqlGitType = type.toUpperCase() var openshiftPromoteSourceProject = promoteSourceEnvironment ? `${safeProjectName}-${ocsafety(promoteSourceEnvironment)}` : "" - // A secret which is the same across all Environments of this Lagoon Project + // A secret which is the same across all Environments of this Lagoon Project var projectSecret = crypto.createHash('sha256').update(`${projectName}-${jwtSecret}`).digest('hex'); + var alertContactHA = "" + var alertContactSA = "" + var monitoringConfig = JSON.parse(projectOpenShift.openshift.monitoringConfig) || "invalid" + if (monitoringConfig != "invalid"){ + alertContactHA = monitoringConfig.uptimerobot.alertContactHA || "" + alertContactSA = monitoringConfig.uptimerobot.alertContactSA || "" + } + var availability = projectOpenShift.availability || "STANDARD" + const billingGroup = projectOpenShift.groups.find(i => i.type == "billing" ) || "" + var uptimeRobotStatusPageId = billingGroup.uptimeRobotStatusPageId || "" } catch(error) { logger.error(`Error while loading information for project ${projectName}`) logger.error(error) @@ -272,6 +282,18 @@ const messageConsumer = async msg => { if (!R.isEmpty(environment.envVariables)) { buildconfig.spec.strategy.customStrategy.env.push({"name": "LAGOON_ENVIRONMENT_VARIABLES", "value": JSON.stringify(environment.envVariables)}) } + if (alertContactHA != undefined && alertContactSA != undefined){ + if (availability == "HIGH") { + buildconfig.spec.strategy.customStrategy.env.push({"name": "MONITORING_ALERTCONTACT","value": alertContactHA}) + } else { + buildconfig.spec.strategy.customStrategy.env.push({"name": "MONITORING_ALERTCONTACT","value": alertContactSA}) + } + } else { + buildconfig.spec.strategy.customStrategy.env.push({"name": "MONITORING_ALERTCONTACT","value": "unconfigured"}) + } + if (uptimeRobotStatusPageId){ + buildconfig.spec.strategy.customStrategy.env.push({"name": "MONITORING_STATUSPAGEID","value": uptimeRobotStatusPageId}) + } return buildconfig } From 04aa7e1193432c99bc7c1ae437071b1aa6b6e9dc Mon Sep 17 00:00:00 2001 From: Tyler Ward Date: Fri, 19 Jun 2020 13:41:09 -0700 Subject: [PATCH 132/280] #1970 - add missing task to RC --- services/logs2rocketchat/src/readFromRabbitMQ.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/logs2rocketchat/src/readFromRabbitMQ.ts b/services/logs2rocketchat/src/readFromRabbitMQ.ts index 58429bbe72..858ea89663 100644 --- a/services/logs2rocketchat/src/readFromRabbitMQ.ts +++ b/services/logs2rocketchat/src/readFromRabbitMQ.ts @@ -112,6 +112,7 @@ export async function readFromRabbitMQ (msg: ConsumeMessage, channelWrapperLogs: case "task:deploy-openshift:finished": case "task:remove-openshift-resources:finished": case "task:builddeploy-openshift:complete": + case "task:builddeploy-kubernetes:complete": text = `*[${meta.projectName}]* ` if (meta.shortSha) { text = `${text} \`${meta.branchName}\` (${meta.shortSha})` From 69b1e928c459dbca0499beb8e2e04995a6afd07e Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Fri, 19 Jun 2020 17:17:57 -0400 Subject: [PATCH 133/280] use overrides instead of overwrite --- .../build-deploy-docker-compose.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index a1354ab452..cbbdd6341e 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -242,7 +242,7 @@ if [[ ( "$BUILD_TYPE" == "pullrequest" || "$BUILD_TYPE" == "branch" ) && ! $TH DOCKERFILE=$(cat $DOCKER_COMPOSE_YAML | shyaml get-value services.$IMAGE_NAME.build.dockerfile false) # allow to overwrite build dockerfile for this environment and service - ENVIRONMENT_DOCKERFILE_OVERRIDE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.overwrites.$IMAGE_NAME.build.dockerfile false) + ENVIRONMENT_DOCKERFILE_OVERRIDE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.overrides.$IMAGE_NAME.build.dockerfile false) if [ ! $ENVIRONMENT_DOCKERFILE_OVERRIDE == "false" ]; then DOCKERFILE=$ENVIRONMENT_DOCKERFILE_OVERRIDE fi @@ -259,7 +259,7 @@ if [[ ( "$BUILD_TYPE" == "pullrequest" || "$BUILD_TYPE" == "branch" ) && ! $TH OVERRIDE_IMAGE=$(cat $DOCKER_COMPOSE_YAML | shyaml get-value services.$IMAGE_NAME.labels.lagoon\\.image false) # allow to overwrite image that we pull for this environment and service - ENVIRONMENT_IMAGE_OVERRIDE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.overwrites.$IMAGE_NAME.image false) + ENVIRONMENT_IMAGE_OVERRIDE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.overrides.$IMAGE_NAME.image false) if [ ! $ENVIRONMENT_IMAGE_OVERRIDE == "false" ]; then OVERRIDE_IMAGE=$ENVIRONMENT_IMAGE_OVERRIDE fi @@ -285,7 +285,7 @@ if [[ ( "$BUILD_TYPE" == "pullrequest" || "$BUILD_TYPE" == "branch" ) && ! $TH BUILD_CONTEXT=$(cat $DOCKER_COMPOSE_YAML | shyaml get-value services.$IMAGE_NAME.build.context .) # allow to overwrite build context for this environment and service - ENVIRONMENT_BUILD_CONTEXT_OVERRIDE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.overwrites.$IMAGE_NAME.build.context false) + ENVIRONMENT_BUILD_CONTEXT_OVERRIDE=$(cat .lagoon.yml | shyaml get-value environments.${BRANCH//./\\.}.overrides.$IMAGE_NAME.build.context false) if [ ! $ENVIRONMENT_BUILD_CONTEXT_OVERRIDE == "false" ]; then BUILD_CONTEXT=$ENVIRONMENT_BUILD_CONTEXT_OVERRIDE fi From 7f7def0ec623be65bcd59f9d7ce5294ac8f0a3ed Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sat, 20 Jun 2020 16:06:56 -0400 Subject: [PATCH 134/280] PERSISTENT_STORAGE_NAME does not exist for these services use SERVICE_NAME instead --- .../openshift-templates/nginx-php-persistent/deployment.yml | 4 ++-- .../nginx-php-redis-persistent/deployment.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/images/oc-build-deploy-dind/openshift-templates/nginx-php-persistent/deployment.yml b/images/oc-build-deploy-dind/openshift-templates/nginx-php-persistent/deployment.yml index f46e4bfd3f..0f089522e3 100644 --- a/images/oc-build-deploy-dind/openshift-templates/nginx-php-persistent/deployment.yml +++ b/images/oc-build-deploy-dind/openshift-templates/nginx-php-persistent/deployment.yml @@ -103,7 +103,7 @@ objects: - name: ${SERVICE_NAME} persistentVolumeClaim: claimName: ${SERVICE_NAME} - - name: ${PERSISTENT_STORAGE_NAME}-twig + - name: ${SERVICE_NAME}-twig emptyDir: {} priorityClassName: lagoon-priority-${ENVIRONMENT_TYPE} containers: @@ -172,7 +172,7 @@ objects: volumeMounts: - name: ${SERVICE_NAME} mountPath: ${PERSISTENT_STORAGE_PATH} - - name: ${PERSISTENT_STORAGE_NAME}-twig + - name: ${SERVICE_NAME}-twig mountPath: ${PERSISTENT_STORAGE_PATH}/php/twig resources: requests: diff --git a/images/oc-build-deploy-dind/openshift-templates/nginx-php-redis-persistent/deployment.yml b/images/oc-build-deploy-dind/openshift-templates/nginx-php-redis-persistent/deployment.yml index 32a666fcb3..d81fbff00e 100644 --- a/images/oc-build-deploy-dind/openshift-templates/nginx-php-redis-persistent/deployment.yml +++ b/images/oc-build-deploy-dind/openshift-templates/nginx-php-redis-persistent/deployment.yml @@ -109,7 +109,7 @@ objects: - name: ${SERVICE_NAME} persistentVolumeClaim: claimName: ${SERVICE_NAME} - - name: ${PERSISTENT_STORAGE_NAME}-twig + - name: ${SERVICE_NAME}-twig emptyDir: {} priorityClassName: lagoon-priority-${ENVIRONMENT_TYPE} containers: @@ -182,7 +182,7 @@ objects: volumeMounts: - name: ${SERVICE_NAME} mountPath: ${PERSISTENT_STORAGE_PATH} - - name: ${PERSISTENT_STORAGE_NAME}-twig + - name: ${SERVICE_NAME}-twig mountPath: ${PERSISTENT_STORAGE_PATH}/php/twig resources: requests: From 80647bfd89d532fb734f9d3504d9b0b4e1b3cab6 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sat, 20 Jun 2020 16:09:51 -0400 Subject: [PATCH 135/280] #1975 typo --- images/oc-build-deploy-dind/build-deploy-docker-compose.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index 3144102fbe..ba9fd44b12 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -91,7 +91,7 @@ do if oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} get service "$SERVICE_NAME" &> /dev/null; then SERVICE_TYPE="mariadb-single" # check if an existing mariadb service instance already exists - elif oc -insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} get serviceinstance "$SERVICE_NAME" &> /dev/null; then + elif oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} get serviceinstance "$SERVICE_NAME" &> /dev/null; then SERVICE_TYPE="mariadb-shared" # check if we can use the dbaas operator elif oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} get mariadbconsumer.v1.mariadb.amazee.io &> /dev/null; then From f50d3ff36b91ce14fb28cafec4e7101f54c1ea25 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sat, 20 Jun 2020 16:13:32 -0400 Subject: [PATCH 136/280] fix issue with emails containing uppercase letters --- services/api/src/bitbucket-sync/repo-permissions.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/api/src/bitbucket-sync/repo-permissions.ts b/services/api/src/bitbucket-sync/repo-permissions.ts index fbf4c89bd5..4815443bb5 100644 --- a/services/api/src/bitbucket-sync/repo-permissions.ts +++ b/services/api/src/bitbucket-sync/repo-permissions.ts @@ -116,7 +116,9 @@ const addUser = async (email: string): Promise => { R.pluck('user'), // @ts-ignore R.pluck('email'), - )(currentMembersQuery) as [string]; + // @ts-ignore + R.map(R.toLower), + )(currentMembersQuery) as [string]; // Get current bitbucket uers const bitbucketUsers = R.pipe( @@ -124,6 +126,8 @@ const addUser = async (email: string): Promise => { R.pluck('user'), // @ts-ignore R.pluck('emailAddress'), + // @ts-ignore + R.map(R.toLower), )(userPermissions) as [string]; // Remove users from lagoon project that are removed in bitbucket repo From 089776ec30ccc0e4bbe902be23606ee7a23b7085 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sat, 20 Jun 2020 16:49:56 -0400 Subject: [PATCH 137/280] fix broken build --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1239eb5a0a..9a5d94e4ae 100644 --- a/Makefile +++ b/Makefile @@ -612,7 +612,7 @@ drupal-test-services = drush-alias webhook-tests = github gitlab bitbucket # All Tests that use API endpoints -api-tests = node features-openshift features-kubernetes nginx elasticsearch active-standby +api-tests = node features-openshift features-kubernetes nginx elasticsearch active-standby-openshift active-standby-kubernetes # All drupal tests drupal-tests = drupal drupal-postgres From 679a937d7b84691c50778d802061dc11efb06058 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sun, 21 Jun 2020 12:58:05 -0400 Subject: [PATCH 138/280] Introduce MARIADB_DATA_DIR and MARIADB_COPY_DATA_DIR_SOURCE They allow to change the mariadb data dir or to copy a data dir on start --- docs/using_lagoon/docker_images/mariadb.md | 2 ++ images/mariadb/Dockerfile | 5 ++- .../entrypoints/9999-mariadb-init.bash | 34 ++++++++++++++----- images/mariadb/my.cnf | 1 + images/mariadb/mysql-backup.sh | 2 +- .../mysql/readiness-probe.sh | 2 +- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/docs/using_lagoon/docker_images/mariadb.md b/docs/using_lagoon/docker_images/mariadb.md index 00fc6edcea..2129f3bc2a 100644 --- a/docs/using_lagoon/docker_images/mariadb.md +++ b/docs/using_lagoon/docker_images/mariadb.md @@ -42,6 +42,8 @@ Environment variables defined in MariaDB base image: | `MARIADB_LOG_SLOW` | empty | Variable to control the save of slow queries. | | `MARIADB_LOG_QUERIES` | empty | Variable to control the save of ALL queries. | | `BACKUPS_DIR` | /var/lib/mysql/backup | Default path for databases backups. | +| `MARIADB_DATA_DIR` | /var/lib/mysql | Path of the mariadb data dir, be careful, changing this can occur data loss! | +| `MARIADB_COPY_DATA_DIR_SOURCE` | unset | Path which the entrypoint script of mariadb will use to copy into the defined `MARIADB_DATA_DIR`, this can be used for prepopulating the MariaDB with a database. The scripts expects actual MariaDB data files and not a sql file! Plus it only copies data if the destination does not already have a mysql datadir in it. | If the `LAGOON_ENVIRONMENT_TYPE` variable is set to `production`, performances are set accordingly by using `MARIADB_INNODB_BUFFER_POOL_SIZE=1024` and `MARIADB_INNODB_LOG_FILE_SIZE=256`. diff --git a/images/mariadb/Dockerfile b/images/mariadb/Dockerfile index c3bf44e43d..bb400b8b70 100644 --- a/images/mariadb/Dockerfile +++ b/images/mariadb/Dockerfile @@ -53,10 +53,9 @@ COPY entrypoints/ /lagoon/entrypoints/ COPY mysql-backup.sh /lagoon/ COPY my.cnf /etc/mysql/my.cnf -RUN for i in /var/run/mysqld /var/lib/mysql /etc/mysql/conf.d /docker-entrypoint-initdb.d/ "${BACKUPS_DIR}"; \ +RUN for i in /var/run/mysqld /var/lib/mysql /etc/mysql/conf.d /docker-entrypoint-initdb.d/ "${BACKUPS_DIR}" /home; \ do mkdir -p $i; chown mysql $i; /bin/fix-permissions $i; \ - done && \ - ln -s /var/lib/mysql/.my.cnf /home/.my.cnf + done COPY root/usr/share/container-scripts/mysql/readiness-probe.sh /usr/share/container-scripts/mysql/readiness-probe.sh RUN /bin/fix-permissions /usr/share/container-scripts/mysql/ \ diff --git a/images/mariadb/entrypoints/9999-mariadb-init.bash b/images/mariadb/entrypoints/9999-mariadb-init.bash index 1540ff34bf..d27129d406 100755 --- a/images/mariadb/entrypoints/9999-mariadb-init.bash +++ b/images/mariadb/entrypoints/9999-mariadb-init.bash @@ -23,7 +23,21 @@ for arg; do esac done - +# check if MARIADB_COPY_DATA_DIR_SOURCE is set, if yes we're coping the contents of the given folder into the data dir folder +# this allows to prefill the datadir with a provided datadir (either added in a Dockerfile build, or mounted into the running container). +# This is different than just setting $MARIADB_DATA_DIR to the source folder, as only /var/lib/mysql is a persistent folder, so setting +# $MARIADB_DATA_DIR to another folder will make mariadb to not store the datadir across container restarts, while with this copy system +# the data will be prefilled and persistent across container restarts. +if [ -n "$MARIADB_COPY_DATA_DIR_SOURCE" ]; then + if [ -d ${MARIADB_DATA_DIR:-/var/lib/mysql}/mysql ]; then + echo "MARIADB_COPY_DATA_DIR_SOURCE is set, but MySQL directory already present in '${MARIADB_DATA_DIR:-/var/lib/mysql}/mysql' skipping copying" + else + echo "MARIADB_COPY_DATA_DIR_SOURCE is set, copying datadir contents from '$MARIADB_COPY_DATA_DIR_SOURCE' to '${MARIADB_DATA_DIR:-/var/lib/mysql}'" + CUR_DIR=${PWD} + cd ${MARIADB_COPY_DATA_DIR_SOURCE}/; tar cf - . | (cd ${MARIADB_DATA_DIR:-/var/lib/mysql}; tar xvf -) + cd $CUR_DIR + fi +fi if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then if [ ! -d "/run/mysqld" ]; then @@ -31,7 +45,7 @@ if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then chown -R mysql:mysql /run/mysqld fi - if [ -d /var/lib/mysql/mysql ]; then + if [ -d ${MARIADB_DATA_DIR:-/var/lib/mysql}/mysql ]; then echo "MySQL directory already present, skipping creation" echo "starting mysql for mysql upgrade." @@ -56,7 +70,7 @@ if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then else echo "MySQL data directory not found, creating initial DBs" - mysql_install_db --skip-name-resolve --skip-test-db --auth-root-authentication-method=normal --datadir=/var/lib/mysql --basedir=/usr + mysql_install_db --skip-name-resolve --skip-test-db --auth-root-authentication-method=normal --datadir=${MARIADB_DATA_DIR:-/var/lib/mysql} --basedir=/usr echo "starting mysql for initdb.d import." /usr/bin/mysqld --skip-networking --wsrep_on=OFF & @@ -107,11 +121,11 @@ EOF cat $tfile | mysql -v -u root rm -v -f $tfile - echo "[client]" >> /var/lib/mysql/.my.cnf - echo "user=root" >> /var/lib/mysql/.my.cnf - echo "password=${MARIADB_ROOT_PASSWORD}" >> /var/lib/mysql/.my.cnf - echo "[mysql]" >> /var/lib/mysql/.my.cnf - echo "database=${MARIADB_DATABASE}" >> /var/lib/mysql/.my.cnf + echo "[client]" >> ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf + echo "user=root" >> ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf + echo "password=${MARIADB_ROOT_PASSWORD}" >> ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf + echo "[mysql]" >> ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf + echo "database=${MARIADB_DATABASE}" >> ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf for f in `ls /docker-entrypoint-initdb.d/*`; do case "$f" in @@ -129,6 +143,8 @@ EOF fi -echo "done, now starting daemon" + ln -s ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf /home/.my.cnf + + echo "done, now starting daemon" fi diff --git a/images/mariadb/my.cnf b/images/mariadb/my.cnf index d7714558e0..2a727bb2d0 100644 --- a/images/mariadb/my.cnf +++ b/images/mariadb/my.cnf @@ -9,6 +9,7 @@ socket = /run/mysqld/mysqld.sock [mysqld] port = 3306 socket = /run/mysqld/mysqld.sock +datadir = ${MARIADB_DATA_DIR:-/var/lib/mysql} character_set_server = ${MARIADB_CHARSET:-utf8mb4} collation_server = ${MARIADB_COLLATION:-utf8mb4_bin} expire_logs_days = 10 diff --git a/images/mariadb/mysql-backup.sh b/images/mariadb/mysql-backup.sh index bf66958096..bea76622ef 100755 --- a/images/mariadb/mysql-backup.sh +++ b/images/mariadb/mysql-backup.sh @@ -21,7 +21,7 @@ set -eu -o pipefail # directory to put the backup files -BACKUP_DIR=/var/lib/mysql/backup +BACKUP_DIR=${MARIADB_DATA_DIR:-/var/lib/mysql}/backup # MYSQL Parameters MARIADB_USER=${MARIADB_USER:-lagoon} diff --git a/images/mariadb/root/usr/share/container-scripts/mysql/readiness-probe.sh b/images/mariadb/root/usr/share/container-scripts/mysql/readiness-probe.sh index d8c6d827c3..368be4374b 100755 --- a/images/mariadb/root/usr/share/container-scripts/mysql/readiness-probe.sh +++ b/images/mariadb/root/usr/share/container-scripts/mysql/readiness-probe.sh @@ -3,7 +3,7 @@ # openshift-mariadb: mysqld readinessProbe # -mysql --defaults-file=/var/lib/mysql/.my.cnf -e"SHOW DATABASES;" +mysql --defaults-file=${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf -e"SHOW DATABASES;" if [ $? -ne 0 ]; then exit 1 From 90d267e17b377863597e5d30015f0ea02d9bd89c Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sun, 21 Jun 2020 13:01:18 -0400 Subject: [PATCH 139/280] Introduce SOLR_DATA_DIR and SOLR_COPY_DATA_DIR_SOURCE They allow to change the solr data dir or to copy a data dir on start --- docs/using_lagoon/docker_images/solr.md | 3 ++- images/solr/20-solr-datadir.sh | 35 ++++++++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/using_lagoon/docker_images/solr.md b/docs/using_lagoon/docker_images/solr.md index 5ed29c1b06..55b8fb2883 100644 --- a/docs/using_lagoon/docker_images/solr.md +++ b/docs/using_lagoon/docker_images/solr.md @@ -25,4 +25,5 @@ Environment variables defined in `Solr` base image. | Environment Variable | Default | Description | | :--- | :--- | :--- | | `SOLR_JAVA_MEM` | 512M | Default Java HEAP size \(ie. `SOLR_JAVA_MEM="-Xms10g -Xmx10g"` \). | - +| `SOLR_DATA_DIR` | /var/solr | Path of the solr data dir, be careful, changing this can occur data loss! | +| `SOLR_COPY_DATA_DIR_SOURCE` | unset | Path which the entrypoint script of solr will use to copy into the defined `SOLR_DATA_DIR`, this can be used for prepopulating the Solr with a core. The scripts expects actual Solr data files! Plus it only copies data if the destination does not already have a solr core in it. | \ No newline at end of file diff --git a/images/solr/20-solr-datadir.sh b/images/solr/20-solr-datadir.sh index c9e9e955aa..e971914506 100755 --- a/images/solr/20-solr-datadir.sh +++ b/images/solr/20-solr-datadir.sh @@ -1,6 +1,27 @@ #!/bin/sh set -eo pipefail +# check if SOLR_COPY_DATA_DIR_SOURCE is set, if yes we're coping the contents of the given folder into the data dir folder +# this allows to prefill the datadir with a provided datadir (either added in a Dockerfile build, or mounted into the running container). +# This is different than just setting $SOLR_DATA_DIR to the source folder, as only /var/solr is a persistent folder, so setting +# $SOLR_DATA_DIR to another folder will make solr to not store the datadir across container restarts, while with this copy system +# the data will be prefilled and persistent across container restarts. +if [ -n "$SOLR_COPY_DATA_DIR_SOURCE" ] + echo "MARIADB_COPY_DATA_DIR_SOURCE is set, start copying from source location" + for solrcorepath in $(ls -d $SOLR_COPY_DATA_DIR_SOURCE/*/ | grep -v lost+found) ; do + corename=$(basename $solrcorepath) + if [ -d ${SOLR_DATA_DIR:-/var/solr}/$corename ]; then + echo "core $corename already present in destination, skipping copying" + else + echo "copying datadir contents from '$SOLR_COPY_DATA_DIR_SOURCE/$corename to '${SOLR_DATA_DIR:-/var/solr}/$corename'" + CUR_DIR=${PWD} + mkdir ${SOLR_DATA_DIR:-/var/solr}/$corename + cd $SOLR_COPY_DATA_DIR_SOURCE/$corename; tar cf - . | (cd ${SOLR_DATA_DIR:-/var/solr}/$corename; tar xvf -) + cd $CUR_DIR + fi + done +fi + # Previously the Solr Config and Solr Data Dir was both kept in the persistent volume: # - Solr data: /opt/solr/server/solr/mycores/${corename}/data # - Solr config: /opt/solr/server/solr/mycores/${corename}/config @@ -41,9 +62,9 @@ if [ ! -n "$(ls /opt/solr/server/solr/mycores)" ]; then printf "\n\n" fi -if [ -n "$(ls /var/solr)" ]; then +if [ -n "$(ls ${SOLR_DATA_DIR:-/var/solr})" ]; then # Iterate through all existing solr cores - for solrcorepath in $(ls -d /var/solr/*/ | grep -v lost+found) ; do + for solrcorepath in $(ls -d ${SOLR_DATA_DIR:-/var/solr}/*/ | grep -v lost+found) ; do corename=$(basename $solrcorepath) if [ -d ${solrcorepath}data ]; then echo "${solrcorepath} has it's data in deprecated location ${solrcorepath}data, moving to ${solrcorepath}." @@ -72,17 +93,19 @@ fi function fixConfig { fail=0 - if cat $1/solrconfig.xml | grep dataDir | grep -qv '/var/solr/${solr.core.name}'; then + if cat $1/solrconfig.xml | grep dataDir | grep -qv "${SOLR_DATA_DIR:-/var/solr}/\${solr.core.name}"; then echo "Found old non lagoon compatible dataDir config in solrconfig.xml:" cat $1/solrconfig.xml | grep dataDir + SOLR_DATA_DIR=${SOLR_DATA_DIR:-/var/solr} + SOLR_DATA_DIR_ESCAPED=${SOLR_DATA_DIR////\\/} # escapig the forward slashes with backslahes if [ -w $1/ ]; then - sed -ibak 's/.*/\/var\/solr\/${solr.core.name}<\/dataDir>/' $1/solrconfig.xml + sed -ibak "s/.*/$SOLR_DATA_DIR_ESCAPED\/\${solr.core.name}<\/dataDir>/" $1/solrconfig.xml echo "automagically updated to compatible config: " - echo ' /var/solr/${solr.core.name}' + echo " ${SOLR_DATA_DIR:-/var/solr}/\${solr.core.name}" echo "Please update your solrconfig.xml to make this persistent." else echo "but no write permission to automagically change to compatible config: " - echo ' /var/solr/${solr.core.name}' + echo " ${SOLR_DATA_DIR:-/var/solr}/\${solr.core.name}" echo "Please update your solrconfig.xml and commit again." fail=1 fi From 05bf84e7cfee716780b242e1b1a73bd9a8101f09 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sun, 21 Jun 2020 13:36:36 -0400 Subject: [PATCH 140/280] fix wrong naming --- .../helmcharts/cli-persistent/templates/_helpers.tpl | 2 +- .../helmcharts/nginx-php-persistent/templates/_helpers.tpl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/images/kubectl-build-deploy-dind/helmcharts/cli-persistent/templates/_helpers.tpl b/images/kubectl-build-deploy-dind/helmcharts/cli-persistent/templates/_helpers.tpl index c05589bb6b..e70d4d2236 100644 --- a/images/kubectl-build-deploy-dind/helmcharts/cli-persistent/templates/_helpers.tpl +++ b/images/kubectl-build-deploy-dind/helmcharts/cli-persistent/templates/_helpers.tpl @@ -79,7 +79,7 @@ lagoon.sh/prBaseBranch: {{ .Values.prBaseBranch | quote }} Generate name for twig storage emptyDir */}} {{- define "cli-persistent.twig-storage.name" -}} -{{- printf "%s-twig" .Values.persistentStorage.name }} +{{- printf "%s-twig" (include "cli-persistent.persistentStorageName" .) }} {{- end -}} {{/* diff --git a/images/kubectl-build-deploy-dind/helmcharts/nginx-php-persistent/templates/_helpers.tpl b/images/kubectl-build-deploy-dind/helmcharts/nginx-php-persistent/templates/_helpers.tpl index a1b413fb4a..98ba6e299d 100644 --- a/images/kubectl-build-deploy-dind/helmcharts/nginx-php-persistent/templates/_helpers.tpl +++ b/images/kubectl-build-deploy-dind/helmcharts/nginx-php-persistent/templates/_helpers.tpl @@ -92,13 +92,13 @@ lagoon.sh/prBaseBranch: {{ .Values.prBaseBranch | quote }} {{/* Generate name for twig storage emptyDir */}} -{{- define "cli-persistent.twig-storage.name" -}} -{{- printf "%s-twig" .Values.persistentStorage.name }} +{{- define "nginx-php-persistent.twig-storage.name" -}} +{{- printf "%s-twig" (include "nginx-php-persistent.persistentStorageName" .) }} {{- end -}} {{/* Generate path for twig storage emptyDir */}} -{{- define "cli-persistent.twig-storage.path" -}} +{{- define "nginx-php-persistent.twig-storage.path" -}} {{- printf "%s/php/twig" .Values.persistentStorage.path }} {{- end -}} From e73e239f326274565daf38b7a4b0f5b1176bc35b Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sun, 21 Jun 2020 13:59:06 -0400 Subject: [PATCH 141/280] correct order of symlink --- images/mariadb/entrypoints/9999-mariadb-init.bash | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/images/mariadb/entrypoints/9999-mariadb-init.bash b/images/mariadb/entrypoints/9999-mariadb-init.bash index d27129d406..fecbd795a0 100755 --- a/images/mariadb/entrypoints/9999-mariadb-init.bash +++ b/images/mariadb/entrypoints/9999-mariadb-init.bash @@ -39,6 +39,8 @@ if [ -n "$MARIADB_COPY_DATA_DIR_SOURCE" ]; then fi fi +ln -s ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf /home/.my.cnf + if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then if [ ! -d "/run/mysqld" ]; then mkdir -p /run/mysqld @@ -143,8 +145,6 @@ EOF fi - ln -s ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf /home/.my.cnf - echo "done, now starting daemon" fi From e684c042b176b43b6a2738ecf7a8bc5f6e67e26f Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Mon, 22 Jun 2020 09:22:45 +1200 Subject: [PATCH 142/280] Adds healthz-php to php fpm --- images/php/fpm/Dockerfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/images/php/fpm/Dockerfile b/images/php/fpm/Dockerfile index 2473a3a753..b92deaaf4b 100644 --- a/images/php/fpm/Dockerfile +++ b/images/php/fpm/Dockerfile @@ -102,6 +102,19 @@ RUN apk add --no-cache fcgi \ && fix-permissions /app \ && fix-permissions /etc/ssmtp/ssmtp.conf + +# Defining Versions - Composer +# @see https://getcomposer.org/download/ +ENV COMPOSER_VERSION=1.10.7 \ + COMPOSER_HASH_SHA256=b94b872729668de5b5fbf62f16ff588d2a23480dda88c0e45cb43b721b75ae29 + +RUN curl -L -o /tmp/composer https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \ + && echo "$COMPOSER_HASH_SHA256 /tmp/composer" | sha256sum \ + && chmod +x /tmp/composer \ + && php -d memory_limit=-1 /tmp/composer create-project amazeeio/healthz-php /healthz-php \ + && rm /tmp/composer + + EXPOSE 9000 ENV AMAZEEIO_DB_HOST=mariadb \ From f182d0a405be27a9bcceeb548612fd4ee9b39862 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sun, 21 Jun 2020 20:43:11 -0400 Subject: [PATCH 143/280] fix not existing template --- .../helmcharts/cli-persistent/templates/_helpers.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/kubectl-build-deploy-dind/helmcharts/cli-persistent/templates/_helpers.tpl b/images/kubectl-build-deploy-dind/helmcharts/cli-persistent/templates/_helpers.tpl index e70d4d2236..c05589bb6b 100644 --- a/images/kubectl-build-deploy-dind/helmcharts/cli-persistent/templates/_helpers.tpl +++ b/images/kubectl-build-deploy-dind/helmcharts/cli-persistent/templates/_helpers.tpl @@ -79,7 +79,7 @@ lagoon.sh/prBaseBranch: {{ .Values.prBaseBranch | quote }} Generate name for twig storage emptyDir */}} {{- define "cli-persistent.twig-storage.name" -}} -{{- printf "%s-twig" (include "cli-persistent.persistentStorageName" .) }} +{{- printf "%s-twig" .Values.persistentStorage.name }} {{- end -}} {{/* From a8c62a82ade502574fde023404ec7d510a75b36c Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sun, 21 Jun 2020 20:46:32 -0400 Subject: [PATCH 144/280] fix broken if --- images/solr/20-solr-datadir.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/solr/20-solr-datadir.sh b/images/solr/20-solr-datadir.sh index e971914506..a325aa6750 100755 --- a/images/solr/20-solr-datadir.sh +++ b/images/solr/20-solr-datadir.sh @@ -6,7 +6,7 @@ set -eo pipefail # This is different than just setting $SOLR_DATA_DIR to the source folder, as only /var/solr is a persistent folder, so setting # $SOLR_DATA_DIR to another folder will make solr to not store the datadir across container restarts, while with this copy system # the data will be prefilled and persistent across container restarts. -if [ -n "$SOLR_COPY_DATA_DIR_SOURCE" ] +if [ -n "$SOLR_COPY_DATA_DIR_SOURCE" ]; then echo "MARIADB_COPY_DATA_DIR_SOURCE is set, start copying from source location" for solrcorepath in $(ls -d $SOLR_COPY_DATA_DIR_SOURCE/*/ | grep -v lost+found) ; do corename=$(basename $solrcorepath) From 18af267c8f16d613063efea7de263aeeadd30941 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 22 Jun 2020 16:42:40 +1000 Subject: [PATCH 145/280] add option to allow pullrequest routes to be autogenerated if autogenerated routes are disabled --- docs/using_lagoon/lagoon_yml.md | 4 ++++ .../kubectl-build-deploy-dind/build-deploy-docker-compose.sh | 4 ++++ images/oc-build-deploy-dind/build-deploy-docker-compose.sh | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/using_lagoon/lagoon_yml.md b/docs/using_lagoon/lagoon_yml.md index 92aee984f0..aa28c37258 100644 --- a/docs/using_lagoon/lagoon_yml.md +++ b/docs/using_lagoon/lagoon_yml.md @@ -126,6 +126,10 @@ Note: If you would like to temporarily disable pre/post-rollout tasks during a d This allows for the disabling of the automatically created routes \(NOT the custom routes per environment, see below for them\) all together. +### `routes.autogenerate.allow_pullrequests` + +This allows pull request routes to be autogenerated, even if the main autogenerated routes are disabled. + ### `routes.autogenerate.insecure` This allows you to define the behavior of the automatic creates routes \(NOT the custom routes per environment, see below for more\). The following options are allowed: diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 1443ba01bb..14900a0362 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -348,6 +348,10 @@ else fi ROUTES_AUTOGENERATE_ENABLED=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.enabled true) +ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.allow_pullrequests $ROUTES_AUTOGENERATE_ENABLED) +if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then + ROUTES_AUTOGENERATE_ENABLED=true +fi touch /kubectl-build-deploy/values.yaml diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index 3ae4948714..848c51d76c 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -429,7 +429,10 @@ else fi ROUTES_AUTOGENERATE_ENABLED=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.enabled true) - +ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.allow_pullrequests $ROUTES_AUTOGENERATE_ENABLED) +if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then + ROUTES_AUTOGENERATE_ENABLED=true +fi for SERVICE_TYPES_ENTRY in "${SERVICE_TYPES[@]}" do From 677fa0e8b564eacea8ccf064ed1451a5cbf2da79 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 22 Jun 2020 19:09:39 +1000 Subject: [PATCH 146/280] add branch_regex for autogenerated routes --- .../kubectl-build-deploy-dind/build-deploy-docker-compose.sh | 4 ++++ images/oc-build-deploy-dind/build-deploy-docker-compose.sh | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 14900a0362..ced83556ad 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -352,6 +352,10 @@ ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autoge if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi +ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex) +if [[ "${BRANCH//./\\.}" =~ $ROUTES_AUTOGENERATE_BRANCH_REGEX ]]; then + ROUTES_AUTOGENERATE_ENABLED=true +fi touch /kubectl-build-deploy/values.yaml diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index 848c51d76c..af302126f8 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -433,6 +433,10 @@ ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autoge if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi +ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex) +if [[ "${BRANCH//./\\.}" =~ $ROUTES_AUTOGENERATE_BRANCH_REGEX ]]; then + ROUTES_AUTOGENERATE_ENABLED=true +fi for SERVICE_TYPES_ENTRY in "${SERVICE_TYPES[@]}" do From 8e47bc2393e3b0ac429a6746b3ba235f11365bd2 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 22 Jun 2020 19:09:48 +1000 Subject: [PATCH 147/280] update documentation --- docs/using_lagoon/lagoon_yml.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/using_lagoon/lagoon_yml.md b/docs/using_lagoon/lagoon_yml.md index aa28c37258..6a40413c49 100644 --- a/docs/using_lagoon/lagoon_yml.md +++ b/docs/using_lagoon/lagoon_yml.md @@ -128,7 +128,25 @@ This allows for the disabling of the automatically created routes \(NOT the cust ### `routes.autogenerate.allow_pullrequests` -This allows pull request routes to be autogenerated, even if the main autogenerated routes are disabled. +This allows pull request to get autogenerated routes when route autogeneration is disabled. + +``` +routes: + autogenerate: + disabled: true + allow_pullrequests: true +``` + +### `routes.autogenerate.branch_regex` + +This allows for any branches in the regex to get autogenerated routes when route autogeneration is disabled. + +``` +routes: + autogenerate: + disabled: true + branch_regex: ^(develop|test)$ +``` ### `routes.autogenerate.insecure` From c5b46d0136178647ee431c75d8a13e196d77267e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 22 Jun 2020 19:12:29 +1000 Subject: [PATCH 148/280] update documentation --- docs/using_lagoon/lagoon_yml.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using_lagoon/lagoon_yml.md b/docs/using_lagoon/lagoon_yml.md index 6a40413c49..43ea3ed4b0 100644 --- a/docs/using_lagoon/lagoon_yml.md +++ b/docs/using_lagoon/lagoon_yml.md @@ -133,7 +133,7 @@ This allows pull request to get autogenerated routes when route autogeneration i ``` routes: autogenerate: - disabled: true + enabled: false allow_pullrequests: true ``` @@ -144,7 +144,7 @@ This allows for any branches in the regex to get autogenerated routes when route ``` routes: autogenerate: - disabled: true + enabled: false branch_regex: ^(develop|test)$ ``` From e5d43868e29f5fc8a22b998edda121bd92fb4eec Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 22 Jun 2020 19:16:48 +1000 Subject: [PATCH 149/280] set a blank fallback to use what ever is set in enabled --- images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh | 2 +- images/oc-build-deploy-dind/build-deploy-docker-compose.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index ced83556ad..27d796543e 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -352,7 +352,7 @@ ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autoge if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi -ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex) +ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex "") if [[ "${BRANCH//./\\.}" =~ $ROUTES_AUTOGENERATE_BRANCH_REGEX ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index af302126f8..ff2da5650d 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -433,7 +433,7 @@ ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autoge if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi -ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex) +ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex "") if [[ "${BRANCH//./\\.}" =~ $ROUTES_AUTOGENERATE_BRANCH_REGEX ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi From 0d646d906d27e5c2590a0555131ba5a368e901d1 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 22 Jun 2020 21:41:05 +1000 Subject: [PATCH 150/280] fix default regex --- images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh | 2 +- images/oc-build-deploy-dind/build-deploy-docker-compose.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 27d796543e..7cd569a6e5 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -352,7 +352,7 @@ ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autoge if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi -ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex "") +ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex "^$") if [[ "${BRANCH//./\\.}" =~ $ROUTES_AUTOGENERATE_BRANCH_REGEX ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index ff2da5650d..151ca99c78 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -433,7 +433,7 @@ ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autoge if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi -ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex "") +ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex "^$") if [[ "${BRANCH//./\\.}" =~ $ROUTES_AUTOGENERATE_BRANCH_REGEX ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi From 6ea9d04e7219c06dcd179f895080e5d96958d22e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 22 Jun 2020 22:15:16 +1000 Subject: [PATCH 151/280] move the branch definition to environments block instead and check here if the autogenerated route should be created --- docs/using_lagoon/lagoon_yml.md | 23 ++++++++++--------- .../build-deploy-docker-compose.sh | 5 ++-- .../build-deploy-docker-compose.sh | 5 ++-- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/using_lagoon/lagoon_yml.md b/docs/using_lagoon/lagoon_yml.md index 43ea3ed4b0..9fe457f44f 100644 --- a/docs/using_lagoon/lagoon_yml.md +++ b/docs/using_lagoon/lagoon_yml.md @@ -137,17 +137,6 @@ routes: allow_pullrequests: true ``` -### `routes.autogenerate.branch_regex` - -This allows for any branches in the regex to get autogenerated routes when route autogeneration is disabled. - -``` -routes: - autogenerate: - enabled: false - branch_regex: ^(develop|test)$ -``` - ### `routes.autogenerate.insecure` This allows you to define the behavior of the automatic creates routes \(NOT the custom routes per environment, see below for more\). The following options are allowed: @@ -294,6 +283,18 @@ environments: mariadb: statefulset ``` +### `environments.[name].autogenerateRoutes` + +This allows for any environments to get autogenerated routes when route autogeneration is disabled. + +``` +routes: + autogenerate: + enabled: false +environments: + develop: + autogenerateRoutes: true +``` #### Cron jobs - `environments.[name].cronjobs` diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 7cd569a6e5..199821f0af 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -352,8 +352,9 @@ ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autoge if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi -ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex "^$") -if [[ "${BRANCH//./\\.}" =~ $ROUTES_AUTOGENERATE_BRANCH_REGEX ]]; then +## fail silently if the key autogenerateRoutes doesn't exist and default to whatever ROUTES_AUTOGENERATE_ENABLED is set to +ROUTES_AUTOGENERATE_BRANCH=$(cat .lagoon.yml | shyaml -q get-value environments.${BRANCH//./\\.}.autogenerateRoutes $ROUTES_AUTOGENERATE_ENABLED) +if [ "$ROUTES_AUTOGENERATE_BRANCH" =~ [Tt]rue ]; then ROUTES_AUTOGENERATE_ENABLED=true fi diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index 151ca99c78..078fb72b65 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -433,8 +433,9 @@ ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autoge if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi -ROUTES_AUTOGENERATE_BRANCH_REGEX=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.branch_regex "^$") -if [[ "${BRANCH//./\\.}" =~ $ROUTES_AUTOGENERATE_BRANCH_REGEX ]]; then +## fail silently if the key autogenerateRoutes doesn't exist and default to whatever ROUTES_AUTOGENERATE_ENABLED is set to +ROUTES_AUTOGENERATE_BRANCH=$(cat .lagoon.yml | shyaml -q get-value environments.${BRANCH//./\\.}.autogenerateRoutes $ROUTES_AUTOGENERATE_ENABLED) +if [ "$ROUTES_AUTOGENERATE_BRANCH" =~ [Tt]rue ]; then ROUTES_AUTOGENERATE_ENABLED=true fi From 3cce76529be9ecc75532ae804ed8b14deacdfc6e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 22 Jun 2020 22:20:33 +1000 Subject: [PATCH 152/280] styling of yaml --- docs/using_lagoon/lagoon_yml.md | 4 ++-- .../kubectl-build-deploy-dind/build-deploy-docker-compose.sh | 2 +- images/oc-build-deploy-dind/build-deploy-docker-compose.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/using_lagoon/lagoon_yml.md b/docs/using_lagoon/lagoon_yml.md index 9fe457f44f..8c91ec1cc4 100644 --- a/docs/using_lagoon/lagoon_yml.md +++ b/docs/using_lagoon/lagoon_yml.md @@ -126,7 +126,7 @@ Note: If you would like to temporarily disable pre/post-rollout tasks during a d This allows for the disabling of the automatically created routes \(NOT the custom routes per environment, see below for them\) all together. -### `routes.autogenerate.allow_pullrequests` +### `routes.autogenerate.allowPullrequests` This allows pull request to get autogenerated routes when route autogeneration is disabled. @@ -134,7 +134,7 @@ This allows pull request to get autogenerated routes when route autogeneration i routes: autogenerate: enabled: false - allow_pullrequests: true + allowPullrequests: true ``` ### `routes.autogenerate.insecure` diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 199821f0af..39fe5ba0ec 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -348,7 +348,7 @@ else fi ROUTES_AUTOGENERATE_ENABLED=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.enabled true) -ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.allow_pullrequests $ROUTES_AUTOGENERATE_ENABLED) +ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.allowPullrequests $ROUTES_AUTOGENERATE_ENABLED) if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index 078fb72b65..09d06f500f 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -429,7 +429,7 @@ else fi ROUTES_AUTOGENERATE_ENABLED=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.enabled true) -ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.allow_pullrequests $ROUTES_AUTOGENERATE_ENABLED) +ROUTES_AUTOGENERATE_ALLOW_PRS=$(cat .lagoon.yml | shyaml get-value routes.autogenerate.allowPullrequests $ROUTES_AUTOGENERATE_ENABLED) if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then ROUTES_AUTOGENERATE_ENABLED=true fi From dedf26e158b6e5c79865137ab8fa76988f0e1232 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 22 Jun 2020 09:40:38 -0500 Subject: [PATCH 153/280] Add newrelic to api --- docker-compose.yaml | 4 + services/api/package.json | 11 ++- services/api/src/apolloServer.js | 110 +++++++++++++++++++----- services/api/src/index.js | 1 + services/api/src/newrelic.js | 49 +++++++++++ yarn.lock | 138 +++++++++++++++++++++++++++++-- 6 files changed, 286 insertions(+), 27 deletions(-) create mode 100644 services/api/src/newrelic.js diff --git a/docker-compose.yaml b/docker-compose.yaml index 05feea5519..89abb67677 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -267,6 +267,10 @@ services: - keycloak ports: - '3000:3000' + # Uncomment for local new relic tracking + # environment: + # - NEW_RELIC_LICENSE_KEY= + # - NEW_RELIC_APP_NAME=api-local labels: lagoon.type: custom lagoon.template: services/api/.lagoon.app.yml diff --git a/services/api/package.json b/services/api/package.json index 1b4aa8f28c..64d0dc55a2 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -23,8 +23,14 @@ "sync:harbor:projects": "node dist/migrations/2-harbor/harborSync.js" }, "nodemonConfig": { - "ignore": ["*.test.js", "../../node-packages/commons/dist/"], - "watch": ["src", "../../node-packages/"], + "ignore": [ + "*.test.js", + "../../node-packages/commons/dist/" + ], + "watch": [ + "src", + "../../node-packages/" + ], "ext": "js,ts,json", "exec": "yarn build --incremental && yarn start --inspect=0.0.0.0:9229" }, @@ -60,6 +66,7 @@ "mariasql": "^0.2.6", "moment": "^2.24.0", "morgan": "^1.9.0", + "newrelic": "^6.9.0", "node-cache": "^4.2.1", "ramda": "^0.25.0", "snakecase-keys": "^1.2.0", diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index 4fcc62484f..bb266f7445 100644 --- a/services/api/src/apolloServer.js +++ b/services/api/src/apolloServer.js @@ -2,14 +2,16 @@ const R = require('ramda'); const { ApolloServer, AuthenticationError, - makeExecutableSchema, + makeExecutableSchema } = require('apollo-server-express'); const NodeCache = require('node-cache'); +const gql = require('graphql-tag'); +const newrelic = require('newrelic'); const { getCredentialsForLegacyToken, getGrantForKeycloakToken, legacyHasPermission, - keycloakHasPermission, + keycloakHasPermission } = require('./util/auth'); const { getSqlClient } = require('./clients/sqlClient'); const { getKeycloakAdminClient } = require('./clients/keycloak-admin'); @@ -29,6 +31,7 @@ const apolloServer = new ApolloServer({ schema, debug: process.env.NODE_ENV === 'development', introspection: true, + tracing: true, subscriptions: { onConnect: async (connectionParams, webSocket) => { const token = R.prop('authToken', connectionParams); @@ -54,7 +57,7 @@ const apolloServer = new ApolloServer({ if (!grant) { legacyCredentials = await getCredentialsForLegacyToken( sqlClientLegacy, - token, + token ); sqlClientLegacy.end(); } @@ -66,7 +69,7 @@ const apolloServer = new ApolloServer({ const keycloakAdminClient = await getKeycloakAdminClient(); const requestCache = new NodeCache({ stdTTL: 0, - checkperiod: 0, + checkperiod: 0 }); const sqlClient = getSqlClient(); @@ -82,10 +85,16 @@ const apolloServer = new ApolloServer({ models: { UserModel: User.User({ keycloakAdminClient }), GroupModel: Group.Group({ keycloakAdminClient }), - BillingModel: BillingModel.BillingModel({ keycloakAdminClient, sqlClient }), - ProjectModel: ProjectModel.ProjectModel({ keycloakAdminClient, sqlClient }), + BillingModel: BillingModel.BillingModel({ + keycloakAdminClient, + sqlClient + }), + ProjectModel: ProjectModel.ProjectModel({ + keycloakAdminClient, + sqlClient + }), EnvironmentModel: EnvironmentModel.EnvironmentModel({ sqlClient }) - }, + } }; }, onDisconnect: (websocket, context) => { @@ -95,14 +104,14 @@ const apolloServer = new ApolloServer({ if (context.requestCache) { context.requestCache.flushAll(); } - }, + } }, context: async ({ req, connection }) => { // Websocket requests if (connection) { // onConnect must always provide connection.context. return { - ...connection.context, + ...connection.context }; } @@ -111,10 +120,10 @@ const apolloServer = new ApolloServer({ const keycloakAdminClient = await getKeycloakAdminClient(); const requestCache = new NodeCache({ stdTTL: 0, - checkperiod: 0, + checkperiod: 0 }); - const sqlClient = getSqlClient() + const sqlClient = getSqlClient(); return { keycloakAdminClient, @@ -123,7 +132,7 @@ const apolloServer = new ApolloServer({ ? keycloakHasPermission( req.kauth.grant, requestCache, - keycloakAdminClient, + keycloakAdminClient ) : legacyHasPermission(req.legacyCredentials), keycloakGrant: req.kauth ? req.kauth.grant : null, @@ -131,10 +140,19 @@ const apolloServer = new ApolloServer({ models: { UserModel: User.User({ keycloakAdminClient }), GroupModel: Group.Group({ keycloakAdminClient, sqlClient }), - BillingModel: BillingModel.BillingModel({ keycloakAdminClient, sqlClient }), - ProjectModel: ProjectModel.ProjectModel({ keycloakAdminClient, sqlClient }), - EnvironmentModel: EnvironmentModel.EnvironmentModel({ keycloakAdminClient, sqlClient }) - }, + BillingModel: BillingModel.BillingModel({ + keycloakAdminClient, + sqlClient + }), + ProjectModel: ProjectModel.ProjectModel({ + keycloakAdminClient, + sqlClient + }), + EnvironmentModel: EnvironmentModel.EnvironmentModel({ + keycloakAdminClient, + sqlClient + }) + } }; } }, @@ -146,10 +164,11 @@ const apolloServer = new ApolloServer({ path: error.path, ...(process.env.NODE_ENV === 'development' ? { extensions: error.extensions } - : {}), + : {}) }; }, plugins: [ + // mariasql client closer plugin { requestDidStart: () => ({ willSendResponse: response => { @@ -159,10 +178,61 @@ const apolloServer = new ApolloServer({ if (response.context.requestCache) { response.context.requestCache.flushAll(); } - }, - }), + } + }) }, - ], + // newrelic instrumentation plugin. Based heavily on https://github.com/essaji/apollo-newrelic-extension-plus + { + requestDidStart({ request }) { + const operationName = R.prop('operationName', request); + const queryString = R.prop('query', request); + const variables = R.prop('variables', request); + + const queryObject = gql` + ${queryString} + `; + const rootFieldName = queryObject.definitions[0].selectionSet.selections.reduce( + (init, q, idx) => + idx === 0 ? `${q.name.value}` : `${init}, ${q.name.value}`, + '' + ); + + // operationName is set by the client and optional. rootFieldName is + // set by the API type defs. + // operationName would be "getHighCottonProjectId" and rootFieldName + // would be "getProjectByName" with a query like: + // query getHighCottonProjectId { getProjectByName(name: "high-cotton") { id } } + const transactionName = operationName ? operationName : rootFieldName; + newrelic.setTransactionName(`graphql (${transactionName})`); + newrelic.addCustomAttribute('gqlQuery', queryString); + newrelic.addCustomAttribute('gqlVars', JSON.stringify(variables)); + + return { + willSendResponse: data => { + const { response } = data; + const traceDuration = R.pathSatisfies( + R.is(Number), + ['extensions', 'tracing', 'duration'], + response + ) + ? `Total Duration (ms): ${R.path( + ['extensions', 'tracing', 'duration'], + response + ) / 1000000}` + : 'No trace data'; + newrelic.addCustomAttribute('totalDuration', traceDuration); + newrelic.addCustomAttribute( + 'errorCount', + R.pipe( + R.propOr([], 'errors'), + R.length + )(response) + ); + } + }; + } + } + ] }); module.exports = apolloServer; diff --git a/services/api/src/index.js b/services/api/src/index.js index 4ac227a914..0adf55220b 100644 --- a/services/api/src/index.js +++ b/services/api/src/index.js @@ -1,3 +1,4 @@ +require('newrelic'); const { initSendToLagoonLogs } = require('@lagoon/commons/dist/logs'); const { initSendToLagoonTasks } = require('@lagoon/commons/dist/tasks'); const waitForKeycloak = require('./util/waitForKeycloak'); diff --git a/services/api/src/newrelic.js b/services/api/src/newrelic.js new file mode 100644 index 0000000000..f88079b178 --- /dev/null +++ b/services/api/src/newrelic.js @@ -0,0 +1,49 @@ +'use strict' +/** + * New Relic agent configuration. + * + * See lib/config/default.js in the agent distribution for a more complete + * description of configuration variables and their potential values. + */ +exports.config = { + /** + * Array of application names. + */ + app_name: ['api'], + logging: { + /** + * Level at which to log. 'trace' is most useful to New Relic when diagnosing + * issues with the agent, 'info' and higher will impose the least overhead on + * production applications. + */ + level: 'info' + }, + /** + * When true, all request headers except for those listed in attributes.exclude + * will be captured for all traces, unless otherwise specified in a destination's + * attributes include/exclude lists. + */ + allow_all_headers: true, + attributes: { + /** + * Prefix of attributes to exclude from all destinations. Allows * as wildcard + * at end. + * + * NOTE: If excluding headers, they must be in camelCase form to be filtered. + * + * @env NEW_RELIC_ATTRIBUTES_EXCLUDE + */ + exclude: [ + 'request.headers.cookie', + 'request.headers.authorization', + 'request.headers.proxyAuthorization', + 'request.headers.setCookie*', + 'request.headers.x*', + 'response.headers.cookie', + 'response.headers.authorization', + 'response.headers.proxyAuthorization', + 'response.headers.setCookie*', + 'response.headers.x*' + ] + } +} diff --git a/yarn.lock b/yarn.lock index 000e24402a..d0dab1bec2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1773,6 +1773,21 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" integrity sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA== +"@grpc/grpc-js@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.0.3.tgz#7fa2ba293ccc1e91b24074c2628c8c68336e18c4" + integrity sha512-JKV3f5Bv2TZxK6eJSB9EarsZrnLxrvcFNwI9goq0YRXa3S6NNoCSnI3cG3lkXVIJ03Wng1WXe76kc2JQtRe7AQ== + dependencies: + semver "^6.2.0" + +"@grpc/proto-loader@^0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.4.tgz#038a3820540f621eeb1b05d81fbedfb045e14de0" + integrity sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA== + dependencies: + lodash.camelcase "^4.3.0" + protobufjs "^6.8.6" + "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -2016,6 +2031,33 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@newrelic/aws-sdk@^1.1.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@newrelic/aws-sdk/-/aws-sdk-1.1.3.tgz#e51adcf0cd3b5a527b96a86ac24b5adaeac1cb0c" + integrity sha512-8O//20g3WxpTWiUcY8EWodfSlQ9qre0smbvA8N1B9sw42DYDfuYq011No/7/yynMPL5taY7cOwKkTUfqzzslCA== + +"@newrelic/koa@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@newrelic/koa/-/koa-3.0.0.tgz#048f636c6c06ab4e823674a2ed3d67677a22ed50" + integrity sha512-SxfcMqSxiKa3pi7dRmVoCXnh/VLc196GmwyGU2Fr5+vMxS5jPVj2a15v1mn2DGu04XngfXDvyt9Xa6u1JVRDpQ== + dependencies: + methods "^1.1.2" + +"@newrelic/native-metrics@^5.1.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@newrelic/native-metrics/-/native-metrics-5.2.0.tgz#db5fb09bc50b73dabe2620629c24dc9fa903a5d5" + integrity sha512-vqqC3uwbiAMsmDkDecqm4Bcn6gwskl31SpYY6X/Zzuee+CcNifry5kcfD2iW7w4ENGDjn53dntvuRLQcxnQyKQ== + dependencies: + nan "^2.14.1" + semver "^5.5.1" + +"@newrelic/superagent@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@newrelic/superagent/-/superagent-2.0.1.tgz#8a0280598fedefd1b45fc83bdbc2707d129c47d5" + integrity sha512-1kOtaYh00DcK0IZ0LD3M6ja3urvm4a/waplr7TzrT/fDN/zgazpGSuRbYVg+O6zZacE4/Iw7OoKYGZW3bgBjJw== + dependencies: + methods "^1.1.2" + "@nodelib/fs.stat@^1.1.2": version "1.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" @@ -3217,6 +3259,11 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== +"@tyriar/fibonacci-heap@^2.0.7": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@tyriar/fibonacci-heap/-/fibonacci-heap-2.0.9.tgz#df3dcbdb1b9182168601f6318366157ee16666e9" + integrity sha512-bYuSNomfn4hu2tPiDN+JZtnzCpSpbJ/PNeulmocDy3xN2X5OkJL65zo6rPZp65cPPhLF9vfT/dgE+RtFRCSxOA== + "@webassemblyjs/ast@1.7.11": version "1.7.11" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace" @@ -3643,6 +3690,11 @@ address@1.1.2, address@^1.0.1: resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== +agent-base@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" + integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== + agentkeepalive@^3.4.1: version "3.5.2" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" @@ -5957,6 +6009,16 @@ concat-stream@^1.5.0, concat-stream@^1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + configstore@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" @@ -6721,7 +6783,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6. dependencies: ms "2.0.0" -debug@4.1.1, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@4.1.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -7417,6 +7479,18 @@ escodegen@^1.12.0, escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" +escodegen@^1.14.1: + version "1.14.2" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.2.tgz#14ab71bf5026c2aa08173afba22c6f3173284a84" + integrity sha512-InuOIiKk8wwuOFg6x9BQXbzjrQhtyXh46K9bqVTPzSo2FnyMBaYGBMC6PhQy7yxxil9vIedFBweQBMK74/7o8A== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-airbnb-base@^12.1.0: version "12.1.0" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz#386441e54a12ccd957b0a92564a4bafebd747944" @@ -7548,7 +7622,7 @@ esprima@^3.1.3: resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -9152,6 +9226,14 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +https-proxy-agent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" + integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== + dependencies: + agent-base "5" + debug "4" + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -10469,7 +10551,7 @@ json-stream@^1.0.0: resolved "https://registry.yarnpkg.com/json-stream/-/json-stream-1.0.0.tgz#1a3854e28d2bbeeab31cc7ddf683d2ddc5652708" integrity sha1-GjhU4o0rvuqzHMfd9oPS3cVlJwg= -json-stringify-safe@5.0.x, json-stringify-safe@~5.0.1: +json-stringify-safe@5.0.x, json-stringify-safe@^5.0.0, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= @@ -11367,7 +11449,7 @@ messageformat@^1.0.2: nopt "~3.0.6" reserved-words "^0.1.2" -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -11719,6 +11801,11 @@ nan@^2.0.9, nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== +nan@^2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" + integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== + nano@^6.4.3: version "6.4.4" resolved "https://registry.yarnpkg.com/nano/-/nano-6.4.4.tgz#4902a095e5186cfb23612c78826ea755b76fadf0" @@ -11797,6 +11884,28 @@ newman@^4.5.4: word-wrap "1.2.3" xmlbuilder "13.0.2" +newrelic@^6.9.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/newrelic/-/newrelic-6.9.0.tgz#b117a68cb2564dff917966984e6f3e073eb9712e" + integrity sha512-prgxTJ4sxS8bG87WbRMW9e7/E0TE9gEiMz/bjrLvo9Pgwo9bCoV7bzIFBaxnaLQReLvr5xWuNEQB+lHackSSSQ== + dependencies: + "@grpc/grpc-js" "1.0.3" + "@grpc/proto-loader" "^0.5.4" + "@newrelic/aws-sdk" "^1.1.1" + "@newrelic/koa" "^3.0.0" + "@newrelic/superagent" "^2.0.1" + "@tyriar/fibonacci-heap" "^2.0.7" + async "^2.1.4" + concat-stream "^2.0.0" + escodegen "^1.14.1" + esprima "^4.0.1" + https-proxy-agent "^4.0.0" + json-stringify-safe "^5.0.0" + readable-stream "^3.6.0" + semver "^5.3.0" + optionalDependencies: + "@newrelic/native-metrics" "^5.1.0" + next-server@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/next-server/-/next-server-8.1.0.tgz#50a9f248ede69fb33d83aa5274ec6c66f421556e" @@ -14328,6 +14437,15 @@ read-pkg@^3.0.0: isarray "0.0.1" string_decoder "~0.10.x" +readable-stream@^3.0.2, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^3.1.1: version "3.5.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" @@ -15146,7 +15264,17 @@ serialised-error@1.1.3: stack-trace "0.0.9" uuid "^3.0.0" -serialize-javascript@1.6.1, serialize-javascript@^1.7.0, serialize-javascript@^2.1.0, serialize-javascript@^2.1.1: +serialize-javascript@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" + integrity sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw== + +serialize-javascript@^1.7.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== + +serialize-javascript@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== From 36aa32b6bd272ba52456b6b611abebaa9715c89e Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Mon, 22 Jun 2020 18:21:56 +0200 Subject: [PATCH 154/280] Added some initial checks, support for rocketchat, and shellcheck --- helpers/check_acme_routes.sh | 122 +++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/helpers/check_acme_routes.sh b/helpers/check_acme_routes.sh index 1057f281d8..c37bca77eb 100755 --- a/helpers/check_acme_routes.sh +++ b/helpers/check_acme_routes.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Description: script to check routes with exposer pods. +# Description: script to check routes with exposer pods. # In case of no DNS record or mis-configuration, script will update the route # by disabling the tls-acme, removing other acme related annotations and add # an interal one for filtering purpose @@ -8,13 +8,12 @@ set -eu -o pipefail # Some variables - # Cluster full hostname and API hostname CLUSTER_HOSTNAME="${CLUSTER_HOSTNAME:-""}" -CLUSTER_API_HOSTNAME="${CLUSTER_API_HOSTNAME:-""}" +CLUSTER_API_HOSTNAME="${CLUSTER_API_HOSTNAME:-"$CLUSTER_HOSTNAME"}" -# Cluster array of IPs -CLUSTER_IPS=($(dig +short $CLUSTER_HOSTNAME)) +# Default command +COMMAND=${1:-"help"} # Set DRYRUN variable to true to run in dry-run mode DRYRUN="${DRYRUN:-"false"}" @@ -26,22 +25,38 @@ REGEX=${REGEX:-".*"} function usage() { echo -e "The available commands are: - help (get this help) - - getpendingroutes (get a list of routes with acme "orderStatus" in Pending - - getdisabledroutes (get a list of routes with "administratively-disabled" annotation + - getpendingroutes (get a list of routes with acme \"orderStatus\" in Pending + - getdisabledroutes (get a list of routes with \"administratively-disabled\" annotation - getbrokenroutes (get a list of all possible broken routes) - updateroutes (update broken routes) - By default, script runs routes' checks on ch.amazee.io towards API endpoint ch.amazeeio.cloud. + By default, script doesn't set any default cluster to run routes' checks. Please set CLUSTER_HOSTNAME and CLUSTER_API_HOSTNAME variables. If you want to change the API endpoint, set CLUSTER_API_HOSTNAME variable. If you want to change the cluster's hostname, set CLUSTER_HOSTNAME variable. If you want to filter the execution of the script only for certain projects, set the REGEX variable. If you want to test against a specific IP, set the CLUSTER_IPS array. Examples: - CLUSTER_HOSTNAME="ch.amazee.io" CLUSTER_API_HOSTNAME="ch.amazeeio.cloud" ./check_acme_routes.sh getpendingroutes (Returns a list of all routes witl TLS in Pending status for the defined cluster) - REGEX="drupal-example" ./check_acme_routes.sh getpendingroutes (Returns a list of all routes for all projects matchiing the regex \`drupal-example\` with TLS in Pending status) - REGEX="drupal-example-master" DRYRUN=true ./check_acme_routes.sh updateroutes (Will run in DRYRUN mode to check and update all broken routes in \`drupal-example-master\` project)" + CLUSTER_HOSTNAME=\"ch.amazee.io\" CLUSTER_API_HOSTNAME=\"ch.amazeeio.cloud\" ./check_acme_routes.sh getpendingroutes (Returns a list of all routes witl TLS in Pending status for the defined cluster) + REGEX=\"drupal-example\" ./check_acme_routes.sh getpendingroutes (Returns a list of all routes for all projects matchiing the regex \`drupal-example\` with TLS in Pending status) + REGEX=\"drupal-example-master\" DRYRUN=true ./check_acme_routes.sh updateroutes (Will run in DRYRUN mode to check and update all broken routes in \`drupal-example-master\` project)" + +} + +# Function that performs mandatory variales and dependencies checks +function initial_checks() { + # By default script doesn't set CLUSTER_HOSTNAME and CLUSTER_API_HOSTNAME. At least CLUSTER_HOSTNAME must be set + if [ -z "$CLUSTER_HOSTNAME" ]; then + echo "Please set CLUSTER_HOSTNAME variable" + usage + exit 1 + fi + # Script depends on `lagoon-cli`. Check if it in installed + if [[ ! $(command -v lagoon) ]]; then + echo "Please install \`lagoon-cli\` from https://github.com/amazeeio/lagoon-cli because the script relys on it" + exit 1 + fi } # function to get a list of all "administratively-disabled" routes @@ -51,12 +66,19 @@ function get_all_disabled_routes() { exit 0 } -# Function to check if you are running the script on the right cluster +# Function to check if you are running the script on the right cluster and if you're logged in correctly function check_cluster_api() { + # Check on which cluster you're going to run commands if oc whoami --show-server | grep -q -v "$CLUSTER_API_HOSTNAME"; then echo "Please connect to the right cluster" exit 1 fi + + # Check if you're logged in correctly + if [ $(oc status|grep -q "Unauthorized";echo $?) -eq 0 ]; then + echo "Please login into the cluster" + exit 1 + fi } # Function to get a list of all routes with acme.openshift.io/status.provisioningStatus.orderStatus=pending @@ -65,10 +87,10 @@ function get_pending_routes() { do IFS=$';' # For each route in a namespace with `tls-acme` set to true, check the `orderStatus` if in pending status - for routelist in $(oc get route -n $namespace -o=jsonpath="{range .items[?(@.metadata.annotations.kubernetes\.io/tls-acme=='true')]}{.metadata.name}{'\n'}{.metadata.annotations.acme\.openshift\.io/status}{';'}{end}"|sed "s/^[[:space:]]*//") + for routelist in $(oc get route -n "$namespace" -o=jsonpath="{range .items[?(@.metadata.annotations.kubernetes\.io/tls-acme=='true')]}{.metadata.name}{'\n'}{.metadata.annotations.acme\.openshift\.io/status}{';'}{end}"|sed "s/^[[:space:]]*//") do - PENDING_ROUTE_NAME=$(echo $routelist|sed -n 1p) - if echo $routelist|sed -n 4p | grep -q pending; then + PENDING_ROUTE_NAME=$(echo "$routelist"|sed -n 1p) + if echo "$routelist"|sed -n 4p | grep -q pending; then STATUS="Pending" echo "Route $PENDING_ROUTE_NAME in $namespace is in $STATUS status" fi @@ -83,26 +105,28 @@ function create_routes_array() { # Get the list of namespaces with broker routes, according to REGEX for namespace in $(oc get routes --all-namespaces|grep exposer|awk '{print $1}'|sort -u|grep -E "$REGEX") do - PROJECTNAME=$(oc get project $namespace -o=jsonpath="{.metadata.labels.lagoon\.sh/project}") - # Get the list of broken unique routes for each namespace - for routelist in $(oc get -n $namespace route|grep exposer|awk -vNAMESPACE="$namespace" -vPROJECTNAME="$PROJECTNAME" '{print $1";"$2";"NAMESPACE";"PROJECTNAME}'|sort -u -k2 -t ";") + PROJECTNAME=$(oc get project "$namespace" -o=jsonpath="{.metadata.labels.lagoon\.sh/project}") + # Get the list of broken unique routes for each namespace + for routelist in $(oc get -n "$namespace" route|grep exposer|awk -vNAMESPACE="$namespace" -vPROJECTNAME="$PROJECTNAME" '{print $1";"$2";"NAMESPACE";"PROJECTNAME}'|sort -u -k2 -t ";") do # Put the list into an array ROUTES_ARRAY+=("$routelist") done done - + # Create a sorted array of unique route to check ROUTES_ARRAY_SORTED=($(sort -u -k 2 -t ";"<<<"${ROUTES_ARRAY[*]}")) } # Function to check the routes, update them and delete the exposer's routes function check_routes() { -# set -x - for i in ${ROUTES_ARRAY_SORTED[@]} + + # Cluster array of IPs + CLUSTER_IPS=($(dig +short "$CLUSTER_HOSTNAME")) + for i in "${ROUTES_ARRAY_SORTED[@]}" do # Tranform the item into an array - route=($(echo $i | tr ";" "\n")) + route=($(echo "$i" | tr ";" "\n")) # Gather some useful variables ROUTE_NAME=${route[0]} @@ -111,10 +135,10 @@ function check_routes() { ROUTE_PROJECTNAME=${route[3]} # Get route DNS record(s) - ROUTE_HOSTNAME_IP=$(dig +short $ROUTE_HOSTNAME) + ROUTE_HOSTNAME_IP=$(dig +short "$ROUTE_HOSTNAME") # Check if the route matches the Cluster's IP(s) - if echo $ROUTE_HOSTNAME_IP | egrep -q -v "${CLUSTER_IPS[*]}"; then + if echo "$ROUTE_HOSTNAME_IP" | grep -E -q -v "${CLUSTER_IPS[*]}"; then # If IP is empty, then no DNS record set if [ -z "$ROUTE_HOSTNAME_IP" ]; then @@ -123,21 +147,21 @@ function check_routes() { DNS_ERROR="$ROUTE_HOSTNAME in $ROUTE_NAMESPACE has no DNS record poiting to ${CLUSTER_IPS[*]} and going to disable tls-acme" fi - echo $DNS_ERROR + echo "$DNS_ERROR" # Call the update function to update the route - update_annotation $ROUTE_HOSTNAME $ROUTE_NAMESPACE - notify_customer $ROUTE_PROJECTNAME + update_annotation "$ROUTE_HOSTNAME" "$ROUTE_NAMESPACE" + notify_customer "$ROUTE_PROJECTNAME" # Now once the main route is updated, it's time to get rid of exposers' routes - for j in $(oc get -n $ROUTE_NAMESPACE route|grep exposer|grep -E '(^|\s)'$ROUTE_HOSTNAME'($|\s)'|awk '{print $1";"$2}') + for j in $(oc get -n "$ROUTE_NAMESPACE" route|grep exposer|grep -E '(^|\s)'"$ROUTE_HOSTNAME"'($|\s)'|awk '{print $1";"$2}') #for j in $(oc get -n $ROUTE_NAMESPACE route|grep exposer|awk '{print $1";"$2}') do - ocroute=($(echo $j | tr ";" "\n")) + ocroute=($(echo "$j" | tr ";" "\n")) OCROUTE_NAME=${ocroute[0]} if [[ $DRYRUN = true ]]; then echo -e "DRYRUN oc delete -n $ROUTE_NAMESPACE route $OCROUTE_NAME" else - oc delete -n $ROUTE_NAMESPACE route $OCROUTE_NAME + oc delete -n "$ROUTE_NAMESPACE" route "$OCROUTE_NAME" fi done fi @@ -154,7 +178,7 @@ function update_annotation() { fi # Annotate the route - oc annotate -n $2 "$OCOPTIONS" --overwrite route $1 acme.openshift.io/status- kubernetes.io/tls-acme-awaiting-authorization-owner- kubernetes.io/tls-acme-awaiting-authorization-at-url- kubernetes.io/tls-acme="false" amazee.io/administratively-disabled="$(date +%s)" + oc annotate -n "$2" "$OCOPTIONS" --overwrite route "$1" acme.openshift.io/status- kubernetes.io/tls-acme-awaiting-authorization-owner- kubernetes.io/tls-acme-awaiting-authorization-at-url- kubernetes.io/tls-acme="false" amazee.io/administratively-disabled="$(date +%s)" } @@ -162,28 +186,29 @@ function update_annotation() { function notify_customer() { # Get Slack|Rocketchat channel and webhook - if [ $(TEST=$(lagoon list slack -p $1 --no-header|awk '{print $3";"$4}'); echo $?) -eq 0 ]; then + if [ $(TEST=$(lagoon list slack -p "$1" --no-header|awk '{print $3";"$4}'); echo $?) -eq 0 ]; then NOTIFICATION="slack" - elif [ $(TEST=$(lagoon list rocketchat -p $1 --no-header|awk '{print $3";"$4}'); echo $?) -eq 0 ]; then + elif [ $(TEST=$(lagoon list rocketchat -p "$1" --no-header|awk '{print $3";"$4}'); echo $?) -eq 0 ]; then NOTIFICATION="rocketchat" else echo "No notification set" return 0 fi - NOTIFICATION_DATA=$(lagoon list $NOTIFICATION -p $1 --no-header|head -n1|awk '{print $3";"$4}') - CHANNEL=$(echo $NOTIFICATION_DATA|cut -f1 -d ";") - WEBHOOK=$(echo $NOTIFICATION_DATA|cut -f2 -d ";") - MESSAGE="Your $ROUTE_HOSTNAME route is configured in the \`.lagoon.yml\` file to issue an TLS certificate from Let's Encrypt. Unfortunately Lagoon is unable to issue a certificate as $DNS_ERROR. \n - To be issued correctly, the DNS records for $ROUTE_HOSTNAME should point to $CLUSTER_HOSTNAME with an "CNAME" record (preferred) or to ${CLUSTER_IPS} via an "A" record (also possible but not preferred).\n - If you don't need the SSL certificate or you are using a CDN that provides you with an TLS certificate, please update your \`.lagoon.yml\` file by setting the \`tls-acme\` parameter to \`false\` for $ROUTE_HOSTNAME, as described here: https://lagoon.readthedocs.io/en/latest/using_lagoon/lagoon_yml/#ssl-configuration-tls-acme.\n - We have now administratively disabled the issuing of Let's Encrypt certificate for $ROUTE_HOSTNAME in order to protect the cluster, this will be reset during the next deployment, therefore we suggest to resolve this issue as soon as possible. Feel free to reach out to us for further information. Thanks you - amazee.io team" + NOTIFICATION_DATA=$(lagoon list $NOTIFICATION -p "$1" --no-header|head -n1|awk '{print $3";"$4}') + CHANNEL=$(echo "$NOTIFICATION_DATA"|cut -f1 -d ";") + WEBHOOK=$(echo "$NOTIFICATION_DATA"|cut -f2 -d ";") + MESSAGE="Your $ROUTE_HOSTNAME route is configured in the \`.lagoon.yml\` file to issue an TLS certificate from Lets Encrypt. Unfortunately Lagoon is unable to issue a certificate as $DNS_ERROR.\nTo be issued correctly, the DNS records for $ROUTE_HOSTNAME should point to $CLUSTER_HOSTNAME with an CNAME record (preferred) or to ${CLUSTER_IPS[*]} via an A record (also possible but not preferred).\nIf you don'\''t need the SSL certificate or you are using a CDN that provides you with an TLS certificate, please update your .lagoon.yml file by setting the tls-acme parameter to false for $ROUTE_HOSTNAME, as described here: https://lagoon.readthedocs.io/en/latest/using_lagoon/lagoon_yml/#ssl-configuration-tls-acme.\nWe have now administratively disabled the issuing of Lets Encrypt certificate for $ROUTE_HOSTNAME in order to protect the cluster, this will be reset during the next deployment, therefore we suggest to resolve this issue as soon as possible. Feel free to reach out to us for further information.\nThanks you.\namazee.io team" # JSON payload JSON="'{\"channel\": \"$CHANNEL\", \"text\":\"$MESSAGE\"}'" - echo -e "Sending message to $CHANNEL" + echo "Sending message $JSON to $CHANNEL" # Execute curl to send message into the channel - curl -X POST -H 'Content-type: application/json' --data "$JSON" $WEBHOOK + if [[ $DRYRUN = true ]]; then + echo "DRYRUN on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data "$JSON" "$WEBHOOK"" + else + curl -X POST -H 'Content-type: application/json' --data "$JSON" "$WEBHOOK" + fi } @@ -193,7 +218,7 @@ function main() { COMMAND="$1" # Check first the cluster you're connected to - echo "You're running the script on $CLUSTER_HOSTNAME" + echo -e "You're running the script on $CLUSTER_HOSTNAME\nDRYRUN mode is set to \"$DRYRUN\"" check_cluster_api case "$COMMAND" in @@ -209,16 +234,16 @@ function main() { getbrokenroutes) echo -e "\nCreating a list of possible broken routes" create_routes_array - echo -e "ROUTE_NAMESPACE;ROUTE_NAME;ROUTE_HOSTNAME"|column -t -s ";" - for i in ${ROUTES_ARRAY_SORTED[@]} + echo -e "ROUTE_NAMESPACE;ROUTE_NAME;ROUTE_HOSTNAME"|column -t -s ";" + for i in "${ROUTES_ARRAY_SORTED[@]}" do # Tranform the item into an array - route=($(echo $i | tr ";" "\n")) + route=($(echo "$i" | tr ";" "\n")) # Gather some useful variables ROUTE_NAME=${route[0]} ROUTE_HOSTNAME=${route[1]} ROUTE_NAMESPACE=${route[2]} - echo -e "$ROUTE_NAMESPACE;$ROUTE_NAME;$ROUTE_HOSTNAME"|column -t -s ";" + echo -e "$ROUTE_NAMESPACE;$ROUTE_NAME;$ROUTE_HOSTNAME"|column -t -s ";" done ;; updateroutes) @@ -232,4 +257,5 @@ function main() { esac } -main $1 +initial_checks "$COMMAND" +main "$COMMAND" \ No newline at end of file From 670efb4ec94fc1c83228590f224bdb0398a43803 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Mon, 22 Jun 2020 13:45:39 -0400 Subject: [PATCH 155/280] v1.7.0 #BlackLivesMatter --- docker-compose.yaml | 90 +++++++++++++++---------------- lagoon-remote/docker-compose.yaml | 20 +++---- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 00a598c87c..7642ea38db 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,7 @@ services: labels: lagoon.type: custom lagoon.template: services/api-db/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api-db:v1-6-0 + lagoon.image: amazeeiolagoon/api-db:v1-7-0 webhook-handler: image: ${IMAGE_REPO:-lagoon}/webhook-handler command: yarn run dev @@ -22,7 +22,7 @@ services: labels: lagoon.type: custom lagoon.template: services/webhook-handler/.lagoon.app.yml - lagoon.image: amazeeiolagoon/webhook-handler:v1-6-0 + lagoon.image: amazeeiolagoon/webhook-handler:v1-7-0 backup-handler: image: ${IMAGE_REPO:-lagoon}/backup-handler restart: on-failure @@ -31,7 +31,7 @@ services: labels: lagoon.type: custom lagoon.template: services/backup-handler/.lagoon.app.yml - lagoon.image: amazeeiolagoon/backup-handler:v1-6-0 + lagoon.image: amazeeiolagoon/backup-handler:v1-7-0 depends_on: - broker broker: @@ -42,7 +42,7 @@ services: labels: lagoon.type: rabbitmq-cluster lagoon.template: services/broker/.lagoon.app.yml - lagoon.image: amazeeiolagoon/broker:v1-6-0 + lagoon.image: amazeeiolagoon/broker:v1-7-0 openshiftremove: image: ${IMAGE_REPO:-lagoon}/openshiftremove command: yarn run dev @@ -52,7 +52,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftremove/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftremove:v1-6-0 + lagoon.image: amazeeiolagoon/openshiftremove:v1-7-0 openshiftbuilddeploy: image: ${IMAGE_REPO:-lagoon}/openshiftbuilddeploy command: yarn run dev @@ -64,7 +64,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftbuilddeploy/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftbuilddeploy:v1-6-0 + lagoon.image: amazeeiolagoon/openshiftbuilddeploy:v1-7-0 openshiftbuilddeploymonitor: image: ${IMAGE_REPO:-lagoon}/openshiftbuilddeploymonitor command: yarn run dev @@ -78,7 +78,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftbuilddeploymonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftbuilddeploymonitor:v1-6-0 + lagoon.image: amazeeiolagoon/openshiftbuilddeploymonitor:v1-7-0 openshiftjobs: image: ${IMAGE_REPO:-lagoon}/openshiftjobs command: yarn run dev @@ -92,7 +92,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftjobs/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftjobs:v1-6-0 + lagoon.image: amazeeiolagoon/openshiftjobs:v1-7-0 openshiftjobsmonitor: image: ${IMAGE_REPO:-lagoon}/openshiftjobsmonitor command: yarn run dev @@ -102,7 +102,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftjobsmonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftjobsmonitor:v1-6-0 + lagoon.image: amazeeiolagoon/openshiftjobsmonitor:v1-7-0 openshiftmisc: image: ${IMAGE_REPO:-lagoon}/openshiftmisc command: yarn run dev @@ -112,7 +112,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftmisc/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftmisc:v1-6-0 + lagoon.image: amazeeiolagoon/openshiftmisc:v1-7-0 kubernetesmisc: image: ${IMAGE_REPO:-lagoon}/kubernetesmisc command: yarn run dev @@ -122,7 +122,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesmisc/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesmisc:v1-6-0 + lagoon.image: amazeeiolagoon/kubernetesmisc:v1-7-0 kubernetesbuilddeploy: image: ${IMAGE_REPO:-lagoon}/kubernetesbuilddeploy command: yarn run dev @@ -135,7 +135,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesbuilddeploy/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesbuilddeploy:v1-6-0 + lagoon.image: amazeeiolagoon/kubernetesbuilddeploy:v1-7-0 kubernetesdeployqueue: image: ${IMAGE_REPO:-lagoon}/kubernetesdeployqueue command: yarn run dev @@ -145,7 +145,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesdeployqueue/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesdeployqueue:v1-6-0 + lagoon.image: amazeeiolagoon/kubernetesdeployqueue:v1-7-0 kubernetesbuilddeploymonitor: image: ${IMAGE_REPO:-lagoon}/kubernetesbuilddeploymonitor command: yarn run dev @@ -159,7 +159,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesbuilddeploymonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesbuilddeploymonitor:v1-6-0 + lagoon.image: amazeeiolagoon/kubernetesbuilddeploymonitor:v1-7-0 kubernetesjobs: image: ${IMAGE_REPO:-lagoon}/kubernetesjobs command: yarn run dev @@ -173,7 +173,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesjobs/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesjobs:v1-6-0 + lagoon.image: amazeeiolagoon/kubernetesjobs:v1-7-0 kubernetesjobsmonitor: image: ${IMAGE_REPO:-lagoon}/kubernetesjobsmonitor command: yarn run dev @@ -187,7 +187,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesjobsmonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesjobsmonitor:v1-6-0 + lagoon.image: amazeeiolagoon/kubernetesjobsmonitor:v1-7-0 kubernetesremove: image: ${IMAGE_REPO:-lagoon}/kubernetesremove command: yarn run dev @@ -197,7 +197,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesremove/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesremove:v1-6-0 + lagoon.image: amazeeiolagoon/kubernetesremove:v1-7-0 logs2rocketchat: image: ${IMAGE_REPO:-lagoon}/logs2rocketchat command: yarn run dev @@ -207,7 +207,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2rocketchat/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2rocketchat:v1-6-0 + lagoon.image: amazeeiolagoon/logs2rocketchat:v1-7-0 logs2slack: image: ${IMAGE_REPO:-lagoon}/logs2slack command: yarn run dev @@ -217,7 +217,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2slack/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2slack:v1-6-0 + lagoon.image: amazeeiolagoon/logs2slack:v1-7-0 logs2microsoftteams: image: ${IMAGE_REPO:-lagoon}/logs2microsoftteams command: yarn run dev @@ -227,7 +227,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2microsoftteams/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2microsoftteams:v1-6-0 + lagoon.image: amazeeiolagoon/logs2microsoftteams:v1-7-0 logs2email: image: ${IMAGE_REPO:-lagoon}/logs2email command: yarn run dev @@ -237,7 +237,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2slack/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2email:v1-6-0 + lagoon.image: amazeeiolagoon/logs2email:v1-7-0 depends_on: - mailhog mailhog: @@ -255,7 +255,7 @@ services: labels: lagoon.type: custom lagoon.template: services/webhooks2tasks/.lagoon.app.yml - lagoon.image: amazeeiolagoon/webhooks2tasks:v1-6-0 + lagoon.image: amazeeiolagoon/webhooks2tasks:v1-7-0 api: image: ${IMAGE_REPO:-lagoon}/api command: yarn run dev @@ -274,7 +274,7 @@ services: labels: lagoon.type: custom lagoon.template: services/api/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api:v1-6-0 + lagoon.image: amazeeiolagoon/api:v1-7-0 ui: image: ${IMAGE_REPO:-lagoon}/ui command: yarn run dev @@ -288,7 +288,7 @@ services: labels: lagoon.type: custom lagoon.template: services/ui/.lagoon.app.yml - lagoon.image: amazeeiolagoon/ui:v1-6-0 + lagoon.image: amazeeiolagoon/ui:v1-7-0 ssh: image: ${IMAGE_REPO:-lagoon}/ssh depends_on: @@ -309,7 +309,7 @@ services: labels: lagoon.type: custom lagoon.template: services/ssh/.lagoon.app.yml - lagoon.image: amazeeiolagoon/ssh:v1-6-0 + lagoon.image: amazeeiolagoon/ssh:v1-7-0 auth-server: image: ${IMAGE_REPO:-lagoon}/auth-server command: yarn run dev @@ -323,7 +323,7 @@ services: labels: lagoon.type: custom lagoon.template: services/auth-server/.lagoon.app.yml - lagoon.image: amazeeiolagoon/auth-server:v1-6-0 + lagoon.image: amazeeiolagoon/auth-server:v1-7-0 keycloak: image: ${IMAGE_REPO:-lagoon}/keycloak user: '111111111' @@ -334,7 +334,7 @@ services: labels: lagoon.type: custom lagoon.template: services/keycloak/.lagoon.app.yml - lagoon.image: amazeeiolagoon/keycloak:v1-6-0 + lagoon.image: amazeeiolagoon/keycloak:v1-7-0 keycloak-db: image: ${IMAGE_REPO:-lagoon}/keycloak-db ports: @@ -342,7 +342,7 @@ services: labels: lagoon.type: custom lagoon.template: services/keycloak-db/.lagoon.app.yml - lagoon.image: amazeeiolagoon/keycloak-db:v1-6-0 + lagoon.image: amazeeiolagoon/keycloak-db:v1-7-0 tests-kubernetes: image: ${IMAGE_REPO:-lagoon}/tests environment: @@ -458,7 +458,7 @@ services: labels: lagoon.type: custom lagoon.template: services/drush-alias/.lagoon.app.yml - lagoon.image: amazeeiolagoon/drush-alias:v1-6-0 + lagoon.image: amazeeiolagoon/drush-alias:v1-7-0 version: '2' logs-db: image: ${IMAGE_REPO:-lagoon}/logs-db @@ -474,14 +474,14 @@ services: labels: lagoon.type: elasticsearch lagoon.template: services/logs-db/.lagoon.single.yml - lagoon.image: amazeeiolagoon/logs-db:v1-6-0 + lagoon.image: amazeeiolagoon/logs-db:v1-7-0 logs-forwarder: image: ${IMAGE_REPO:-lagoon}/logs-forwarder user: '111111111' labels: lagoon.type: custom lagoon.template: services/logs-forwarder/.lagoon.single.yml - lagoon.image: amazeeiolagoon/logs-forwarder:v1-6-0 + lagoon.image: amazeeiolagoon/logs-forwarder:v1-7-0 logs-db-ui: image: ${IMAGE_REPO:-lagoon}/logs-db-ui user: '111111111' @@ -493,14 +493,14 @@ services: labels: lagoon.type: kibana lagoon.template: services/logs-db-ui/.lagoon.yml - lagoon.image: amazeeiolagoon/logs-db-ui:v1-6-0 + lagoon.image: amazeeiolagoon/logs-db-ui:v1-7-0 logs-db-curator: image: ${IMAGE_REPO:-lagoon}/logs-db-curator user: '111111111' labels: lagoon.type: cli lagoon.template: services/logs-db-curator/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs-db-curator:v1-6-0 + lagoon.image: amazeeiolagoon/logs-db-curator:v1-7-0 logs2logs-db: image: ${IMAGE_REPO:-lagoon}/logs2logs-db user: '111111111' @@ -516,7 +516,7 @@ services: labels: lagoon.type: logstash lagoon.template: services/logs2logs-db/.lagoon.yml - lagoon.image: amazeeiolagoon/logs2logs-db:v1-6-0 + lagoon.image: amazeeiolagoon/logs2logs-db:v1-7-0 auto-idler: image: ${IMAGE_REPO:-lagoon}/auto-idler user: '111111111' @@ -529,7 +529,7 @@ services: labels: lagoon.type: custom lagoon.template: services/auto-idler/.lagoon.yml - lagoon.image: amazeeiolagoon/auto-idler:v1-6-0 + lagoon.image: amazeeiolagoon/auto-idler:v1-7-0 storage-calculator: image: ${IMAGE_REPO:-lagoon}/storage-calculator user: '111111111' @@ -538,7 +538,7 @@ services: labels: lagoon.type: custom lagoon.template: services/storage-calculator/.lagoon.yml - lagoon.image: amazeeiolagoon/storage-calculator:v1-6-0 + lagoon.image: amazeeiolagoon/storage-calculator:v1-7-0 logs-collector: image: openshift/origin-logging-fluentd:v3.6.1 labels: @@ -610,7 +610,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-core/harbor-core.yml - lagoon.image: amazeeiolagoon/harbor-core:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-core:v1-7-0 harbor-database: image: ${IMAGE_REPO:-lagoon}/harbor-database hostname: harbor-database @@ -624,7 +624,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-database/harbor-database.yml - lagoon.image: amazeeiolagoon/harbor-database:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-database:v1-7-0 harbor-jobservice: image: ${IMAGE_REPO:-lagoon}/harbor-jobservice hostname: harbor-jobservice @@ -653,7 +653,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-jobservice/harbor-jobservice.yml - lagoon.image: amazeeiolagoon/harbor-jobservice:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-jobservice:v1-7-0 harbor-nginx: image: ${IMAGE_REPO:-lagoon}/harbor-nginx hostname: harbor-nginx @@ -669,7 +669,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-nginx/harbor-nginx.yml - lagoon.image: amazeeiolagoon/harbor-nginx:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-nginx:v1-7-0 harbor-portal: image: ${IMAGE_REPO:-lagoon}/harbor-portal hostname: harbor-portal @@ -679,7 +679,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-portal/harbor-portal.yml - lagoon.image: amazeeiolagoon/harbor-portal:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-portal:v1-7-0 harbor-redis: image: ${IMAGE_REPO:-lagoon}/harbor-redis hostname: harbor-redis @@ -689,7 +689,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-redis/harbor-redis.yml - lagoon.image: amazeeiolagoon/harbor-redis:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-redis:v1-7-0 harbor-trivy: image: ${IMAGE_REPO:-lagoon}/harbor-trivy hostname: harbor-trivy @@ -721,7 +721,7 @@ services: lagoon.type: custom lagoon.template: services/harbor-trivy/harbor-trivy.yml lagoon.name: harbor-trivy - lagoon.image: amazeeiolagoon/harbor-trivy:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-trivy:v1-7-0 harborregistry: image: ${IMAGE_REPO:-lagoon}/harborregistry hostname: harborregistry @@ -743,7 +743,7 @@ services: lagoon.type: custom lagoon.template: services/harborregistry/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistry:v1-6-0 + lagoon.image: amazeeiolagoon/harborregistry:v1-7-0 harborregistryctl: image: ${IMAGE_REPO:-lagoon}/harborregistryctl hostname: harborregistryctl @@ -758,4 +758,4 @@ services: lagoon.type: custom lagoon.template: services/harborregistryctl/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistryctl:v1-6-0 + lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-0 diff --git a/lagoon-remote/docker-compose.yaml b/lagoon-remote/docker-compose.yaml index 32008d281e..377ec52859 100644 --- a/lagoon-remote/docker-compose.yaml +++ b/lagoon-remote/docker-compose.yaml @@ -35,61 +35,61 @@ services: lagoon.type: custom lagoon.template: harborclair/harborclair.yml lagoon.name: harborclair - lagoon.image: amazeeiolagoon/harborclair:v1-6-0 + lagoon.image: amazeeiolagoon/harborclair:v1-7-0 harborclairadapter: image: ${IMAGE_REPO:-lagoon}/harborclairadapter labels: lagoon.type: custom lagoon.template: harborclairadapter/harborclair.yml lagoon.name: harborclair - lagoon.image: amazeeiolagoon/harborclairadapter:v1-6-0 + lagoon.image: amazeeiolagoon/harborclairadapter:v1-7-0 harbor-core: image: ${IMAGE_REPO:-lagoon}/harbor-core labels: lagoon.type: custom lagoon.template: harbor-core/harbor-core.yml - lagoon.image: amazeeiolagoon/harbor-core:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-core:v1-7-0 harbor-database: image: ${IMAGE_REPO:-lagoon}/harbor-database labels: lagoon.type: custom lagoon.template: harbor-database/harbor-database.yml - lagoon.image: amazeeiolagoon/harbor-database:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-database:v1-7-0 harbor-jobservice: image: ${IMAGE_REPO:-lagoon}/harbor-jobservice labels: lagoon.type: custom lagoon.template: harbor-jobservice/harbor-jobservice.yml - lagoon.image: amazeeiolagoon/harbor-jobservice:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-jobservice:v1-7-0 harbor-nginx: image: ${IMAGE_REPO:-lagoon}/harbor-nginx labels: lagoon.type: custom lagoon.template: harbor-nginx/harbor-nginx.yml - lagoon.image: amazeeiolagoon/harbor-nginx:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-nginx:v1-7-0 harbor-portal: image: ${IMAGE_REPO:-lagoon}/harbor-portal labels: lagoon.type: custom lagoon.template: harbor-portal/harbor-portal.yml - lagoon.image: amazeeiolagoon/harbor-portal:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-portal:v1-7-0 harbor-redis: image: ${IMAGE_REPO:-lagoon}/harbor-redis labels: lagoon.type: custom lagoon.template: harbor-redis/harbor-redis.yml - lagoon.image: amazeeiolagoon/harbor-redis:v1-6-0 + lagoon.image: amazeeiolagoon/harbor-redis:v1-7-0 harborregistry: image: ${IMAGE_REPO:-lagoon}/harborregistry labels: lagoon.type: custom lagoon.template: harborregistry/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistry:v1-6-0 + lagoon.image: amazeeiolagoon/harborregistry:v1-7-0 harborregistryctl: image: ${IMAGE_REPO:-lagoon}/harborregistryctl labels: lagoon.type: custom lagoon.template: harborregistryctl/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistryctl:v1-6-0 + lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-0 From b5e93c45990bee1d02716379743b41645fc93b90 Mon Sep 17 00:00:00 2001 From: Tim Clifford Date: Mon, 22 Jun 2020 18:52:25 +0100 Subject: [PATCH 156/280] Problems section per project, problems dashboards and webhooks integration --- node-packages/commons/src/api.ts | 128 +++-- services/api/src/data/mock-data.js | 50 ++ services/api/src/mocks.js | 110 +---- services/api/src/resolvers.js | 7 + .../src/resources/environment/resolvers.ts | 19 + services/api/src/resources/problem/helpers.ts | 61 +++ .../api/src/resources/problem/resolvers.ts | 71 ++- services/api/src/resources/problem/sql.ts | 51 +- services/api/src/resources/project/helpers.ts | 19 +- .../api/src/resources/project/resolvers.ts | 4 + services/api/src/resources/project/sql.ts | 19 +- services/api/src/typeDefs.js | 16 +- services/ui/.gitignore | 1 + .../ui/.storybook/decorators/ApiConnection.js | 2 +- services/ui/.storybook/presets.js | 1 + services/ui/.storybook/webpack.config.js | 13 +- services/ui/package.json | 7 +- services/ui/server.js | 24 +- services/ui/src/components/Accordion/index.js | 89 ++++ .../src/components/Accordion/index.stories.js | 12 + .../components/ActiveStandbyConfirm/index.js | 4 +- services/ui/src/components/Filters/helpers.js | 33 ++ services/ui/src/components/Filters/index.js | 70 +++ services/ui/src/components/NavTabs/index.js | 2 +- services/ui/src/components/Problems/index.js | 372 +++++++-------- .../src/components/Problems/index.stories.js | 4 - .../ui/src/components/Problems/sortedItems.js | 11 +- .../components/ProblemsByIdentifier/index.js | 327 +++++++++++++ .../ProblemsByIdentifier/index.stories.js | 25 + .../ProblemsByIdentifier/sortedItems.js | 70 +++ .../ProblemsByProject/Honeycomb/index.js | 190 ++++++++ .../Honeycomb/index.stories.js | 26 + .../ProblemsByProject/Honeycomb/styling.css | 52 ++ .../src/components/ProblemsByProject/index.js | 306 ++++++++++++ .../ProblemsByProject/sortedItems.js | 63 +++ .../ui/src/components/errors/QueryError.js | 10 +- services/ui/src/components/link/Problems.js | 2 +- services/ui/src/lib/fragment/Problem.js | 4 + services/ui/src/lib/query/AllProblems.js | 33 ++ .../ui/src/lib/query/AllProblemsByProject.js | 21 + .../lib/query/AllProjectsAndEnvironments.js | 14 + .../ui/src/lib/query/AllProjectsProblems.js | 19 + .../src/lib/query/ProjectByEnvironmentId.js | 17 + services/ui/src/lib/withQueryErrorNoHeader.js | 7 + .../ui/src/lib/withQueryLoadingNoHeader.js | 7 + services/ui/src/next.config.js | 2 +- services/ui/src/pages/_error.js | 10 +- .../problems-dashboard-by-project-hex.js | 169 +++++++ .../pages/problems-dashboard-by-project.js | 265 +++++++++++ services/ui/src/pages/problems-dashboard.js | 190 ++++++++ services/ui/src/webpack.shared-config.js | 1 + services/ui/tsconfig.json | 31 ++ .../processHarborVulnerabilityList.ts | 2 +- .../webhooks2tasks/src/webhooks/problems.ts | 5 +- yarn.lock | 448 +++++++++++++----- 55 files changed, 2951 insertions(+), 565 deletions(-) create mode 100644 services/api/src/data/mock-data.js create mode 100644 services/api/src/resources/problem/helpers.ts create mode 100644 services/ui/src/components/Accordion/index.js create mode 100644 services/ui/src/components/Accordion/index.stories.js create mode 100644 services/ui/src/components/Filters/helpers.js create mode 100644 services/ui/src/components/Filters/index.js create mode 100644 services/ui/src/components/ProblemsByIdentifier/index.js create mode 100644 services/ui/src/components/ProblemsByIdentifier/index.stories.js create mode 100644 services/ui/src/components/ProblemsByIdentifier/sortedItems.js create mode 100644 services/ui/src/components/ProblemsByProject/Honeycomb/index.js create mode 100644 services/ui/src/components/ProblemsByProject/Honeycomb/index.stories.js create mode 100644 services/ui/src/components/ProblemsByProject/Honeycomb/styling.css create mode 100644 services/ui/src/components/ProblemsByProject/index.js create mode 100644 services/ui/src/components/ProblemsByProject/sortedItems.js create mode 100644 services/ui/src/lib/query/AllProblems.js create mode 100644 services/ui/src/lib/query/AllProblemsByProject.js create mode 100644 services/ui/src/lib/query/AllProjectsAndEnvironments.js create mode 100644 services/ui/src/lib/query/AllProjectsProblems.js create mode 100644 services/ui/src/lib/query/ProjectByEnvironmentId.js create mode 100644 services/ui/src/lib/withQueryErrorNoHeader.js create mode 100644 services/ui/src/lib/withQueryLoadingNoHeader.js create mode 100644 services/ui/src/pages/problems-dashboard-by-project-hex.js create mode 100644 services/ui/src/pages/problems-dashboard-by-project.js create mode 100644 services/ui/src/pages/problems-dashboard.js create mode 100644 services/ui/tsconfig.json diff --git a/node-packages/commons/src/api.ts b/node-packages/commons/src/api.ts index 494913d359..1b615ac19a 100644 --- a/node-packages/commons/src/api.ts +++ b/node-packages/commons/src/api.ts @@ -1308,69 +1308,66 @@ export const addProblem = ({ links }) => { return graphqlapi.mutate( - ` - ($id: Int, - $environment: Int!, - $identifier: String!, - $severity: ProblemSeverityRating!, - $source: String!, - $severityScore: SeverityScore, - $data: String!, - $service: String, - $associatedPackage: String, - $description: String, - $version: String, - $fixedVersion: String, - $links: String) { - addProblem(input: { - id: $id - environment: $environment - identifier: $identifier - severity: $severity - source: $source - severityScore: $severityScore - data: $data - service: $service - associatedPackage: $associatedPackage - description: $description - version: $version - fixedVersion: $fixedVersion - links: $links - }) { + `($id: Int, + $environment: Int!, + $identifier: String!, + $severity: ProblemSeverityRating!, + $source: String!, + $severityScore: SeverityScore, + $data: String!, + $service: String, + $associatedPackage: String, + $description: String, + $version: String, + $fixedVersion: String, + $links: String) { + addProblem(input: { + id: $id + environment: $environment + identifier: $identifier + severity: $severity + source: $source + severityScore: $severityScore + data: $data + service: $service + associatedPackage: $associatedPackage + description: $description + version: $version + fixedVersion: $fixedVersion + links: $links + }) { + id + environment { id - environment { - id - } - identifier - severity - source - severityScore - data - associatedPackage - description - version - fixedVersion - links } - } - `, - { - id, - environment, - identifier, - severity, - source, - severityScore, - data, - service, - associatedPackage, - description, - version, - fixedVersion, + identifier + severity + source + severityScore + data + associatedPackage + description + version + fixedVersion links - }, - ); -} + } + }`, + { + id, + environment, + identifier, + severity, + source, + severityScore, + data, + service, + associatedPackage, + description, + version, + fixedVersion, + links + }, +)}; export const deleteProblemsFromSource = ( environment, @@ -1387,9 +1384,7 @@ export const deleteProblemsFromSource = ( source, service } - ); -} - + )}; const problemFragment = graphqlapi.createFragment(` fragment on Problem { @@ -1407,7 +1402,7 @@ fragment on Problem { data created deleted -} +} `); export const getProblemsforProjectEnvironment = async ( @@ -1430,7 +1425,7 @@ export const getProblemsforProjectEnvironment = async ( project }); return response.environmentByName.problems; -} +}; export const getProblemHarborScanMatches = () => graphqlapi.query( `query getProblemHarborScanMatches { @@ -1443,4 +1438,5 @@ export const getProblemHarborScanMatches = () => graphqlapi.query( defaultLagoonService regex } - }`); + }` +); \ No newline at end of file diff --git a/services/api/src/data/mock-data.js b/services/api/src/data/mock-data.js new file mode 100644 index 0000000000..e61a42625a --- /dev/null +++ b/services/api/src/data/mock-data.js @@ -0,0 +1,50 @@ +export const packages = [ + 'ansible', + 'apache-log', + 'awl', + 'cacti', + 'chromium', + 'commons-configuration2', + 'consul', + 'dom4j', + 'drupal', + 'file-roller', + 'glibc', + 'golang-go.crypto', + 'graphicsmagick', + 'http-parser', + 'imagemagick', + 'jruby', + 'ksh', + 'libmicrodns', + 'libxml-security-java', + 'linux', + 'lucene-solr', + 'lxc-templates', + 'matrix-synapse', + 'mbedtls', + 'netty', + 'nginx', + 'node-yarnpkg', + 'nodejs', + 'nss', + 'openjdk-11', + 'phantomjs', + 'php7.1', + 'php7.3', + 'python', + 'rmysql', + 'ruby-json-jwt', + 'ruby-omniauth', + 'salt', + 'shiro', + 'slirp', + 'squid', + 'ssvnc', + 'thrift', + 'tomcat9', + 'trafficserver', + 'varnish', + 'xerces-c', + 'yubikey-val', +]; \ No newline at end of file diff --git a/services/api/src/mocks.js b/services/api/src/mocks.js index f416f4e48c..8a16c8d496 100644 --- a/services/api/src/mocks.js +++ b/services/api/src/mocks.js @@ -1,5 +1,6 @@ import { MockList } from 'graphql-tools'; import faker from 'faker/locale/en'; +import { packages } from './data/mock-data'; // The mocks object is an Apollo Resolver Map where each mock function has the // following definition: (parent, args, context, info) => {} @@ -409,116 +410,42 @@ mocks.Task = (parent, args = {}, context, info) => { }; }; +mocks.ProblemIdentifier = () => { + const recentYear = faker.random.arrayElement(['2019', '2020']); + const vuln_id = `CVE-${recentYear}-${faker.random.number({min: 1000, max: 99999})}`; + + return { + identifier: vuln_id, + problem: mocks.Problem(), + }; +}; + mocks.Problem = () => { - const packages = [ - 'ansible', - 'apache-log4j1.2', - 'awl', - 'cacti', - 'ceph', - 'chromium', - 'commons-configuration2', - 'consul', - 'dom4j', - 'drupal', - 'file-roller', - 'freeipa', - 'freeradius', - 'glibc', - 'golang-github-buger-jsonparser', - 'golang-github-opencontainers-selinux', - 'golang-github-proglottis-gpgme', - 'golang-go.crypto', - 'golang-golang-x-net-dev', - 'graphicsmagick', - 'http-parser', - 'imagemagick', - 'inetutils', - 'ipmitool', - 'janus', - 'jruby', - 'knot-resolver', - 'ksh', - 'libapache2-mod-auth-openidc', - 'libjackson-json-java', - 'libmicrodns', - 'libopenmpt', - 'libpam-radius-auth', - 'libperlspeak-perl', - 'librsvg', - 'libspring-java', - 'libusrsctp', - 'libvirt', - 'libxml-security-java', - 'linux', - 'lucene-solr', - 'lxc-templates', - 'matrix-synapse', - 'mbedtls', - 'mupdf', - 'netty', - 'nginx', - 'node-yarnpkg', - 'nodejs', - 'nss', - 'openjdk-11', - 'phantomjs', - 'php7.0', - 'php7.1', - 'php7.2', - 'php7.3', - 'puma', - 'python', - 'rmysql', - 'ruby-json-jwt', - 'ruby-omniauth', - 'salt', - 'shiro', - 'slirp', - 'squid', - 'ssvnc', - 'thrift', - 'tomcat9', - 'trafficserver', - 'varnish', - 'xcftools', - 'xerces-c', - 'yubikey-val', - ]; const recentYear = faker.random.arrayElement(['2019', '2020']); const vuln_id = `CVE-${recentYear}-${faker.random.number({min: 1000, max: 99999})}`; - const source = faker.random.arrayElement(['Clair', 'Drutiny']); + const source = faker.random.arrayElement(['Harbor', 'Drutiny']); const created = faker.date.between('2019-10-01 00:00:00', '2020-03-31 23:59:59').toUTCString(); const associatedPackage = faker.random.arrayElement(packages); const version = `${faker.random.number(4)}.${faker.random.number(9)}.${faker.random.number(49)}`; const fixedVersion = `${version}+deb8u${faker.random.number(9)}`; - const severity = faker.random.arrayElement(['Unknown', 'Negligible', 'Low', 'Medium', 'High', 'Critical']); + const severity = faker.random.arrayElement(['UNKNOWN', 'NEGLIGIBLE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL']); const description = faker.lorem.paragraph(); const links = `https://security-tracker.debian.org/tracker/${vuln_id}`; - const deleted = '0000-00-00 00:00:00'; const severityScore = `0.${faker.random.number({min:1, max:9})}`; - const data = "{hello: 'world'}"; + const data = ({ id: faker.random.number(), hello: 'hello', world: 'world' }); return { - id: `${faker.random.number(9999999)}`, identifier: vuln_id, + severity: severity, source: source, - status: mocks.TaskStatusType(), - associatedPackage, - version, - fixedVersion, - severity, + severityScore: severityScore, + associatedPackage: associatedPackage, description, links, - created, - deleted, - severityScore, data }; }; -mocks.Problems = () => generator(mocks.Problem, 1, 50); - mocks.ProblemMutation = (schema) => { return Array.from({ length: faker.random.number({ @@ -547,8 +474,9 @@ mocks.Query = () => ({ userCanSshToEnvironment: () => mocks.Environment(), deploymentByRemoteId: () => mocks.Deployment(), taskByRemoteId: () => mocks.Task(), - allProjects: () => new MockList(9), + allProjects: () => new MockList(600), allOpenshifts: () => new MockList(9), + allProblems: () => new MockList(5), allEnvironments: (parent, args = {}, context, info) => { const project = args.hasOwnProperty('project') ? args.project diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index ae3fced5c5..13483f971b 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -2,10 +2,13 @@ const GraphQLDate = require('graphql-iso-date'); const GraphQLJSON = require('graphql-type-json'); const { + getAllProblems, getProblemsByEnvironmentId, addProblem, deleteProblem, deleteProblemsFromSource, + addProblemsFromSource, + getProblemSources, getProblemHarborScanMatches, addProblemHarborScanMatch, deleteProblemHarborScanMatch, @@ -61,6 +64,7 @@ const { addOrUpdateEnvironment, addOrUpdateEnvironmentStorage, getEnvironmentByName, + getEnvironmentById, getEnvironmentByOpenshiftProjectName, getEnvironmentHoursMonthByEnvironmentId, getEnvironmentStorageByEnvironmentId, @@ -281,7 +285,9 @@ const resolvers = { projectByGitUrl: getProjectByGitUrl, projectByName: getProjectByName, groupByName: getGroupByName, + problemSources: getProblemSources, environmentByName: getEnvironmentByName, + environmentById: getEnvironmentById, environmentByOpenshiftProjectName: getEnvironmentByOpenshiftProjectName, userCanSshToEnvironment, deploymentByRemoteId: getDeploymentByRemoteId, @@ -289,6 +295,7 @@ const resolvers = { allProjects: getAllProjects, allOpenshifts: getAllOpenshifts, allEnvironments: getAllEnvironments, + allProblems: getAllProblems, allGroups: getAllGroups, allProjectsInGroup: getAllProjectsInGroup, billingGroupCost: getBillingGroupCost, diff --git a/services/api/src/resources/environment/resolvers.ts b/services/api/src/resources/environment/resolvers.ts index f00417f8f0..137866d6f9 100644 --- a/services/api/src/resources/environment/resolvers.ts +++ b/services/api/src/resources/environment/resolvers.ts @@ -49,6 +49,25 @@ export const getEnvironmentByName: ResolverFn = async ( return environment; }; +export const getEnvironmentById = async ( + root, + args, + { sqlClient, hasPermission }, +) => { + const environment = await Helpers(sqlClient).getEnvironmentById(args.id); + + if (!environment) { + return null; + } + + await hasPermission('environment', 'view', { + project: environment.project, + }); + + const rows = await query(sqlClient, Sql.selectEnvironmentById(args.id)); + return rows[0]; +}; + export const getEnvironmentsByProjectId: ResolverFn = async ( project, unformattedArgs, diff --git a/services/api/src/resources/problem/helpers.ts b/services/api/src/resources/problem/helpers.ts new file mode 100644 index 0000000000..bfb4f15914 --- /dev/null +++ b/services/api/src/resources/problem/helpers.ts @@ -0,0 +1,61 @@ +import * as R from 'ramda'; +import { MariaClient } from 'mariasql'; +import { query } from '../../util/db'; +import { Helpers as projectHelpers } from '../project/helpers'; +import { Sql } from './sql'; + +export const Helpers = (sqlClient: MariaClient) => { + const groupByProblemIdentifier = (problems) => problems.reduce((obj, problem) => { + obj[problem.identifier] = obj[problem.identifier] || []; + obj[problem.identifier].push(problem); + return obj; + }, {}); + + const getAllProblems = async (source, environment, envType, severity) => { + const environmentType = envType && envType.map(t => t.toLowerCase() || []); + + return await query( + sqlClient, + Sql.selectAllProblems({ + source, + environmentId: environment, + environmentType, + severity, + }) + ); + }; + + const getSeverityOptions = async () => ( + R.map( + R.prop('severity'), + await query(sqlClient, Sql.selectSeverityOptions()), + ) + ); + + const getProblemsWithProjects = async (problems, hasPermission, args: any = []) => { + const withProjects = await Object.keys(problems).map((key) => { + let projects = problems[key].map(async (problem) => { + const envType = !R.isEmpty(args.envType) && args.envType; + const {id, project, openshiftProjectName, name, envName, environmentType}: any = + await projectHelpers(sqlClient).getProjectByEnvironmentId(problem.environment, envType) || {}; + + hasPermission('project', 'view', { + project: !R.isNil(project) && project, + }); + + return (!R.isNil(id)) && {id, project, openshiftProjectName, name, environments: {name: envName}, type: environmentType}; + }); + const {...problem} = R.prop(0, problems[key]); + return {identifier: key, problem: {...problem}, projects: projects, problems: problems[key]}; + }); + + return await Promise.all(withProjects); + }; + + return { + getAllProblems, + getSeverityOptions, + groupByProblemIdentifier, + getProblemsWithProjects + }; +}; \ No newline at end of file diff --git a/services/api/src/resources/problem/resolvers.ts b/services/api/src/resources/problem/resolvers.ts index 2075613228..6fe7eeebb5 100644 --- a/services/api/src/resources/problem/resolvers.ts +++ b/services/api/src/resources/problem/resolvers.ts @@ -1,21 +1,69 @@ -// @flow - import * as R from 'ramda'; -import { sendToLagoonLogs } from '@lagoon/commons/src/logs'; -import { createMiscTask } from '@lagoon/commons/src/tasks'; -import { knex, query, isPatchEmpty } from '../../util/db'; -import { Helpers as environmentHelpers } from '../environment/helpers'; +import { query, prepare } from '../../util/db'; import { Sql } from './sql'; +import { Helpers as problemHelpers } from './helpers'; +import { Helpers as environmentHelpers } from '../environment/helpers'; +import { ResolverFn } from '../'; +const logger = require('../../logger'); + +export const getAllProblems: ResolverFn = async ( + root, + args, + { sqlClient } +) => { + let rows = []; + + try { + if (!R.isEmpty(args)) { + rows = await problemHelpers(sqlClient).getAllProblems(args.source, args.environment, args.envType, args.severity); + } + else { + rows = await query(sqlClient, Sql.selectAllProblems({source: [], environmentId: 0, environmentType: [], severity: []})); + } + } + catch (err) { + if (err) { + logger.warn(err); + return []; + } + } + + const problems = rows && rows.map(problem => { + const { environment: envId, name, project, environmentType, openshiftProjectName, ...rest} = problem; + return { ...rest, environment: { id: envId, name, project, environmentType, openshiftProjectName }}; + }); + + const sorted = R.sort(R.descend(R.prop('severity')), problems); + return sorted.map((row: any) => ({ ...(row as Object) })); +}; -/* :: +export const getSeverityOptions = async ( + root, + args, + { sqlClient }, +) => { + return await problemHelpers(sqlClient).getSeverityOptions(); +}; -import type {ResolversObj} from '../'; +export const getProblemSources = async ( + root, + args, + { sqlClient }, +) => { + const preparedQuery = prepare( + sqlClient, + `SELECT DISTINCT source FROM environment_problem`, + ); -*/ + return R.map( + R.prop('source'), + await query(sqlClient, preparedQuery(args)) + ); +}; export const getProblemsByEnvironmentId = async ( { id: environmentId }, - {severity}, + {severity, source}, { sqlClient, hasPermission }, ) => { const environment = await environmentHelpers(sqlClient).getEnvironmentById(environmentId); @@ -29,6 +77,7 @@ export const getProblemsByEnvironmentId = async ( Sql.selectProblemsByEnvironmentId({ environmentId, severity, + source, }), ); @@ -189,4 +238,4 @@ export const deleteProblemHarborScanMatch = async ( await query(sqlClient, Sql.deleteProblemHarborScanMatch(id)); return 'success'; -} +}; diff --git a/services/api/src/resources/problem/sql.ts b/services/api/src/resources/problem/sql.ts index 7e54880091..32323e3a16 100644 --- a/services/api/src/resources/problem/sql.ts +++ b/services/api/src/resources/problem/sql.ts @@ -1,12 +1,4 @@ -// @flow - -import { knex } from '../../util/db'; - -/* :: - -import type {SqlObj} from '../'; - -*/ +const { knex } = require('../../util/db'); const standardEnvironmentReturn = { id: 'id', @@ -34,24 +26,55 @@ const standardProblemHarborScanMatchReturn = { default_lagoon_environment: 'defaultLagoonEnvironment', default_lagoon_service_name: 'defaultLagoonServiceName', regex: 'regex' - }; +}; + +export const Sql = { + selectAllProblems: ({ + source = [], + environmentId, + environmentType = [], + severity = [], + }: { source: string[], environmentId: number, environmentType: string[], severity: string[]}) => { + let q = knex('environment_problem as p') + .join('environment as e', {environment: 'e.id'}, '=', {environment: 'p.environment'}) + .where('p.deleted', '=', '0000-00-00 00:00:00') + .select('p.*', {environment: 'e.id'}, { name: 'e.name', project: 'e.project', + environmentType: 'e.environment_type', openshiftProjectName: 'e.openshift_project_name'}); -export const Sql /* : SqlObj */ = { - selectAllProblems: () => + if (environmentType.length > 0) { + q.whereIn('e.environment_type', environmentType); + } + if (source.length > 0) { + q.whereIn('p.source', source); + } + if (environmentId) { + q.where('p.environment', environmentId); + } + if (severity.length > 0) { + q.whereIn('p.severity', severity); + } + return q.toString(); + }, + selectSeverityOptions: () => knex('environment_problem') - .select(standardEnvironmentReturn).toString(), + .select('severity') + .toString(), selectProblemByDatabaseId: (id) => knex('environment_problem').where('id', id).toString(), selectProblemsByEnvironmentId: ({ environmentId, severity = [], + source = [], }) => { let q = knex('environment_problem').select(standardEnvironmentReturn) .where('environment', environmentId) .where('deleted', '=', '0000-00-00 00:00:00'); - if(severity.length > 0) { + if (severity.length > 0) { q.whereIn('severity', severity); } + if (source.length > 0) { + q.whereIn('source', source); + } return q.toString() }, insertProblem: ({environment, severity, severity_score, identifier, lagoon_service, source, diff --git a/services/api/src/resources/project/helpers.ts b/services/api/src/resources/project/helpers.ts index 7b8b30e69c..985f7eb128 100644 --- a/services/api/src/resources/project/helpers.ts +++ b/services/api/src/resources/project/helpers.ts @@ -1,8 +1,8 @@ -import * as R from 'ramda'; +const R = require('ramda'); import { MariaClient } from 'mariasql'; -import { asyncPipe } from '@lagoon/commons/dist/util'; -import { query } from '../../util/db'; -import { Sql } from './sql'; +const { asyncPipe } = require('@lagoon/commons/dist/util'); +const { query } = require('../../util/db'); +const { Sql } = require('./sql'); export const Helpers = (sqlClient: MariaClient) => { const getProjectById = async (id: number) => { @@ -10,10 +10,15 @@ export const Helpers = (sqlClient: MariaClient) => { return R.prop(0, rows); }; - const getProjectByEnvironmentId = async (environmentId) => { - const rows = await query(sqlClient, Sql.selectProjectByEnvironmentId(environmentId)); + const getProjectByName = async (name: string) => { + const rows = await query(sqlClient, Sql.selectProjectByName(name)); return R.prop(0, rows); - } + }; + + const getProjectByEnvironmentId = async (environmentId: number, environmentType = null) => { + const rows = await query(sqlClient, Sql.selectProjectByEnvironmentId(environmentId, environmentType)); + return R.prop(0, rows); + }; const getProjectsByIds = (projectIds: number[]) => query(sqlClient, Sql.selectProjectsByIds(projectIds)); diff --git a/services/api/src/resources/project/resolvers.ts b/services/api/src/resources/project/resolvers.ts index bfff4debbe..6613890bf3 100644 --- a/services/api/src/resources/project/resolvers.ts +++ b/services/api/src/resources/project/resolvers.ts @@ -168,6 +168,10 @@ export const getProjectByName: ResolverFn = async ( const rows = await query(sqlClient, prep(args)); const project = rows[0]; + if (!project) { + return null; + } + await hasPermission('project', 'view', { project: project.id, }); diff --git a/services/api/src/resources/project/sql.ts b/services/api/src/resources/project/sql.ts index aea8d90b4a..b787cc531d 100644 --- a/services/api/src/resources/project/sql.ts +++ b/services/api/src/resources/project/sql.ts @@ -29,12 +29,19 @@ export const Sql = { knex('project as p') .whereIn('p.id', projectIds) .toString(), - selectProjectByEnvironmentId: (environmentId) => - knex('environment as e') - .select('e.id', 'e.project', 'e.openshift_project_name', 'p.name') - .leftJoin('project as p', 'p.id', '=', 'e.project') - .where('e.id', environmentId) - .toString(), + selectProjectByEnvironmentId: ( + environmentId, + environmentType = [] + ): {environmentId: number, environmentType: string} => { + let q = knex('environment as e') + .select('e.id', {envName: 'e.name'}, 'e.environment_type', 'e.project', 'e.openshift_project_name', 'p.name') + .leftJoin('project as p', 'p.id', '=', 'e.project'); + if (environmentType && environmentType.length > 0) { + q.where('e.environment_type', environmentType); + } + q.where('e.id', environmentId); + return q.toString(); + }, updateProject: ({ id, patch }: { id: number, patch: { [key: string]: any } }) => knex('project') .where('id', '=', id) diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 111eff0b26..3c9eba1d4d 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -172,7 +172,6 @@ const typeDefs = gql` created: String } - input BulkProblem { severity: ProblemSeverityRating severityScore: SeverityScore @@ -433,6 +432,10 @@ const typeDefs = gql` """ developmentEnvironmentsLimit: Int """ + Name of the OpenShift Project/Namespace + """ + openshiftProjectName: String + """ Deployed Environments for this Project """ environments( @@ -546,7 +549,7 @@ const typeDefs = gql` backups(includeDeleted: Boolean): [Backup] tasks(id: Int): [Task] services: [EnvironmentService] - problems(severity: [ProblemSeverityRating]): [Problem] + problems(severity: [ProblemSeverityRating], source: [String]): [Problem] } type EnvironmentHitsMonth { @@ -692,6 +695,7 @@ const typeDefs = gql` """ projectByGitUrl(gitUrl: String!): Project environmentByName(name: String!, project: Int!): Environment + environmentById(id: Int!): Environment """ Returns Environment Object by a given openshiftProjectName """ @@ -720,6 +724,11 @@ const typeDefs = gql` """ allEnvironments(createdAfter: String, type: EnvType, order: EnvOrderType): [Environment] """ + Returns all Problems matching given filter (all if no filter defined) + """ + allProblems(source: [String], project: Int, environment: Int, envType: [EnvType], identifier: String, severity: [ProblemSeverityRating]): [Problem] + problemSources: [String] + """ Returns all Groups matching given filter (all if no filter defined) """ allGroups(name: String, type: String): [GroupInterface] @@ -1211,8 +1220,6 @@ const typeDefs = gql` parentGroup: GroupInput } - - input AddBillingModifierInput { """ The existing billing group for this modifier @@ -1478,7 +1485,6 @@ const typeDefs = gql` removeGroupsFromProject(input: ProjectGroupsInput!): Project updateProjectMetadata(input: UpdateMetadataInput!): Project removeProjectMetadataByKey(input: RemoveMetadataInput!): Project - addBillingModifier(input: AddBillingModifierInput!): BillingModifier updateBillingModifier(input: UpdateBillingModifierInput!): BillingModifier deleteBillingModifier(input: DeleteBillingModifierInput!): String diff --git a/services/ui/.gitignore b/services/ui/.gitignore index c1733462af..80e2398775 100644 --- a/services/ui/.gitignore +++ b/services/ui/.gitignore @@ -8,3 +8,4 @@ storybook-static #environment .env +.python-version diff --git a/services/ui/.storybook/decorators/ApiConnection.js b/services/ui/.storybook/decorators/ApiConnection.js index 6390875749..ed5aa163a4 100644 --- a/services/ui/.storybook/decorators/ApiConnection.js +++ b/services/ui/.storybook/decorators/ApiConnection.js @@ -6,7 +6,7 @@ import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemo import { SchemaLink } from 'apollo-link-schema'; import { ApolloProvider } from 'react-apollo'; import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; -import typeDefs from 'api/src/typeDefs'; +import typeDefs from 'api/dist/typeDefs'; import mocks, { seed } from 'api/src/mocks'; import introspectionQueryResultData from 'api/src/fragmentTypes.json'; diff --git a/services/ui/.storybook/presets.js b/services/ui/.storybook/presets.js index 9531befa38..c32795a60c 100644 --- a/services/ui/.storybook/presets.js +++ b/services/ui/.storybook/presets.js @@ -1,3 +1,4 @@ module.exports = [ '@storybook/addon-docs/preset', + '@storybook/preset-typescript' ]; diff --git a/services/ui/.storybook/webpack.config.js b/services/ui/.storybook/webpack.config.js index 5024c45895..0564ff7802 100644 --- a/services/ui/.storybook/webpack.config.js +++ b/services/ui/.storybook/webpack.config.js @@ -9,8 +9,19 @@ module.exports = async ({ config, mode }) => { // Add alias for storybook decorators and components. config.resolve.alias.storybook = __dirname; + config.module.rules.push({ + test: /\.(ts|tsx)$/, + use: [ + { + loader: require.resolve('ts-loader'), + }, + ], + }); + + config.resolve.extensions.push('.ts', '.tsx'); + // Debug config. - // console.dir(config, { depth: null }); + //console.dir(config, { depth: null }); return config; }; diff --git a/services/ui/package.json b/services/ui/package.json index a407902934..d0b93473ab 100644 --- a/services/ui/package.json +++ b/services/ui/package.json @@ -42,10 +42,11 @@ "react-beautiful-dnd": "^13.0.0", "react-copy-to-clipboard": "^5.0.1", "react-dom": "^16.8.4", + "react-hexgrid": "^1.0.3", "react-highlight-words": "^0.14.0", "react-modal": "^3.8.1", "react-nice-dates": "^1.0.2", - "react-select": "^2.1.1", + "react-select": "^3.0.0", "react-typekit": "^1.1.3", "recompose": "^0.30.0", "resize-observer-polyfill": "^1.5.1", @@ -63,6 +64,7 @@ "@storybook/addon-viewport": "^5.3.0-beta.16", "@storybook/addons": "^5.3.0-beta.16", "@storybook/core": "^5.3.0-beta.16", + "@storybook/preset-typescript": "^3.0.0", "@storybook/react": "^5.3.0-beta.16", "apollo-link-schema": "^1.2.4", "babel-loader": "^8.0.6", @@ -71,7 +73,8 @@ "prop-types": "^15.7.2", "react-is": "^16.12.0", "regenerator-runtime": "^0.13.3", - "require-context.macro": "^1.2.2" + "require-context.macro": "^1.2.2", + "ts-loader": "^7.0.5" }, "postcss": { "plugins": { diff --git a/services/ui/server.js b/services/ui/server.js index b7015f19a5..f4631870cf 100644 --- a/services/ui/server.js +++ b/services/ui/server.js @@ -27,7 +27,6 @@ app app.render(req, res, '/project', { projectName: req.params.projectSlug }); }); - server.get('/admin/billing/:billingGroupSlug', (req, res) => { app.render(req, res, '/admin/billing', { billingGroupName: req.params.billingGroupSlug }); }); @@ -44,8 +43,6 @@ app app.render(req, res, '/admin/billing', { billingGroupName: req.params.billingGroupSlug, year: req.params.yearSlug, month: req.params.monthSlug, lang: req.params.lang }); }); - - server.get('/projects/:projectSlug/:environmentSlug', (req, res) => { app.render(req, res, '/environment', { openshiftProjectName: req.params.environmentSlug @@ -105,6 +102,27 @@ app } ); + server.get( + '/problems/project', + (req, res) => { + app.render(req, res, '/problems-dashboard-by-project'); + } + ); + + server.get( + '/problems', + (req, res) => { + app.render(req, res, '/problems-dashboard-by-project-hex'); + } + ); + + server.get( + '/problems/identifier', + (req, res) => { + app.render(req, res, '/problems-dashboard'); + } + ); + server.get('*', (req, res) => { return handle(req, res); }); diff --git a/services/ui/src/components/Accordion/index.js b/services/ui/src/components/Accordion/index.js new file mode 100644 index 0000000000..3b1f9de96f --- /dev/null +++ b/services/ui/src/components/Accordion/index.js @@ -0,0 +1,89 @@ +import React, { useState, Fragment } from "react"; +import PropTypes from "prop-types"; + +const Accordion = ({ children, defaultValue = true, minified = false, className = "", onToggle, columns }) => { + const [visibility, setVisibility] = useState(defaultValue); + const accordionType = minified ? 'minified' : 'wide'; + const colCountClass = columns && 'cols-'+Object.keys(columns).length; + + return ( +
+
{ + setVisibility(!visibility); + if (onToggle) onToggle(!visibility); + }}> + {Object.keys(columns).map((item, i) =>
{columns[item]}
)} +
+ + {visibility ? {children} : null} + +
+ ); +}; + +Accordion.propTypes = { + children: PropTypes.any.isRequired, + defaultValue: PropTypes.bool, + className: PropTypes.string, + onToggle: PropTypes.func, + columns: PropTypes.any.isRequired +}; + +export default Accordion; \ No newline at end of file diff --git a/services/ui/src/components/Accordion/index.stories.js b/services/ui/src/components/Accordion/index.stories.js new file mode 100644 index 0000000000..65244e1b57 --- /dev/null +++ b/services/ui/src/components/Accordion/index.stories.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import Accordion from './index'; + +export default { + component: Accordion, + title: 'Components/Accordion', +}; + +export const Default = () => ( + Some default body content. +); \ No newline at end of file diff --git a/services/ui/src/components/ActiveStandbyConfirm/index.js b/services/ui/src/components/ActiveStandbyConfirm/index.js index ef2eb99f78..6738557e27 100644 --- a/services/ui/src/components/ActiveStandbyConfirm/index.js +++ b/services/ui/src/components/ActiveStandbyConfirm/index.js @@ -3,7 +3,7 @@ import Modal from 'components/Modal'; import Button from 'components/Button'; import { bp, color } from 'lib/variables'; // @TODO: add this once the logic exists -import withLogic from 'components/ActiveStandbyConfirm/logic'; +//import withLogic from 'components/ActiveStandbyConfirm/logic'; import ActiveStandby from 'components/ActiveStandbyConfirm'; /** @@ -66,4 +66,4 @@ export const ActiveStandbyConfirm = ({ ); }; -export default withLogic(ActiveStandbyConfirm); +export default ActiveStandbyConfirm; diff --git a/services/ui/src/components/Filters/helpers.js b/services/ui/src/components/Filters/helpers.js new file mode 100644 index 0000000000..1e7837ee5e --- /dev/null +++ b/services/ui/src/components/Filters/helpers.js @@ -0,0 +1,33 @@ +import gql from "graphql-tag"; + +export const getProjectOptions = gql` + query getProjectOptions { + allProjects { + id + name + environments { + id + name + } + } + } +`; + +export const getSourceOptions = gql` + query getProblemSources { + sources: problemSources + } +`; + +const getSeverityEnumQuery = gql` + query severityEnum { + __type(name: "ProblemSeverityRating") { + name + enumValues { + name + } + } + } +`; + +export default getSeverityEnumQuery; diff --git a/services/ui/src/components/Filters/index.js b/services/ui/src/components/Filters/index.js new file mode 100644 index 0000000000..e3ad427884 --- /dev/null +++ b/services/ui/src/components/Filters/index.js @@ -0,0 +1,70 @@ +import React from 'react'; +import Select from 'react-select'; +import { bp } from 'lib/variables'; +import { color } from 'lib/variables'; + +/** + * Displays a select filter and sends state back to parent in a callback. + * + */ +const SelectFilter = ({ title, options, onFilterChange, loading, defaultValue, isMulti}) => { + + const handleChange = (values) => { + onFilterChange(values); + }; + + const selectStyles = { + container: (styles) => { + return ({ + ...styles, + "width": "100%" + }) + }, + control: styles => ({ ...styles, backgroundColor: 'white' }), + option: (styles, { data, isDisabled, isFocused, isSelected }) => { + return { + ...styles, + backgroundColor: isDisabled && 'grey', + cursor: isDisabled ? 'not-allowed' : 'default', + }; + }, + }; + + return ( +
+ +
- {!currentItems.length &&
No Problems
} - {currentItems.map((problem) => { - return ( filterResults(item)) &&
No Problems
} + {sortedItems.filter(item => filterResults(item)).map((problem) => { + + const {id, description, environment, project, data, service, deleted, version, fixedVersion, + links, __typename, created, ...selectedColumns} = problem; + const formatCreated = moment.utc(created) + .local() + .format('DD MM YYYY, HH:mm:ss'); + const { identifier, severity, source, severityScore, associatedPackage } = selectedColumns; + const columns = {identifier, severity, source, created: formatCreated, severityScore, associatedPackage}; + + return ( +
@@ -131,186 +124,149 @@ const Problems = ({ problems }) => {
)} -
-
Raw Data:
+ {problem.data && ( +
+
{Object.entries(JSON.parse(problem.data)).map(([a, b]) => { - if(b) { + if (b) { return ( -
- -
{b}
-
+
+ +
{b}
+
); } })}
-
+
)}
- ); - })} + ); + })}
- -
+ .row-data { + padding: 0; + margin: 0; + background: #2d2d2d; + color: white; + font: 0.8rem Inconsolata, monospace; + line-height: 2; + transition: all 0.6s ease-in-out; + } + } + `} +
); }; diff --git a/services/ui/src/components/Problems/index.stories.js b/services/ui/src/components/Problems/index.stories.js index 6a5591f537..0033d8110f 100644 --- a/services/ui/src/components/Problems/index.stories.js +++ b/services/ui/src/components/Problems/index.stories.js @@ -1,16 +1,12 @@ import React from 'react'; import Problems from './index'; -import faker from 'faker/locale/en'; import mocks, { generator } from 'api/src/mocks'; -import {MockList} from "graphql-tools"; export default { component: Problems, title: 'Components/Problems', } -let temp = mocks.ProblemMutation(mocks.Problem); - export const Default = () => ( ); diff --git a/services/ui/src/components/Problems/sortedItems.js b/services/ui/src/components/Problems/sortedItems.js index 013a459711..b9ba246ca6 100644 --- a/services/ui/src/components/Problems/sortedItems.js +++ b/services/ui/src/components/Problems/sortedItems.js @@ -2,17 +2,14 @@ import React, {useState} from "react"; import moment from 'moment'; import hash from 'object-hash'; -const useSortableData = (initialItems) => { +const useSortableProblemsData = (initialItems) => { const initialConfig = {key: 'identifier', direction: 'ascending'}; const [sortConfig, setSortConfig] = React.useState(initialConfig); const [currentItems, setCurrentItems] = useState(initialItems); const getClassNamesFor = (name) => { - if (!sortConfig) { - return; - } - - return sortConfig.key === name ? sortConfig.direction : undefined; + if (!sortConfig) return; + return sortConfig.key === name && sortConfig.direction || 'no-sort'; }; const sortedItems = React.useMemo(() => { @@ -59,4 +56,4 @@ const useSortableData = (initialItems) => { return { sortedItems: currentItems, getClassNamesFor, requestSort }; }; -export default useSortableData; \ No newline at end of file +export default useSortableProblemsData; diff --git a/services/ui/src/components/ProblemsByIdentifier/index.js b/services/ui/src/components/ProblemsByIdentifier/index.js new file mode 100644 index 0000000000..f2b9a77639 --- /dev/null +++ b/services/ui/src/components/ProblemsByIdentifier/index.js @@ -0,0 +1,327 @@ +import React, { useState } from 'react'; +import { bp, color, fontSize } from 'lib/variables'; +import useSortableData from './sortedItems'; +import Accordion from 'components/Accordion'; +import ProblemsLink from 'components/link/Problems'; + +const ProblemsByIdentifier = ({ problems }) => { + const { sortedItems, getClassNamesFor, requestSort } = useSortableData(problems); + + const [problemTerm, setProblemTerm] = useState(''); + const [hasFilter, setHasFilter] = React.useState(false); + const [moreProjectsLimit, setMoreProjectsLimit] = React.useState(5); + + const handleProblemFilterChange = (event) => { + setHasFilter(false); + + if (event.target.value !== null || event.target.value !== '') { + setHasFilter(true); + } + setProblemTerm(event.target.value); + }; + + const handleSort = (key) => { + return requestSort(key); + }; + + const filterResults = (item) => { + const lowercasedFilter = problemTerm.toLowerCase(); + if (problemTerm == null || problemTerm === '') { + return problems; + } + + return Object.keys(item).some(key => { + if (item[key] !== null) { + return item[key].toString().toLowerCase().includes(lowercasedFilter); + } + }); + }; + + const onLoadMore = () => { + setMoreProjectsLimit(moreProjectsLimit+moreProjectsLimit); + }; + + return ( +
+
+ +
+
+ + + + +
+
+ {!sortedItems.filter(item => filterResults(item)).length &&
No Problems
} + {sortedItems.filter(item => filterResults(item)).map((item) => { + const {identifier, source, severity, problems, environment } = item; + const { description, associatedPackage, links } = problems[0] || ''; + + const columns = { + identifier: identifier, source, severity, + projectsAffected: problems && problems.filter(p => p != null).length || 0 + }; + + return ( + +
+
+
+ + {description &&
+ {description.length > 250 ? description.substring(0, 247)+'...' : description} +
} +
+
+ + {associatedPackage &&
{associatedPackage}
} +
+
+ + {links && } +
+
+
+
+ + {problems && problems.filter(p => p != null).slice(0, moreProjectsLimit).map(problem => { + const { id, name: envName, openshiftProjectName, environmentType, project } = problem.environment || ''; + + return ( +
+ + {project ? `${project.name}` : ''}{envName ? ` : ${envName.toLowerCase()}` : ''} + +
+ ) + })} + {problems && problems.filter(p => p != null).length > moreProjectsLimit && + + } +
+
+
+
+ ); + })} +
+ +
+ ); +}; + +export default ProblemsByIdentifier; diff --git a/services/ui/src/components/ProblemsByIdentifier/index.stories.js b/services/ui/src/components/ProblemsByIdentifier/index.stories.js new file mode 100644 index 0000000000..66f1ec571c --- /dev/null +++ b/services/ui/src/components/ProblemsByIdentifier/index.stories.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { Query } from 'react-apollo'; +import AllProblemsQuery from 'lib/query/AllProblems'; +import mocks, { generator } from 'api/src/mocks'; +import ProblemsByIdentifier from './index'; + +export default { + component: ProblemsByIdentifier, + title: 'Components/ProblemsByIdentifier', +} + +export const Default = ({ problems }) => ; +Default.story = { + decorators: [ + storyFn => ( + + {({data}) => storyFn({problems: data.problems})} + + ), + ], +}; + +export const NoProblems = () => ( + +); diff --git a/services/ui/src/components/ProblemsByIdentifier/sortedItems.js b/services/ui/src/components/ProblemsByIdentifier/sortedItems.js new file mode 100644 index 0000000000..c67f2c97d8 --- /dev/null +++ b/services/ui/src/components/ProblemsByIdentifier/sortedItems.js @@ -0,0 +1,70 @@ +import React, {useState, useMemo} from "react"; +import hash from 'object-hash'; + +const useSortableData = (initialItems, initialConfig = {key: 'severity', direction: 'ascending'}) => { + const [sortConfig, setSortConfig] = React.useState(initialConfig); + const [currentItems, setCurrentItems] = useState(initialItems); + + const getClassNamesFor = (name) => { + if (!sortConfig) return; + return sortConfig.key === name ? sortConfig.direction : undefined; + }; + + const sortedItems = useMemo(() => { + let sortableItems = [...currentItems]; + + if (sortConfig !== null) { + sortableItems.sort((a, b) => { + let aParsed, bParsed = ''; + + if (sortConfig.key === 'identifier') { + aParsed = a[sortConfig.key].toString().toLowerCase().trim(); + bParsed = b[sortConfig.key].toString().toLowerCase().trim(); + } + else if (sortConfig.key === 'projectsAffected') { + aParsed = a.problems.length; + bParsed = b.problems.length; + } + else { + let aProblem, bProblem; + + if (a[sortConfig.key] === undefined) aProblem = a.problem; + if (b[sortConfig.key] === undefined) bProblem = b.problem; + + let aItem = a[sortConfig.key] || aProblem[sortConfig.key]; + aParsed = aItem.toString().toLowerCase().trim(); + + let bItem = b[sortConfig.key] || bProblem[sortConfig.key]; + bParsed = bItem.toString().toLowerCase().trim(); + } + + if (aParsed < bParsed) return sortConfig.direction === 'ascending' ? -1 : 1; + if (aParsed > bParsed) return sortConfig.direction === 'ascending' ? 1 : -1; + return 0; + }); + } + + return sortableItems; + }, [currentItems, sortConfig]); + + if (hash(sortedItems) !== hash(currentItems)) { + setCurrentItems(sortedItems); + } + + const requestSort = (key) => { + let direction = 'ascending'; + + if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { + direction = 'descending'; + } + + setCurrentItems(sortedItems); + setSortConfig({ key, direction }); + + return { sortedItems: currentItems }; + }; + + return { sortedItems: currentItems, currentSortConfig: sortConfig, getClassNamesFor, requestSort }; +}; + +export default useSortableData; \ No newline at end of file diff --git a/services/ui/src/components/ProblemsByProject/Honeycomb/index.js b/services/ui/src/components/ProblemsByProject/Honeycomb/index.js new file mode 100644 index 0000000000..9219633ba1 --- /dev/null +++ b/services/ui/src/components/ProblemsByProject/Honeycomb/index.js @@ -0,0 +1,190 @@ +import React, {useState, Fragment, useEffect, useRef} from "react"; +import { HexGrid, Layout, Hexagon, Text, GridGenerator, HexUtils } from 'react-hexgrid'; +import * as R from 'ramda'; +import ProblemsByProject from "components/ProblemsByProject"; +import {LoadingPageNoHeader} from 'pages/_loading'; +import {ErrorNoHeader} from 'pages/_error'; +import { bp } from 'lib/variables'; +import './styling.css'; + +const config = { + "width": 1200, + "height": 100, + "layout": {"width": 4, "height": 4, "flat": false, "spacing": 1.08}, + "origin": {"x": 0, "y": 0}, + "map": "rectangle", +}; + +const Honeycomb = ({ data, filter }) => { + const { projectsProblems } = data || []; + const [projects, setProjects] = useState(projects); + const [projectInView, setProjectInView] = useState(false); + const [display, setDisplay] = useState({type: "normal", multiplier: 2}); + + const generator = GridGenerator.getGenerator(config.map); + const projectCount = projectsProblems && parseInt(projectsProblems.length); + const displayMultiple = display && parseInt(display.multiplier * 8); + let rows = projectsProblems && parseInt(projectCount / displayMultiple); + + const hexs = generator.apply(config, [displayMultiple, ++rows]); + const layout = config.layout; + const size = { + x: parseInt(display.hexSize * layout.width), + y: parseInt(display.hexSize * layout.height) + }; + + const handleHexClick = (project) => { + const {environments, id, name} = project || []; + const problems = environments && environments.filter(e => e instanceof Object).map(e => { + return e.problems; + }); + + const problemsPerProject = Array.prototype.concat.apply([], problems); + const critical = problemsPerProject.filter(p => p.severity === 'CRITICAL').length; + const high = problemsPerProject.filter(p => p.severity === 'HIGH').length; + const medium = problemsPerProject.filter(p => p.severity === 'MEDIUM').length; + const low = problemsPerProject.filter(p => p.severity === 'LOW').length; + + setProjectInView({name: name, environments: environments, severityCount: {critical: critical, high: high, medium: medium, low: low}}); + }; + + const flattenProblems = (project) => { + const {environments} = project || []; + const filterProblems = environments && environments.filter(e => e instanceof Object).map(e => { + return e.problems; + }); + return Array.prototype.concat.apply([], filterProblems); + }; + + const sortByProjects = (projects) => { + return projects && projects.sort((a, b) => { + const aProblems = flattenProblems(a); + const bProblems = flattenProblems(b); + + return bProblems.length - aProblems.length; + }); + }; + + const getClassName = (critical) => { + if (critical === 0) { return "no-critical" } + if (critical === 1) { return "light-red" } else + if (critical >= 1 && critical <= 5) { return "red" } else + if (critical >= 5 && critical < 10) { return "dark-red" } else + if (critical >= 10 && critical < 15) { return "darker-red" } + }; + + useEffect(() => { + const count = projectsProblems && projectsProblems.length; + if (count <= 48) setDisplay({type: "normal", multiplier: 2, hexSize: 4, viewBox: "180 -20 100 100"}); + if (count >= 49 && count <= 96) setDisplay({type: "medium", multiplier: 4, hexSize: 1, viewBox: "65 -30 100 100"}); + if (count >= 97 && count <=479) setDisplay({type: "large", multiplier: 4, hexSize: 1, viewBox: "65 -10 100 100"}); + if (count >= 480) setDisplay({type: "extra-large", multiplier: 5.5, hexSize: 0.66, viewBox: "30 -10 100 100"}); + + const filterProjects = !filter.showCleanProjects ? projectsProblems && projectsProblems.filter(p => { + return !R.isEmpty(flattenProblems(p)) + }) : projectsProblems && projectsProblems; + + const sortProjects = filterProjects && sortByProjects(filterProjects); + + setProjects(sortProjects); + }, [projectsProblems, filter]); + + return ( +
+ {!projects && } + {projects && +
+
+ +
+
+ } + {projects && + <> + + + {hexs.slice(0, projects.length).map((hex, i) => { + const project = projects[i] || null; + const {environments, id, name} = project; + const filterProblems = environments && environments.filter(e => e instanceof Object).map(e => { + return e.problems; + }); + + const problemsPerProject = Array.prototype.concat.apply([], filterProblems); + const critical = problemsPerProject.filter(p => p.severity === 'CRITICAL').length; + const problemCount = problemsPerProject.length || 0; + + return ( + handleHexClick(project)}> + {problemsPerProject.length ? P: {problemCount}, C: {critical} + : P: {problemCount}} + + )})} + + +
+
+
+ {projectInView ? + <> +
+ {projectInView.environments && projectInView.environments.map(environment => ( +
+ + +
+ ))} + + :
No project selected
+ } +
+
+
+ + } + +
+ ); +}; + +export default Honeycomb; \ No newline at end of file diff --git a/services/ui/src/components/ProblemsByProject/Honeycomb/index.stories.js b/services/ui/src/components/ProblemsByProject/Honeycomb/index.stories.js new file mode 100644 index 0000000000..566c734246 --- /dev/null +++ b/services/ui/src/components/ProblemsByProject/Honeycomb/index.stories.js @@ -0,0 +1,26 @@ +import React from 'react'; +import Honeycomb from './index'; +import { Query } from 'react-apollo'; +import AllProjectsProblemsQuery from 'lib/query/AllProjectsProblems'; + +export default { + component: Honeycomb, + title: 'Components/Honeycomb', +} + +export const Default = (projects) => { + return projects && +}; +Default.story = { + decorators: [ + storyFn => ( + + {({data: projectsProblems}) => projectsProblems && storyFn({projects: projectsProblems})} + + ), + ], +}; + +export const NoProjects = () => ( + +); diff --git a/services/ui/src/components/ProblemsByProject/Honeycomb/styling.css b/services/ui/src/components/ProblemsByProject/Honeycomb/styling.css new file mode 100644 index 0000000000..be4e4bd1a4 --- /dev/null +++ b/services/ui/src/components/ProblemsByProject/Honeycomb/styling.css @@ -0,0 +1,52 @@ +svg.grid { + margin: 0 auto; + width: 100%; +} +svg.grid g { + fill: #f2f2f2; + fill-opacity: 0.6; +} +svg.grid g:hover { + fill-opacity: 1; +} +svg.grid g:hover text { + fill-opacity: 1; +} +svg.grid g .green g { + fill: #cbf3cf; +} +svg.grid g .light-red g { + fill: #ffa19c; +} +svg.grid g .red g { + fill: #ff6961; +} +svg.grid g .dark-red g { + fill: #c30a00; +} +svg.grid g .darker-red g { + fill: #880700; +} +svg.grid g polygon { + cursor: pointer; + stroke: #4c84ff; + stroke-width: 0.2; + transition: fill-opacity .2s; +} +svg.grid g text { + font-size: 0.325em; + fill: #000; + fill-opacity: 0.5; + transition: fill-opacity .2s; +} +svg.grid g text.no-text { + font-size: 0; +} +svg.grid path { + fill: none; + stroke: hsl(60, 20%, 70%); + stroke-width: 0.4em; + stroke-opacity: 0.3; + stroke-linecap: round; + stroke-linejoin: round; +} \ No newline at end of file diff --git a/services/ui/src/components/ProblemsByProject/index.js b/services/ui/src/components/ProblemsByProject/index.js new file mode 100644 index 0000000000..fb10237391 --- /dev/null +++ b/services/ui/src/components/ProblemsByProject/index.js @@ -0,0 +1,306 @@ +import React, { useState, useEffect } from 'react'; +import { bp, color, fontSize } from 'lib/variables'; +import useSortableData from './sortedItems'; +import Accordion from 'components/Accordion'; + +const ProblemsByProject = ({ problems }) => { + const { sortedItems, getClassNamesFor, requestSort } = useSortableData(problems, {key: 'id', direction: 'ascending'}); + + const [problemTerm, setProblemTerm] = useState(''); + const [hasFilter, setHasFilter] = React.useState(false); + + const handleProblemFilterChange = (event) => { + setHasFilter(false); + + if (event.target.value !== null || event.target.value !== '') { + setHasFilter(true); + } + setProblemTerm(event.target.value); + }; + + const handleSort = (key) => { + return requestSort(key); + }; + + const filterResults = (item) => { + const lowercasedFilter = problemTerm.toLowerCase(); + if (problemTerm == null || problemTerm === '') { + return problems; + } + + return Object.keys(item).some(key => { + if (item[key] !== null) { + return item[key].toString().toLowerCase().includes(lowercasedFilter); + } + }); + }; + + return ( +
+
+ +
+
+ + + + +
+
+ {!sortedItems.filter(problem => filterResults(problem)).length &&
No Problems
} + {sortedItems.filter(problem => filterResults(problem)).map((problem) => { + + const {identifier, source, severity, associatedPackage, data } = problem; + const columns = {identifier, source, severity, associatedPackage}; + const parsedData = JSON.parse(data); + + return ( + +
+
+ + {problem &&
+ {(problem.description).length > 250 ? problem.description.substring(0, 247)+'...' : problem.description} +
} +
+
+ + {problem &&
{problem.associatedPackage}
} +
+
+ + {problem && } +
+ {problem && (
+ +
+ {parsedData &&
{Object.keys(parsedData).map((key) => { + return
{key}: {`${parsedData[key]}`}
+ })}
} +
+
)} +
+
+ ); + })} +
+ +
+ ); +}; + +export default ProblemsByProject; diff --git a/services/ui/src/components/ProblemsByProject/sortedItems.js b/services/ui/src/components/ProblemsByProject/sortedItems.js new file mode 100644 index 0000000000..3884ed3917 --- /dev/null +++ b/services/ui/src/components/ProblemsByProject/sortedItems.js @@ -0,0 +1,63 @@ +import React, {useState, useEffect, useMemo} from "react"; +import hash from 'object-hash'; + +const useSortableData = (initialItems, initialConfig) => { + const [sortConfig, setSortConfig] = React.useState(initialConfig); + const [currentItems, setCurrentItems] = useState(initialItems); + + const getClassNamesFor = (name) => { + if (!sortConfig) return; + return sortConfig.key === name ? sortConfig.direction : undefined; + }; + + const sortedItems = useMemo(() => { + if (!currentItems) return; + + let sortableItems = [...currentItems]; + + if (sortConfig !== null) { + sortableItems.sort((a, b) => { + let aParsed, bParsed = ''; + + if (sortConfig.key === 'identifier') { + aParsed = a[sortConfig.key].toString().toLowerCase().trim(); + bParsed = b[sortConfig.key].toString().toLowerCase().trim(); + } + else { + let aProblem = a[sortConfig.key]; + aParsed = aProblem.toString().toLowerCase().trim(); + + let bProblem = b[sortConfig.key]; + bParsed = bProblem.toString().toLowerCase().trim(); + } + + if (aParsed < bParsed) return sortConfig.direction === 'ascending' ? -1 : 1; + if (aParsed > bParsed) return sortConfig.direction === 'ascending' ? 1 : -1; + return 0; + }); + } + + return sortableItems; + }, [currentItems, sortConfig]); + + if (hash(sortedItems) !== hash(currentItems)) { + setCurrentItems(sortedItems); + } + + const requestSort = (key) => { + let direction = 'ascending'; + + if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { + direction = 'descending'; + } + + setCurrentItems(sortedItems); + setSortConfig({ key, direction }); + + return { sortedItems: currentItems }; + }; + + return { sortedItems: currentItems, currentSortConfig: sortConfig, getClassNamesFor, requestSort }; +}; + +export default useSortableData; \ No newline at end of file diff --git a/services/ui/src/components/errors/QueryError.js b/services/ui/src/components/errors/QueryError.js index 287cf814c4..49d959bf8b 100644 --- a/services/ui/src/components/errors/QueryError.js +++ b/services/ui/src/components/errors/QueryError.js @@ -1,6 +1,12 @@ import React from 'react'; -import ErrorPage from 'pages/_error'; +import ErrorPage, {ErrorNoHeader} from 'pages/_error'; -export default ({ error }) => ( +const QueryError = ({ error }) => ( ); + +export const QueryNoHeaderError = ({ error }) => ( + +); + +export default QueryError; \ No newline at end of file diff --git a/services/ui/src/components/link/Problems.js b/services/ui/src/components/link/Problems.js index bc102849fa..d4a701609e 100644 --- a/services/ui/src/components/link/Problems.js +++ b/services/ui/src/components/link/Problems.js @@ -16,7 +16,7 @@ const ProblemsLink = ({ projectSlug, children, className = null, - prefetch = false + prefetch = false, }) => { const linkData = getLinkData(environmentSlug, projectSlug); diff --git a/services/ui/src/lib/fragment/Problem.js b/services/ui/src/lib/fragment/Problem.js index fad587c399..5f8f221b5e 100644 --- a/services/ui/src/lib/fragment/Problem.js +++ b/services/ui/src/lib/fragment/Problem.js @@ -4,6 +4,10 @@ export default gql` fragment problemFields on Problem { id identifier + environment { + id + name + } data severity source diff --git a/services/ui/src/lib/query/AllProblems.js b/services/ui/src/lib/query/AllProblems.js new file mode 100644 index 0000000000..c122e10b33 --- /dev/null +++ b/services/ui/src/lib/query/AllProblems.js @@ -0,0 +1,33 @@ +import gql from 'graphql-tag'; + +export default gql` + query getAllProblemsQuery($source: [String], $project: Int, $environment: Int, $envType: [EnvType], $identifier: String, $severity: [ProblemSeverityRating]) { + problems: allProblems(source: $source , project: $project, environment: $environment, identifier: $identifier, envType: $envType, severity: $severity) { + id + identifier + environment { + id + name + environmentType + openshiftProjectName + project { + id + name + problemsUi + } + } + data + severity + source + service + created + deleted + severityScore + associatedPackage + description + version + fixedVersion + links + } + } +`; \ No newline at end of file diff --git a/services/ui/src/lib/query/AllProblemsByProject.js b/services/ui/src/lib/query/AllProblemsByProject.js new file mode 100644 index 0000000000..c8cf4bf82d --- /dev/null +++ b/services/ui/src/lib/query/AllProblemsByProject.js @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import ProblemsFragment from 'lib/fragment/Problem'; + +export default gql` + query getAllProblemsByProjectQuery($name: String!, $severity: [ProblemSeverityRating], $source: [String], $envType: EnvType) { + project: projectByName(name: $name) { + id + name + openshiftProjectName + problemsUi + environments(type: $envType) { + id + name + problems(severity: $severity, source: $source) { + ...problemFields + } + } + } + } + ${ProblemsFragment} +`; \ No newline at end of file diff --git a/services/ui/src/lib/query/AllProjectsAndEnvironments.js b/services/ui/src/lib/query/AllProjectsAndEnvironments.js new file mode 100644 index 0000000000..ea2a6f79e3 --- /dev/null +++ b/services/ui/src/lib/query/AllProjectsAndEnvironments.js @@ -0,0 +1,14 @@ +import gql from 'graphql-tag'; + +export default gql`{ + projects: allProjects { + id + name + environments(type: PRODUCTION) { + id + name + openshiftProjectName + } + } +} +`; diff --git a/services/ui/src/lib/query/AllProjectsProblems.js b/services/ui/src/lib/query/AllProjectsProblems.js new file mode 100644 index 0000000000..ad6a06d4e2 --- /dev/null +++ b/services/ui/src/lib/query/AllProjectsProblems.js @@ -0,0 +1,19 @@ +import gql from 'graphql-tag'; +import ProblemsFragment from 'lib/fragment/Problem'; + +export default gql` + query getAllProjectsProblemsQuery($severity: [ProblemSeverityRating], $source: [String], $envType: EnvType) { + projectsProblems: allProjects { + id + name + environments(type: $envType) { + id + name + problems(severity: $severity, source: $source) { + ...problemFields + } + } + } + } + ${ProblemsFragment} +`; \ No newline at end of file diff --git a/services/ui/src/lib/query/ProjectByEnvironmentId.js b/services/ui/src/lib/query/ProjectByEnvironmentId.js new file mode 100644 index 0000000000..4d376f8fbc --- /dev/null +++ b/services/ui/src/lib/query/ProjectByEnvironmentId.js @@ -0,0 +1,17 @@ +import gql from 'graphql-tag'; + +export default gql` + query getProjectByEnvironmentId($id: Int!) { + environment: environmentById(id: $id) { + id + name + project { + name + openshift { + name + } + gitUrl + } + } + } +`; diff --git a/services/ui/src/lib/withQueryErrorNoHeader.js b/services/ui/src/lib/withQueryErrorNoHeader.js new file mode 100644 index 0000000000..68667da80e --- /dev/null +++ b/services/ui/src/lib/withQueryErrorNoHeader.js @@ -0,0 +1,7 @@ +import {QueryNoHeaderError} from 'components/errors/QueryError'; +import renderWhile from 'lib/renderWhile'; + +export default renderWhile( + ({ error }) => error, + QueryNoHeaderError +); diff --git a/services/ui/src/lib/withQueryLoadingNoHeader.js b/services/ui/src/lib/withQueryLoadingNoHeader.js new file mode 100644 index 0000000000..ce1a59d9bd --- /dev/null +++ b/services/ui/src/lib/withQueryLoadingNoHeader.js @@ -0,0 +1,7 @@ +import { LoadingPageNoHeader } from 'pages/_loading'; +import renderWhile from 'lib/renderWhile'; + +export default renderWhile( + ({ loading }) => loading, + LoadingPageNoHeader +); diff --git a/services/ui/src/next.config.js b/services/ui/src/next.config.js index dabe30520f..2b93818e7d 100644 --- a/services/ui/src/next.config.js +++ b/services/ui/src/next.config.js @@ -1,7 +1,7 @@ const webpackShared = require('./webpack.shared-config'); require('dotenv-extended').load(); -const withCSS = require('@zeit/next-css') +const withCSS = require('@zeit/next-css'); const lagoonRoutes = (process.env.LAGOON_ROUTES && process.env.LAGOON_ROUTES.split(',')) || []; diff --git a/services/ui/src/pages/_error.js b/services/ui/src/pages/_error.js index 543c1ce20d..e79e208528 100644 --- a/services/ui/src/pages/_error.js +++ b/services/ui/src/pages/_error.js @@ -41,9 +41,17 @@ export default class Error extends React.Component { } } +export class ErrorNoHeader extends React.Component { + static displayName = 'ErrorNoHeader'; + + render() { + const { errorMessage } = this.props; + return (errorMessage &&

{errorMessage}

); + } +} + if (process.env.NODE_ENV !== 'production') { Error.propTypes = { - statusCode: PropTypes.number, errorMessage: PropTypes.string, }; } diff --git a/services/ui/src/pages/problems-dashboard-by-project-hex.js b/services/ui/src/pages/problems-dashboard-by-project-hex.js new file mode 100644 index 0000000000..bc91db79f3 --- /dev/null +++ b/services/ui/src/pages/problems-dashboard-by-project-hex.js @@ -0,0 +1,169 @@ +import React, {useEffect, useState} from 'react'; +import * as R from 'ramda'; +import Head from 'next/head'; +import {useQuery} from "@apollo/react-hooks"; +import AllProjectsProblemsQuery from 'lib/query/AllProjectsProblems'; +import getSeverityEnumQuery, {getProjectOptions, getSourceOptions} from 'components/Filters/helpers'; +import Honeycomb from "components/ProblemsByProject/Honeycomb"; +import MainLayout from 'layouts/MainLayout'; +import SelectFilter from 'components/Filters'; +import { bp } from 'lib/variables'; + +/** + * Displays problems page by project. + * + */ +const ProblemsDashboardProductHexPage = () => { + const [showCleanProjects, setShowCleanProjects] = useState(true); + const [source, setSource] = useState([]); + const [severity, setSeverity] = useState(['CRITICAL']); + const [envType, setEnvType] = useState('PRODUCTION'); + + const { data: severities, loading: severityLoading } = useQuery(getSeverityEnumQuery); + const { data: sources, loading: sourceLoading } = useQuery(getSourceOptions); + + const { data: projectsProblems, loading: projectsProblemsLoading} = + useQuery(AllProjectsProblemsQuery, { + variables: { + severity: severity, + source: source, + envType: envType + } + }); + + const handleEnvTypeChange = (envType) => setEnvType(envType.value); + const handleShowAllProjectsCheck = () => setShowCleanProjects(!showCleanProjects); + + const handleSourceChange = (source) => { + let values = source && source.map(s => s.value) || []; + setSource(values); + }; + + const handleSeverityChange = (severity) => { + let values = severity && severity.map(s => s.value) || []; + setSeverity(values); + }; + + const sourceOptions = (sources) => { + return sources && sources.map(s => ({ value: s, label: s})); + }; + + const severityOptions = (enums) => { + return enums && enums.map(s => ({ value: s.name, label: s.name})); + }; + + return ( + <> + + Problems Dashboard By Project + + +
+
+ + + +
+
+ + +
+
+
+
+ +
+
+ +
+ ); +}; + +export default ProblemsDashboardProductHexPage; diff --git a/services/ui/src/pages/problems-dashboard-by-project.js b/services/ui/src/pages/problems-dashboard-by-project.js new file mode 100644 index 0000000000..d78a6ab007 --- /dev/null +++ b/services/ui/src/pages/problems-dashboard-by-project.js @@ -0,0 +1,265 @@ +import React, {useEffect, useState} from 'react'; +import * as R from 'ramda'; +import Head from 'next/head'; +import { Query } from 'react-apollo'; +import {useQuery} from "@apollo/react-hooks"; +import AllProblemsByProjectQuery from 'lib/query/AllProblemsByProject'; +import getSeverityEnumQuery, {getProjectOptions, getSourceOptions} from 'components/Filters/helpers'; +import withQueryLoadingNoHeader from 'lib/withQueryLoadingNoHeader'; +import withQueryErrorNoHeader from 'lib/withQueryErrorNoHeader'; +import ProblemsByProject from "components/ProblemsByProject"; +import Accordion from "components/Accordion"; +import MainLayout from 'layouts/MainLayout'; +import SelectFilter from 'components/Filters'; +import { bp } from 'lib/variables'; + +/** + * Displays the problems overview page by project. + */ +const ProblemsDashboardProductPage = () => { + const [projectSelect, setProjectSelect] = useState([]); + const [source, setSource] = useState([]); + const [severity, setSeverity] = useState(['CRITICAL']); + const [envType, setEnvType] = useState('PRODUCTION'); + + const { data: projects, loading: projectsLoading } = useQuery(getProjectOptions); + const { data: severities, loading: severityLoading } = useQuery(getSeverityEnumQuery); + const { data: sources, loading: sourceLoading } = useQuery(getSourceOptions); + + const handleProjectChange = (project) => { + let values = project && project.map(p => p.value) || []; + setProjectSelect(values); + }; + + const handleEnvTypeChange = (envType) => { + setEnvType(envType.value); + }; + + const handleSourceChange = (source) => { + let values = source && source.map(s => s.value) || []; + setSource(values); + }; + + const handleSeverityChange = (severity) => { + let values = severity && severity.map(s => s.value) || []; + setSeverity(values); + }; + + const projectOptions = (projects) => { + return projects && projects.map(p => ({ value: p.name, label: p.name})); + }; + + const sourceOptions = (sources) => { + return sources && sources.map(s => ({ value: s, label: s})); + }; + + const severityOptions = (enums) => { + return enums && enums.map(s => ({ value: s.name, label: s.name})); + }; + + return ( + <> + + Problems Dashboard By Project + + +
+
+ + +
+
+ + +
+ +
+
+ {projects && +
+
+ +
+
+ } +
+ {projects && projects.allProjects.map(project => { + const filterProjectSelect = projectSelect.filter(s => { + return s.includes(project.name); + }).toString() || ''; + + return ( + + {R.compose( + withQueryLoadingNoHeader, + withQueryErrorNoHeader + )(({data: { project }}) => { + const {environments, id, name} = project || []; + const filterProblems = environments && environments.filter(e => e instanceof Object).map(e => { + return e.problems; + }); + + const problemsPerProject = Array.prototype.concat.apply([], filterProblems); + const critical = problemsPerProject.filter(p => p.severity === 'CRITICAL').length; + const high = problemsPerProject.filter(p => p.severity === 'HIGH').length; + const medium = problemsPerProject.filter(p => p.severity === 'MEDIUM').length; + const low = problemsPerProject.filter(p => p.severity === 'LOW').length; + + const columns = {name, problemCount: problemsPerProject.length}; + + return ( + <> + {environments && +
+
+ + {!environments.length &&
No Environments
} +
+
    +
  • {Object.keys(problemsPerProject).length} Problems
  • +
  • {critical}
  • +
  • {high}
  • +
  • {medium}
  • +
  • {low}
  • +
+
+ {environments.map(environment => ( +
+ + +
+ ))} +
+
+
+ } + ); + })}
+ )})} +
+ +
+
+ ); +}; + +export default ProblemsDashboardProductPage; diff --git a/services/ui/src/pages/problems-dashboard.js b/services/ui/src/pages/problems-dashboard.js new file mode 100644 index 0000000000..e3df2e9e9c --- /dev/null +++ b/services/ui/src/pages/problems-dashboard.js @@ -0,0 +1,190 @@ +import React, {useState} from 'react'; +import * as R from 'ramda'; +import Head from 'next/head'; +import { Query } from 'react-apollo'; +import {useQuery} from "@apollo/react-hooks"; +import AllProblemsQuery from 'lib/query/AllProblems'; +import getSeverityEnumQuery, {getSourceOptions} from 'components/Filters/helpers'; +import withQueryLoadingNoHeader from 'lib/withQueryLoadingNoHeader'; +import withQueryErrorNoHeader from 'lib/withQueryErrorNoHeader'; +import ProblemsByIdentifier from "components/ProblemsByIdentifier"; +import MainLayout from 'layouts/MainLayout'; +import SelectFilter from 'components/Filters'; +import { bp } from 'lib/variables'; + +/** + * Displays the problems overview page. + * + */ +const ProblemsDashboardPage = () => { + const [source, setSource] = useState([]); + const [severity, setSeverity] = useState(['CRITICAL']); + const [envType, setEnvType] = useState('PRODUCTION'); + + const { data: severities, loading: severityLoading } = useQuery(getSeverityEnumQuery); + const { data: sources, loading: sourceLoading } = useQuery(getSourceOptions); + + const handleEnvTypeChange = (envType) => setEnvType(envType.value); + + const handleSourceChange = (source) => { + let values = source && source.map(s => s.value) || []; + setSource(values); + }; + + const handleSeverityChange = (severity) => { + let values = severity && severity.map(s => s.value) || []; + setSeverity(values); + }; + + const sourceOptions = (sources) => { + return sources && sources.map(s => ({ value: s, label: s})); + }; + + const severityOptions = (enums) => { + return enums && enums.map(s => ({ value: s.name, label: s.name})); + }; + + const groupByProblemIdentifier = (problems) => problems.reduce((arr, problem) => { + arr[problem.identifier] = arr[problem.identifier] || []; + arr[problem.identifier].push(problem); + return arr; + }, {}); + + + return ( + <> + + Problems Dashboard + + +
+

Problems Dashboard By Identifier

+
+ + + +
+ +
+ + {R.compose( + withQueryLoadingNoHeader, + withQueryErrorNoHeader + )(({data: {problems} }) => { + + // Group problems by identifier + const problemsById = groupByProblemIdentifier(problems); + const problemIdentifiers = Object.keys(problemsById).map(p => { + const problem = problemsById[p][0]; + + return {identifier: p, source: problem.source, severity: problem.severity, problems: problemsById[p]}; + }, []); + + const critical = problems.filter(p => p.severity === 'CRITICAL').length; + const high = problems.filter(p => p.severity === 'HIGH').length; + const medium = problems.filter(p => p.severity === 'MEDIUM').length; + const low = problems.filter(p => p.severity === 'LOW').length; + + return ( + <> +
+
+
+
    +
  • {problems && Object.keys(problems).length} Problems
  • +
  • {critical}
  • +
  • {high}
  • +
  • {medium}
  • +
  • {low}
  • +
+
    +
  • {envType.charAt(0).toUpperCase() + envType.slice(1).toLowerCase()} environments
  • +
+
+ +
+ +
+ ); + })} +
+
+ ); +}; + +export default ProblemsDashboardPage; diff --git a/services/ui/src/webpack.shared-config.js b/services/ui/src/webpack.shared-config.js index f26992da42..07aa81c127 100644 --- a/services/ui/src/webpack.shared-config.js +++ b/services/ui/src/webpack.shared-config.js @@ -1,6 +1,7 @@ const path = require('path'); module.exports = { + extensions: ['.ts', '.tsx', '.js'], alias: { components: path.join(__dirname, 'components'), layouts: path.join(__dirname, 'layouts'), diff --git a/services/ui/tsconfig.json b/services/ui/tsconfig.json new file mode 100644 index 0000000000..d610fbcaaa --- /dev/null +++ b/services/ui/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build/lib", + "module": "commonjs", + "target": "es5", + "lib": ["es5", "es6", "es7", "es2017", "dom"], + "sourceMap": true, + "allowJs": false, + "jsx": "react", + "moduleResolution": "node", + "rootDirs": ["src", "stories"], + "baseUrl": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": true, + "declaration": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "exclude": ["node_modules", "build", "scripts", "../api"], + "include": ["./src"], + "references": [ + { "path": "../../node-packages/commons" } + ] +} diff --git a/services/webhooks2tasks/src/handlers/problems/processHarborVulnerabilityList.ts b/services/webhooks2tasks/src/handlers/problems/processHarborVulnerabilityList.ts index 930594b9a6..89c368612e 100644 --- a/services/webhooks2tasks/src/handlers/problems/processHarborVulnerabilityList.ts +++ b/services/webhooks2tasks/src/handlers/problems/processHarborVulnerabilityList.ts @@ -90,4 +90,4 @@ export async function processHarborVulnerabilityList( ); }); } - } \ No newline at end of file + } diff --git a/services/webhooks2tasks/src/webhooks/problems.ts b/services/webhooks2tasks/src/webhooks/problems.ts index 458754a916..b0bc613de1 100644 --- a/services/webhooks2tasks/src/webhooks/problems.ts +++ b/services/webhooks2tasks/src/webhooks/problems.ts @@ -1,6 +1,5 @@ // @flow - import uuid4 from 'uuid4'; import { logger } from '@lagoon/commons/dist/local-logging'; import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; @@ -8,13 +7,11 @@ import { harborScanningCompleted } from '../handlers/problems/harborScanningComp import { processHarborVulnerabilityList } from '../handlers/problems/processHarborVulnerabilityList'; import { processDrutinyResultset } from '../handlers/problems/processDrutinyResults'; - import { WebhookRequestData, Project } from '../types'; - export async function processProblems( rabbitMsg, channelWrapperWebhooks @@ -73,4 +70,4 @@ async function unhandled(webhook: WebhookRequestData, fullEvent: string) { `Unhandled webhook ${fullEvent}` ); return; -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 54eb915f12..bbc6732d59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -65,6 +65,13 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" + integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw== + dependencies: + "@babel/highlight" "^7.10.1" + "@babel/core@7.1.2": version "7.1.2" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.1.2.tgz#f8d2a9ceb6832887329a7b60f9d035791400ba4e" @@ -146,6 +153,16 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/generator@^7.10.1": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.2.tgz#0fa5b5b2389db8bfdfcc3492b551ee20f5dd69a9" + integrity sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA== + dependencies: + "@babel/types" "^7.10.2" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/generator@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369" @@ -220,6 +237,18 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helper-create-class-features-plugin@^7.10.1": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.2.tgz#7474295770f217dbcf288bf7572eb213db46ee67" + integrity sha512-5C/QhkGFh1vqcziq1vAL6SI9ymzUp8BCYjFpvYVhWP4DlATIb3u5q3iUd35mvlyGs8fO7hckkW7i0tmH+5+bvQ== + dependencies: + "@babel/helper-function-name" "^7.10.1" + "@babel/helper-member-expression-to-functions" "^7.10.1" + "@babel/helper-optimise-call-expression" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/helper-create-class-features-plugin@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.7.4.tgz#fce60939fd50618610942320a8d951b3b639da2d" @@ -292,6 +321,15 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-function-name@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz#92bd63829bfc9215aca9d9defa85f56b539454f4" + integrity sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.1" + "@babel/template" "^7.10.1" + "@babel/types" "^7.10.1" + "@babel/helper-function-name@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e" @@ -315,6 +353,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-get-function-arity@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz#7303390a81ba7cb59613895a192b93850e373f7d" + integrity sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw== + dependencies: + "@babel/types" "^7.10.1" + "@babel/helper-get-function-arity@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0" @@ -343,6 +388,13 @@ dependencies: "@babel/types" "^7.5.5" +"@babel/helper-member-expression-to-functions@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz#432967fd7e12a4afef66c4687d4ca22bc0456f15" + integrity sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g== + dependencies: + "@babel/types" "^7.10.1" + "@babel/helper-member-expression-to-functions@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.4.tgz#356438e2569df7321a8326644d4b790d2122cb74" @@ -395,6 +447,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-optimise-call-expression@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz#b4a1f2561870ce1247ceddb02a3860fa96d72543" + integrity sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg== + dependencies: + "@babel/types" "^7.10.1" + "@babel/helper-optimise-call-expression@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz#034af31370d2995242aa4df402c3b7794b2dcdf2" @@ -407,6 +466,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== +"@babel/helper-plugin-utils@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz#ec5a5cf0eec925b66c60580328b122c01230a127" + integrity sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA== + "@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351" @@ -446,6 +510,16 @@ "@babel/traverse" "^7.5.5" "@babel/types" "^7.5.5" +"@babel/helper-replace-supers@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz#ec6859d20c5d8087f6a2dc4e014db7228975f13d" + integrity sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.1" + "@babel/helper-optimise-call-expression" "^7.10.1" + "@babel/traverse" "^7.10.1" + "@babel/types" "^7.10.1" + "@babel/helper-replace-supers@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.7.4.tgz#3c881a6a6a7571275a72d82e6107126ec9e2cdd2" @@ -479,6 +553,13 @@ dependencies: "@babel/types" "7.0.0-beta.44" +"@babel/helper-split-export-declaration@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" + integrity sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g== + dependencies: + "@babel/types" "^7.10.1" + "@babel/helper-split-export-declaration@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" @@ -493,6 +574,11 @@ dependencies: "@babel/types" "^7.7.4" +"@babel/helper-validator-identifier@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5" + integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw== + "@babel/helper-wrap-function@^7.1.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" @@ -549,11 +635,25 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/highlight@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0" + integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg== + dependencies: + "@babel/helper-validator-identifier" "^7.10.1" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.1.2", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.6.2": version "7.6.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1" integrity sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg== +"@babel/parser@^7.10.1": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.2.tgz#871807f10442b92ff97e4783b9b54f6a0ca812d0" + integrity sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ== + "@babel/parser@^7.2.3", "@babel/parser@^7.4.2", "@babel/parser@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.4.tgz#75ab2d7110c2cf2fa949959afb05fa346d2231bb" @@ -790,6 +890,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-typescript@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.1.tgz#5e82bc27bb4202b93b949b029e699db536733810" + integrity sha512-X/d8glkrAtra7CaQGMiGs/OGa6XgUzqPcBXCIGFCpCqnfGlT0Wfbzo/B89xHhnInTaItPK8LALblVXcUOEh95Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/plugin-transform-arrow-functions@^7.0.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" @@ -1334,6 +1441,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-typescript@^7.10.1", "@babel/plugin-transform-typescript@^7.3.2": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.1.tgz#2c54daea231f602468686d9faa76f182a94507a6" + integrity sha512-v+QWKlmCnsaimLeqq9vyCsVRMViZG1k2SZTlcZvB+TqyH570Zsij8nvVUZzOASCRiQFUxkLrn9Wg/kH0zgy5OQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/plugin-syntax-typescript" "^7.10.1" + "@babel/plugin-transform-unicode-regex@^7.0.0": version "7.6.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.6.2.tgz#b692aad888a7e8d8b1b214be6b9dc03d5031f698" @@ -1485,6 +1601,14 @@ "@babel/plugin-transform-react-jsx-self" "^7.7.4" "@babel/plugin-transform-react-jsx-source" "^7.7.4" +"@babel/preset-typescript@^7.3.3", "@babel/preset-typescript@^7.8.3": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.10.1.tgz#a8d8d9035f55b7d99a2461a0bdc506582914d07e" + integrity sha512-m6GV3y1ShiqxnyQj10600ZVOFrSSAa8HQ3qIUk2r+gcGtHTIRw0dJnFLt1WNXpKjtVw7yw1DAPU/6ma2ZvgJuA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/plugin-transform-typescript" "^7.10.1" + "@babel/runtime-corejs2@7.1.2": version "7.1.2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.1.2.tgz#8695811a3fd8091f54f274b9320334e5e8c62200" @@ -1514,6 +1638,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.8.7": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.1.tgz#b6eb75cac279588d3100baecd1b9894ea2840822" + integrity sha512-nQbbCbQc9u/rpg1XCxoMYQTbSMVZjCDxErQ1ClCn9Pvcmv1lGads19ep0a2VsEiIJeHqjZley6EQGEC3Yo1xMA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@7.0.0-beta.44": version "7.0.0-beta.44" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" @@ -1533,6 +1664,15 @@ "@babel/parser" "^7.6.0" "@babel/types" "^7.6.0" +"@babel/template@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811" + integrity sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig== + dependencies: + "@babel/code-frame" "^7.10.1" + "@babel/parser" "^7.10.1" + "@babel/types" "^7.10.1" + "@babel/template@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" @@ -1573,6 +1713,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.1.tgz#bbcef3031e4152a6c0b50147f4958df54ca0dd27" + integrity sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ== + dependencies: + "@babel/code-frame" "^7.10.1" + "@babel/generator" "^7.10.1" + "@babel/helper-function-name" "^7.10.1" + "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/parser" "^7.10.1" + "@babel/types" "^7.10.1" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/traverse@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558" @@ -1606,6 +1761,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.10.1", "@babel/types@^7.10.2": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.2.tgz#30283be31cad0dbf6fb00bd40641ca0ea675172d" + integrity sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng== + dependencies: + "@babel/helper-validator-identifier" "^7.10.1" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@babel/types@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" @@ -1643,18 +1807,6 @@ resolved "https://registry.yarnpkg.com/@egoist/vue-to-react/-/vue-to-react-1.1.0.tgz#83c884b8608e8ee62e76c03e91ce9c26063a91ad" integrity sha512-MwfwXHDh6ptZGLEtNLPXp2Wghteav7mzpT2Mcwl3NZWKF814i5hhHnNkVrcQQEuxUroSWQqzxLkMKSb+nhPang== -"@emotion/babel-utils@^0.6.4": - version "0.6.10" - resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" - integrity sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow== - dependencies: - "@emotion/hash" "^0.6.6" - "@emotion/memoize" "^0.6.6" - "@emotion/serialize" "^0.9.1" - convert-source-map "^1.5.1" - find-root "^1.1.0" - source-map "^0.7.2" - "@emotion/cache@^10.0.17", "@emotion/cache@^10.0.9": version "10.0.19" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.19.tgz#d258d94d9c707dcadaf1558def968b86bb87ad71" @@ -1691,11 +1843,6 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.3.tgz#a166882c81c0c6040975dd30df24fae8549bd96f" integrity sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw== -"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" - integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ== - "@emotion/is-prop-valid@0.8.5": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.5.tgz#2dda0791f0eafa12b7a0a5b39858405cc7bde983" @@ -1708,11 +1855,6 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78" integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow== -"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" - integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ== - "@emotion/serialize@^0.11.12", "@emotion/serialize@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.14.tgz#56a6d8d04d837cc5b0126788b2134c51353c6488" @@ -1724,16 +1866,6 @@ "@emotion/utils" "0.11.2" csstype "^2.5.7" -"@emotion/serialize@^0.9.1": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" - integrity sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ== - dependencies: - "@emotion/hash" "^0.6.6" - "@emotion/memoize" "^0.6.6" - "@emotion/unitless" "^0.6.7" - "@emotion/utils" "^0.8.2" - "@emotion/sheet@0.9.3": version "0.9.3" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a" @@ -1762,31 +1894,16 @@ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.4.tgz#6c51afdf1dd0d73666ba09d2eb6c25c220d6fe4c" integrity sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ== -"@emotion/stylis@^0.7.0": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" - integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ== - "@emotion/unitless@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.4.tgz#a87b4b04e5ae14a88d48ebef15015f6b7d1f5677" integrity sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ== -"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": - version "0.6.7" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" - integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg== - "@emotion/utils@0.11.2": version "0.11.2" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183" integrity sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA== -"@emotion/utils@^0.8.2": - version "0.8.2" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" - integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== - "@emotion/weak-memoize@0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" @@ -2496,6 +2613,18 @@ pretty-hrtime "^1.0.3" regenerator-runtime "^0.13.3" +"@storybook/node-logger@^5.3.17": + version "5.3.19" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.3.19.tgz#c414e4d3781aeb06298715220012f552a36dff29" + integrity sha512-hKshig/u5Nj9fWy0OsyU04yqCxr0A9pydOHIassr4fpLAaePIN2YvqCqE2V+TxQHjZUnowSSIhbXrGt0DI5q2A== + dependencies: + "@types/npmlog" "^4.1.2" + chalk "^3.0.0" + core-js "^3.0.1" + npmlog "^4.1.2" + pretty-hrtime "^1.0.3" + regenerator-runtime "^0.13.3" + "@storybook/postinstall@5.3.0-beta.16": version "5.3.0-beta.16" resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-5.3.0-beta.16.tgz#e679e22147b87f43ee6343a2bd51ea7538410eab" @@ -2503,6 +2632,17 @@ dependencies: core-js "^3.0.1" +"@storybook/preset-typescript@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@storybook/preset-typescript/-/preset-typescript-3.0.0.tgz#e157baf6f2c4982c3da3328f5e1a640b3d7db9e4" + integrity sha512-tEbFWg5h/8SPfSCNXPxyqY418704K14q5H/xb9t0ARMXK3kZPTkKqKvdTvYg3UEKBBYbc+GA57UWaL+9b+DbDg== + dependencies: + "@babel/preset-typescript" "^7.8.3" + "@storybook/node-logger" "^5.3.17" + "@types/babel__core" "^7.1.6" + babel-preset-typescript-vue "^1.0.3" + fork-ts-checker-webpack-plugin "^4.1.0" + "@storybook/react@^5.3.0-beta.16": version "5.3.0-beta.16" resolved "https://registry.yarnpkg.com/@storybook/react/-/react-5.3.0-beta.16.tgz#2ddd01eda9b8fb214fd24e6e05554b96d12fc8e8" @@ -2772,6 +2912,17 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" +"@types/babel__core@^7.1.6": + version "7.1.8" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.8.tgz#057f725aca3641f49fc11c7a87a9de5ec588a5d7" + integrity sha512-KXBiQG2OXvaPWFPDS1rD8yV9vO0OuWIqAEqLsbfX0oU2REN5KuoMnZ1gClWcBhO5I3n6oTVAmrMufOvRqdmFTQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__generator@*": version "7.6.0" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.0.tgz#f1ec1c104d1bb463556ecb724018ab788d0c172a" @@ -3066,6 +3217,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.15.tgz#bfff4e23e9e70be6eec450419d51e18de1daf8e7" integrity sha512-daFGV9GSs6USfPgxceDA8nlSe48XrVCJfDeYm7eokxq/ye7iuOH87hKXgMtEAVLFapkczbZsx868PMDT1Y0a6A== +"@types/npmlog@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.2.tgz#d070fe6a6b78755d1092a3dc492d34c3d8f871c4" + integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA== + "@types/p-cancelable@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-1.0.1.tgz#4f0ce8aa3ee0007c2768b9b3e6e22af20a6eecbd" @@ -4624,24 +4780,6 @@ babel-plugin-emotion@^10.0.20, babel-plugin-emotion@^10.0.22, babel-plugin-emoti find-root "^1.1.0" source-map "^0.5.7" -babel-plugin-emotion@^9.2.11: - version "9.2.11" - resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" - integrity sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@emotion/babel-utils" "^0.6.4" - "@emotion/hash" "^0.6.2" - "@emotion/memoize" "^0.6.1" - "@emotion/stylis" "^0.7.0" - babel-plugin-macros "^2.0.0" - babel-plugin-syntax-jsx "^6.18.0" - convert-source-map "^1.5.0" - find-root "^1.1.0" - mkdirp "^0.5.1" - source-map "^0.5.7" - touch "^2.0.1" - babel-plugin-extract-import-names@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.5.1.tgz#79fb8550e3e0a9e8654f9461ccade56c9a669a74" @@ -4890,6 +5028,16 @@ babel-preset-jest@^24.9.0: babel-plugin-transform-undefined-to-void "^6.9.4" lodash "^4.17.11" +babel-preset-typescript-vue@^1.0.3: + version "1.1.1" + resolved "https://registry.yarnpkg.com/babel-preset-typescript-vue/-/babel-preset-typescript-vue-1.1.1.tgz#6a617dcb0ee26f911735d5f2bbe530286b2c7c02" + integrity sha512-wXeR7Y4xCsRUEdm4t4qlpv4wnxolS6jU0c7P2E6zJRWeG1sR0e6NL7DRN0tNuUwkUt0PU8bqVo4vzoA2VEuxnw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.3.2" + "@babel/preset-typescript" "^7.3.3" + vue-template-compiler "^2.6.11" + babel-runtime@6.x.x, babel-runtime@^6.23.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" @@ -6045,7 +6193,7 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@1.6.0, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1: +convert-source-map@1.6.0, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== @@ -6189,19 +6337,6 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-emotion@^9.2.12: - version "9.2.12" - resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f" - integrity sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA== - dependencies: - "@emotion/hash" "^0.6.2" - "@emotion/memoize" "^0.6.1" - "@emotion/stylis" "^0.7.0" - "@emotion/unitless" "^0.6.2" - csstype "^2.5.2" - stylis "^3.5.0" - stylis-rule-sheet "^0.0.10" - create-error-class@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" @@ -6432,10 +6567,10 @@ csstype@^2.2.0, csstype@^2.5.7: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== -csstype@^2.5.2: - version "2.6.6" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" - integrity sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg== +csstype@^2.6.7: + version "2.6.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" + integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== currently-unhandled@^0.4.1: version "0.4.1" @@ -7049,6 +7184,14 @@ dom-helpers@^3.4.0: dependencies: "@babel/runtime" "^7.1.2" +dom-helpers@^5.0.1: + version "5.1.4" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" + integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^2.6.7" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -7284,14 +7427,6 @@ emotion-theming@^10.0.19: "@emotion/weak-memoize" "0.2.4" hoist-non-react-statics "^3.3.0" -emotion@^9.1.2: - version "9.2.12" - resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9" - integrity sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ== - dependencies: - babel-plugin-emotion "^9.2.11" - create-emotion "^9.2.12" - encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -7311,6 +7446,15 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +enhanced-resolve@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" + integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + enhanced-resolve@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" @@ -8196,6 +8340,19 @@ fork-ts-checker-webpack-plugin@1.5.0: tapable "^1.0.0" worker-rpc "^0.1.0" +fork-ts-checker-webpack-plugin@^4.1.0: + version "4.1.6" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz#5055c703febcf37fa06405d400c122b905167fc5" + integrity sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw== + dependencies: + "@babel/code-frame" "^7.5.5" + chalk "^2.4.1" + micromatch "^3.1.10" + minimatch "^3.0.4" + semver "^5.6.0" + tapable "^1.0.0" + worker-rpc "^0.1.0" + form-data@*, form-data@^2.3.3, form-data@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" @@ -11358,6 +11515,14 @@ memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: errno "^0.1.3" readable-stream "^2.0.1" +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + meow@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/meow/-/meow-4.0.1.tgz#d48598f6f4b1472f35bf6317a95945ace347f975" @@ -11423,7 +11588,7 @@ microevent.ts@~0.1.1: resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== -micromatch@4.x, micromatch@^4.0.2: +micromatch@4.x, micromatch@^4.0.0, micromatch@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== @@ -13824,13 +13989,6 @@ rabbitmq-pub-sub@^0.2.5: "@types/bunyan" "0.0.35" amqplib "^0.5.1" -raf@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - ramda@^0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" @@ -14053,6 +14211,13 @@ react-helmet-async@^1.0.2: react-fast-compare "^2.0.4" shallowequal "^1.1.0" +react-hexgrid@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/react-hexgrid/-/react-hexgrid-1.0.3.tgz#fe0749eaa61d159805c913c8fb78502ce71e247a" + integrity sha512-+rXia1Q/tGtEWrRRNGku9dZx+3yVcotr4cs3IF0PddJaaddpxUEMgZDqSway0QGNTT43eVbclR5UScwHBgnx2A== + dependencies: + classnames "^2.2.5" + react-highlight-words@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.14.0.tgz#a1a40ff0a49ce78e7feb375a4e0a5fd1ca9c9609" @@ -14069,13 +14234,6 @@ react-hotkeys@2.0.0: dependencies: prop-types "^15.6.1" -react-input-autosize@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" - integrity sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA== - dependencies: - prop-types "^15.5.8" - react-input-autosize@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2" @@ -14167,18 +14325,19 @@ react-redux@^7.0.2: prop-types "^15.7.2" react-is "^16.9.0" -react-select@^2.1.1: - version "2.4.4" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.4.4.tgz#ba72468ef1060c7d46fbb862b0748f96491f1f73" - integrity sha512-C4QPLgy9h42J/KkdrpVxNmkY6p4lb49fsrbDk/hRcZpX7JvZPNb6mGj+c5SzyEtBv1DmQ9oPH4NmhAFvCrg8Jw== +react-select@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.0.tgz#ab098720b2e9fe275047c993f0d0caf5ded17c27" + integrity sha512-wBFVblBH1iuCBprtpyGtd1dGMadsG36W5/t2Aj8OE6WbByDg5jIFyT7X5gT+l0qmT5TqWhxX+VsKJvCEl2uL9g== dependencies: - classnames "^2.2.5" - emotion "^9.1.2" + "@babel/runtime" "^7.4.4" + "@emotion/cache" "^10.0.9" + "@emotion/core" "^10.0.9" + "@emotion/css" "^10.0.9" memoize-one "^5.0.0" prop-types "^15.6.0" - raf "^3.4.0" - react-input-autosize "^2.2.1" - react-transition-group "^2.2.1" + react-input-autosize "^2.2.2" + react-transition-group "^4.3.0" react-select@^3.0.8: version "3.0.8" @@ -14240,6 +14399,16 @@ react-transition-group@^2.2.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" +react-transition-group@^4.3.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" + integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-typekit@^1.1.3: version "1.1.4" resolved "https://registry.yarnpkg.com/react-typekit/-/react-typekit-1.1.4.tgz#1305675bd8d348eeafc53f013edf1ec164b01f73" @@ -14499,6 +14668,11 @@ regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" @@ -15173,7 +15347,17 @@ serialised-error@1.1.3: stack-trace "0.0.9" uuid "^3.0.0" -serialize-javascript@1.6.1, serialize-javascript@^1.7.0, serialize-javascript@^2.1.0, serialize-javascript@^2.1.1: +serialize-javascript@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" + integrity sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw== + +serialize-javascript@^1.7.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== + +serialize-javascript@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== @@ -15446,7 +15630,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@0.7.3, source-map@^0.7.2: +source-map@0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -15908,12 +16092,12 @@ styled-jsx@3.2.1: stylis "3.5.4" stylis-rule-sheet "0.0.10" -stylis-rule-sheet@0.0.10, stylis-rule-sheet@^0.0.10: +stylis-rule-sheet@0.0.10: version "0.0.10" resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== -stylis@3.5.4, stylis@^3.5.0: +stylis@3.5.4: version "3.5.4" resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== @@ -16323,13 +16507,6 @@ token-stream@0.0.1: resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a" integrity sha1-zu78cXp2xDFvEm0LnbqlXX598Bo= -touch@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" - integrity sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A== - dependencies: - nopt "~1.0.10" - touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" @@ -16464,6 +16641,17 @@ ts-jest@^26.0.0: semver "7.x" yargs-parser "18.x" +ts-loader@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-7.0.5.tgz#789338fb01cb5dc0a33c54e50558b34a73c9c4c5" + integrity sha512-zXypEIT6k3oTc+OZNx/cqElrsbBtYqDknf48OZos0NQ3RTt045fBIU8RRSu+suObBzYB355aIPGOe/3kj9h7Ig== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + ts-map@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/ts-map/-/ts-map-1.0.3.tgz#1c4d218dec813d2103b7e04e4bcf348e1471c1ff" @@ -17135,6 +17323,14 @@ vue-template-compiler@^2.0.0: de-indent "^1.0.2" he "^1.1.0" +vue-template-compiler@^2.6.11: + version "2.6.11" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080" + integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA== + dependencies: + de-indent "^1.0.2" + he "^1.1.0" + w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" From d52f5bdff329c3913e0865b657393a14fc7d5f54 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Tue, 23 Jun 2020 08:19:05 -0400 Subject: [PATCH 157/280] #1987 hotfix: correct escaping of slashes --- images/solr/20-solr-datadir.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/solr/20-solr-datadir.sh b/images/solr/20-solr-datadir.sh index a325aa6750..d7c1f486cc 100755 --- a/images/solr/20-solr-datadir.sh +++ b/images/solr/20-solr-datadir.sh @@ -97,7 +97,7 @@ function fixConfig { echo "Found old non lagoon compatible dataDir config in solrconfig.xml:" cat $1/solrconfig.xml | grep dataDir SOLR_DATA_DIR=${SOLR_DATA_DIR:-/var/solr} - SOLR_DATA_DIR_ESCAPED=${SOLR_DATA_DIR////\\/} # escapig the forward slashes with backslahes + SOLR_DATA_DIR_ESCAPED=${SOLR_DATA_DIR//\//\\/} # escapig the forward slashes with backslahes if [ -w $1/ ]; then sed -ibak "s/.*/$SOLR_DATA_DIR_ESCAPED\/\${solr.core.name}<\/dataDir>/" $1/solrconfig.xml echo "automagically updated to compatible config: " From 5f6bcf698a721d5d93240076d49e27f90ffbf70e Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Tue, 23 Jun 2020 18:17:20 +0200 Subject: [PATCH 158/280] Fix notification of error message to rocketchat/slack --- helpers/check_acme_routes.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/helpers/check_acme_routes.sh b/helpers/check_acme_routes.sh index c37bca77eb..476a07390f 100755 --- a/helpers/check_acme_routes.sh +++ b/helpers/check_acme_routes.sh @@ -161,6 +161,7 @@ function check_routes() { if [[ $DRYRUN = true ]]; then echo -e "DRYRUN oc delete -n $ROUTE_NAMESPACE route $OCROUTE_NAME" else + echo -e "\nDelete route $OCROUTE_NAME" oc delete -n "$ROUTE_NAMESPACE" route "$OCROUTE_NAME" fi done @@ -172,13 +173,13 @@ function check_routes() { # Function to update route's annotation (ie: update tls-amce, remove tls-acme-awaiting-* and set a new one for internal purpose) function update_annotation() { echo "Update route's annotations" - OCOPTIONS="" + OCOPTIONS="--overwrite" if [[ "$DRYRUN" = "true" ]]; then - OCOPTIONS="--dry-run" + OCOPTIONS="--dry-run --overwrite" fi # Annotate the route - oc annotate -n "$2" "$OCOPTIONS" --overwrite route "$1" acme.openshift.io/status- kubernetes.io/tls-acme-awaiting-authorization-owner- kubernetes.io/tls-acme-awaiting-authorization-at-url- kubernetes.io/tls-acme="false" amazee.io/administratively-disabled="$(date +%s)" + oc annotate -n "$2" $OCOPTIONS route "$1" acme.openshift.io/status- kubernetes.io/tls-acme-awaiting-authorization-owner- kubernetes.io/tls-acme-awaiting-authorization-at-url- kubernetes.io/tls-acme="false" amazee.io/administratively-disabled="$(date +%s)" } @@ -199,15 +200,15 @@ function notify_customer() { WEBHOOK=$(echo "$NOTIFICATION_DATA"|cut -f2 -d ";") MESSAGE="Your $ROUTE_HOSTNAME route is configured in the \`.lagoon.yml\` file to issue an TLS certificate from Lets Encrypt. Unfortunately Lagoon is unable to issue a certificate as $DNS_ERROR.\nTo be issued correctly, the DNS records for $ROUTE_HOSTNAME should point to $CLUSTER_HOSTNAME with an CNAME record (preferred) or to ${CLUSTER_IPS[*]} via an A record (also possible but not preferred).\nIf you don'\''t need the SSL certificate or you are using a CDN that provides you with an TLS certificate, please update your .lagoon.yml file by setting the tls-acme parameter to false for $ROUTE_HOSTNAME, as described here: https://lagoon.readthedocs.io/en/latest/using_lagoon/lagoon_yml/#ssl-configuration-tls-acme.\nWe have now administratively disabled the issuing of Lets Encrypt certificate for $ROUTE_HOSTNAME in order to protect the cluster, this will be reset during the next deployment, therefore we suggest to resolve this issue as soon as possible. Feel free to reach out to us for further information.\nThanks you.\namazee.io team" - # JSON payload - JSON="'{\"channel\": \"$CHANNEL\", \"text\":\"$MESSAGE\"}'" - echo "Sending message $JSON to $CHANNEL" + # json Payload + JSON=\'"{\"channel\": \"$CHANNEL\", \"text\": \"${MESSAGE}\"}"\' + echo -e "Sending notification into ${CHANNEL}" # Execute curl to send message into the channel if [[ $DRYRUN = true ]]; then echo "DRYRUN on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data "$JSON" "$WEBHOOK"" else - curl -X POST -H 'Content-type: application/json' --data "$JSON" "$WEBHOOK" + eval "curl -X POST -H 'Content-type: application/json' --data "${JSON}" ${WEBHOOK}" fi } @@ -258,4 +259,4 @@ function main() { } initial_checks "$COMMAND" -main "$COMMAND" \ No newline at end of file +main "$COMMAND" From 244e481135df9ed4900cbbf28a1b90ef059f2958 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Tue, 23 Jun 2020 13:35:01 -0400 Subject: [PATCH 159/280] Brainfuck - v1.7.1 --- docker-compose.yaml | 90 +++++++++++++++---------------- lagoon-remote/docker-compose.yaml | 20 +++---- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 7642ea38db..db90b7d98b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,7 @@ services: labels: lagoon.type: custom lagoon.template: services/api-db/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api-db:v1-7-0 + lagoon.image: amazeeiolagoon/api-db:v1-7-1 webhook-handler: image: ${IMAGE_REPO:-lagoon}/webhook-handler command: yarn run dev @@ -22,7 +22,7 @@ services: labels: lagoon.type: custom lagoon.template: services/webhook-handler/.lagoon.app.yml - lagoon.image: amazeeiolagoon/webhook-handler:v1-7-0 + lagoon.image: amazeeiolagoon/webhook-handler:v1-7-1 backup-handler: image: ${IMAGE_REPO:-lagoon}/backup-handler restart: on-failure @@ -31,7 +31,7 @@ services: labels: lagoon.type: custom lagoon.template: services/backup-handler/.lagoon.app.yml - lagoon.image: amazeeiolagoon/backup-handler:v1-7-0 + lagoon.image: amazeeiolagoon/backup-handler:v1-7-1 depends_on: - broker broker: @@ -42,7 +42,7 @@ services: labels: lagoon.type: rabbitmq-cluster lagoon.template: services/broker/.lagoon.app.yml - lagoon.image: amazeeiolagoon/broker:v1-7-0 + lagoon.image: amazeeiolagoon/broker:v1-7-1 openshiftremove: image: ${IMAGE_REPO:-lagoon}/openshiftremove command: yarn run dev @@ -52,7 +52,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftremove/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftremove:v1-7-0 + lagoon.image: amazeeiolagoon/openshiftremove:v1-7-1 openshiftbuilddeploy: image: ${IMAGE_REPO:-lagoon}/openshiftbuilddeploy command: yarn run dev @@ -64,7 +64,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftbuilddeploy/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftbuilddeploy:v1-7-0 + lagoon.image: amazeeiolagoon/openshiftbuilddeploy:v1-7-1 openshiftbuilddeploymonitor: image: ${IMAGE_REPO:-lagoon}/openshiftbuilddeploymonitor command: yarn run dev @@ -78,7 +78,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftbuilddeploymonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftbuilddeploymonitor:v1-7-0 + lagoon.image: amazeeiolagoon/openshiftbuilddeploymonitor:v1-7-1 openshiftjobs: image: ${IMAGE_REPO:-lagoon}/openshiftjobs command: yarn run dev @@ -92,7 +92,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftjobs/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftjobs:v1-7-0 + lagoon.image: amazeeiolagoon/openshiftjobs:v1-7-1 openshiftjobsmonitor: image: ${IMAGE_REPO:-lagoon}/openshiftjobsmonitor command: yarn run dev @@ -102,7 +102,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftjobsmonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftjobsmonitor:v1-7-0 + lagoon.image: amazeeiolagoon/openshiftjobsmonitor:v1-7-1 openshiftmisc: image: ${IMAGE_REPO:-lagoon}/openshiftmisc command: yarn run dev @@ -112,7 +112,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftmisc/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftmisc:v1-7-0 + lagoon.image: amazeeiolagoon/openshiftmisc:v1-7-1 kubernetesmisc: image: ${IMAGE_REPO:-lagoon}/kubernetesmisc command: yarn run dev @@ -122,7 +122,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesmisc/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesmisc:v1-7-0 + lagoon.image: amazeeiolagoon/kubernetesmisc:v1-7-1 kubernetesbuilddeploy: image: ${IMAGE_REPO:-lagoon}/kubernetesbuilddeploy command: yarn run dev @@ -135,7 +135,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesbuilddeploy/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesbuilddeploy:v1-7-0 + lagoon.image: amazeeiolagoon/kubernetesbuilddeploy:v1-7-1 kubernetesdeployqueue: image: ${IMAGE_REPO:-lagoon}/kubernetesdeployqueue command: yarn run dev @@ -145,7 +145,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesdeployqueue/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesdeployqueue:v1-7-0 + lagoon.image: amazeeiolagoon/kubernetesdeployqueue:v1-7-1 kubernetesbuilddeploymonitor: image: ${IMAGE_REPO:-lagoon}/kubernetesbuilddeploymonitor command: yarn run dev @@ -159,7 +159,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesbuilddeploymonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesbuilddeploymonitor:v1-7-0 + lagoon.image: amazeeiolagoon/kubernetesbuilddeploymonitor:v1-7-1 kubernetesjobs: image: ${IMAGE_REPO:-lagoon}/kubernetesjobs command: yarn run dev @@ -173,7 +173,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesjobs/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesjobs:v1-7-0 + lagoon.image: amazeeiolagoon/kubernetesjobs:v1-7-1 kubernetesjobsmonitor: image: ${IMAGE_REPO:-lagoon}/kubernetesjobsmonitor command: yarn run dev @@ -187,7 +187,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesjobsmonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesjobsmonitor:v1-7-0 + lagoon.image: amazeeiolagoon/kubernetesjobsmonitor:v1-7-1 kubernetesremove: image: ${IMAGE_REPO:-lagoon}/kubernetesremove command: yarn run dev @@ -197,7 +197,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesremove/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesremove:v1-7-0 + lagoon.image: amazeeiolagoon/kubernetesremove:v1-7-1 logs2rocketchat: image: ${IMAGE_REPO:-lagoon}/logs2rocketchat command: yarn run dev @@ -207,7 +207,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2rocketchat/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2rocketchat:v1-7-0 + lagoon.image: amazeeiolagoon/logs2rocketchat:v1-7-1 logs2slack: image: ${IMAGE_REPO:-lagoon}/logs2slack command: yarn run dev @@ -217,7 +217,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2slack/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2slack:v1-7-0 + lagoon.image: amazeeiolagoon/logs2slack:v1-7-1 logs2microsoftteams: image: ${IMAGE_REPO:-lagoon}/logs2microsoftteams command: yarn run dev @@ -227,7 +227,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2microsoftteams/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2microsoftteams:v1-7-0 + lagoon.image: amazeeiolagoon/logs2microsoftteams:v1-7-1 logs2email: image: ${IMAGE_REPO:-lagoon}/logs2email command: yarn run dev @@ -237,7 +237,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2slack/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2email:v1-7-0 + lagoon.image: amazeeiolagoon/logs2email:v1-7-1 depends_on: - mailhog mailhog: @@ -255,7 +255,7 @@ services: labels: lagoon.type: custom lagoon.template: services/webhooks2tasks/.lagoon.app.yml - lagoon.image: amazeeiolagoon/webhooks2tasks:v1-7-0 + lagoon.image: amazeeiolagoon/webhooks2tasks:v1-7-1 api: image: ${IMAGE_REPO:-lagoon}/api command: yarn run dev @@ -274,7 +274,7 @@ services: labels: lagoon.type: custom lagoon.template: services/api/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api:v1-7-0 + lagoon.image: amazeeiolagoon/api:v1-7-1 ui: image: ${IMAGE_REPO:-lagoon}/ui command: yarn run dev @@ -288,7 +288,7 @@ services: labels: lagoon.type: custom lagoon.template: services/ui/.lagoon.app.yml - lagoon.image: amazeeiolagoon/ui:v1-7-0 + lagoon.image: amazeeiolagoon/ui:v1-7-1 ssh: image: ${IMAGE_REPO:-lagoon}/ssh depends_on: @@ -309,7 +309,7 @@ services: labels: lagoon.type: custom lagoon.template: services/ssh/.lagoon.app.yml - lagoon.image: amazeeiolagoon/ssh:v1-7-0 + lagoon.image: amazeeiolagoon/ssh:v1-7-1 auth-server: image: ${IMAGE_REPO:-lagoon}/auth-server command: yarn run dev @@ -323,7 +323,7 @@ services: labels: lagoon.type: custom lagoon.template: services/auth-server/.lagoon.app.yml - lagoon.image: amazeeiolagoon/auth-server:v1-7-0 + lagoon.image: amazeeiolagoon/auth-server:v1-7-1 keycloak: image: ${IMAGE_REPO:-lagoon}/keycloak user: '111111111' @@ -334,7 +334,7 @@ services: labels: lagoon.type: custom lagoon.template: services/keycloak/.lagoon.app.yml - lagoon.image: amazeeiolagoon/keycloak:v1-7-0 + lagoon.image: amazeeiolagoon/keycloak:v1-7-1 keycloak-db: image: ${IMAGE_REPO:-lagoon}/keycloak-db ports: @@ -342,7 +342,7 @@ services: labels: lagoon.type: custom lagoon.template: services/keycloak-db/.lagoon.app.yml - lagoon.image: amazeeiolagoon/keycloak-db:v1-7-0 + lagoon.image: amazeeiolagoon/keycloak-db:v1-7-1 tests-kubernetes: image: ${IMAGE_REPO:-lagoon}/tests environment: @@ -458,7 +458,7 @@ services: labels: lagoon.type: custom lagoon.template: services/drush-alias/.lagoon.app.yml - lagoon.image: amazeeiolagoon/drush-alias:v1-7-0 + lagoon.image: amazeeiolagoon/drush-alias:v1-7-1 version: '2' logs-db: image: ${IMAGE_REPO:-lagoon}/logs-db @@ -474,14 +474,14 @@ services: labels: lagoon.type: elasticsearch lagoon.template: services/logs-db/.lagoon.single.yml - lagoon.image: amazeeiolagoon/logs-db:v1-7-0 + lagoon.image: amazeeiolagoon/logs-db:v1-7-1 logs-forwarder: image: ${IMAGE_REPO:-lagoon}/logs-forwarder user: '111111111' labels: lagoon.type: custom lagoon.template: services/logs-forwarder/.lagoon.single.yml - lagoon.image: amazeeiolagoon/logs-forwarder:v1-7-0 + lagoon.image: amazeeiolagoon/logs-forwarder:v1-7-1 logs-db-ui: image: ${IMAGE_REPO:-lagoon}/logs-db-ui user: '111111111' @@ -493,14 +493,14 @@ services: labels: lagoon.type: kibana lagoon.template: services/logs-db-ui/.lagoon.yml - lagoon.image: amazeeiolagoon/logs-db-ui:v1-7-0 + lagoon.image: amazeeiolagoon/logs-db-ui:v1-7-1 logs-db-curator: image: ${IMAGE_REPO:-lagoon}/logs-db-curator user: '111111111' labels: lagoon.type: cli lagoon.template: services/logs-db-curator/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs-db-curator:v1-7-0 + lagoon.image: amazeeiolagoon/logs-db-curator:v1-7-1 logs2logs-db: image: ${IMAGE_REPO:-lagoon}/logs2logs-db user: '111111111' @@ -516,7 +516,7 @@ services: labels: lagoon.type: logstash lagoon.template: services/logs2logs-db/.lagoon.yml - lagoon.image: amazeeiolagoon/logs2logs-db:v1-7-0 + lagoon.image: amazeeiolagoon/logs2logs-db:v1-7-1 auto-idler: image: ${IMAGE_REPO:-lagoon}/auto-idler user: '111111111' @@ -529,7 +529,7 @@ services: labels: lagoon.type: custom lagoon.template: services/auto-idler/.lagoon.yml - lagoon.image: amazeeiolagoon/auto-idler:v1-7-0 + lagoon.image: amazeeiolagoon/auto-idler:v1-7-1 storage-calculator: image: ${IMAGE_REPO:-lagoon}/storage-calculator user: '111111111' @@ -538,7 +538,7 @@ services: labels: lagoon.type: custom lagoon.template: services/storage-calculator/.lagoon.yml - lagoon.image: amazeeiolagoon/storage-calculator:v1-7-0 + lagoon.image: amazeeiolagoon/storage-calculator:v1-7-1 logs-collector: image: openshift/origin-logging-fluentd:v3.6.1 labels: @@ -610,7 +610,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-core/harbor-core.yml - lagoon.image: amazeeiolagoon/harbor-core:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-core:v1-7-1 harbor-database: image: ${IMAGE_REPO:-lagoon}/harbor-database hostname: harbor-database @@ -624,7 +624,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-database/harbor-database.yml - lagoon.image: amazeeiolagoon/harbor-database:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-database:v1-7-1 harbor-jobservice: image: ${IMAGE_REPO:-lagoon}/harbor-jobservice hostname: harbor-jobservice @@ -653,7 +653,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-jobservice/harbor-jobservice.yml - lagoon.image: amazeeiolagoon/harbor-jobservice:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-jobservice:v1-7-1 harbor-nginx: image: ${IMAGE_REPO:-lagoon}/harbor-nginx hostname: harbor-nginx @@ -669,7 +669,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-nginx/harbor-nginx.yml - lagoon.image: amazeeiolagoon/harbor-nginx:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-nginx:v1-7-1 harbor-portal: image: ${IMAGE_REPO:-lagoon}/harbor-portal hostname: harbor-portal @@ -679,7 +679,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-portal/harbor-portal.yml - lagoon.image: amazeeiolagoon/harbor-portal:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-portal:v1-7-1 harbor-redis: image: ${IMAGE_REPO:-lagoon}/harbor-redis hostname: harbor-redis @@ -689,7 +689,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-redis/harbor-redis.yml - lagoon.image: amazeeiolagoon/harbor-redis:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-redis:v1-7-1 harbor-trivy: image: ${IMAGE_REPO:-lagoon}/harbor-trivy hostname: harbor-trivy @@ -721,7 +721,7 @@ services: lagoon.type: custom lagoon.template: services/harbor-trivy/harbor-trivy.yml lagoon.name: harbor-trivy - lagoon.image: amazeeiolagoon/harbor-trivy:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-trivy:v1-7-1 harborregistry: image: ${IMAGE_REPO:-lagoon}/harborregistry hostname: harborregistry @@ -743,7 +743,7 @@ services: lagoon.type: custom lagoon.template: services/harborregistry/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistry:v1-7-0 + lagoon.image: amazeeiolagoon/harborregistry:v1-7-1 harborregistryctl: image: ${IMAGE_REPO:-lagoon}/harborregistryctl hostname: harborregistryctl @@ -758,4 +758,4 @@ services: lagoon.type: custom lagoon.template: services/harborregistryctl/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-0 + lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-1 diff --git a/lagoon-remote/docker-compose.yaml b/lagoon-remote/docker-compose.yaml index 377ec52859..77ce2433fe 100644 --- a/lagoon-remote/docker-compose.yaml +++ b/lagoon-remote/docker-compose.yaml @@ -35,61 +35,61 @@ services: lagoon.type: custom lagoon.template: harborclair/harborclair.yml lagoon.name: harborclair - lagoon.image: amazeeiolagoon/harborclair:v1-7-0 + lagoon.image: amazeeiolagoon/harborclair:v1-7-1 harborclairadapter: image: ${IMAGE_REPO:-lagoon}/harborclairadapter labels: lagoon.type: custom lagoon.template: harborclairadapter/harborclair.yml lagoon.name: harborclair - lagoon.image: amazeeiolagoon/harborclairadapter:v1-7-0 + lagoon.image: amazeeiolagoon/harborclairadapter:v1-7-1 harbor-core: image: ${IMAGE_REPO:-lagoon}/harbor-core labels: lagoon.type: custom lagoon.template: harbor-core/harbor-core.yml - lagoon.image: amazeeiolagoon/harbor-core:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-core:v1-7-1 harbor-database: image: ${IMAGE_REPO:-lagoon}/harbor-database labels: lagoon.type: custom lagoon.template: harbor-database/harbor-database.yml - lagoon.image: amazeeiolagoon/harbor-database:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-database:v1-7-1 harbor-jobservice: image: ${IMAGE_REPO:-lagoon}/harbor-jobservice labels: lagoon.type: custom lagoon.template: harbor-jobservice/harbor-jobservice.yml - lagoon.image: amazeeiolagoon/harbor-jobservice:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-jobservice:v1-7-1 harbor-nginx: image: ${IMAGE_REPO:-lagoon}/harbor-nginx labels: lagoon.type: custom lagoon.template: harbor-nginx/harbor-nginx.yml - lagoon.image: amazeeiolagoon/harbor-nginx:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-nginx:v1-7-1 harbor-portal: image: ${IMAGE_REPO:-lagoon}/harbor-portal labels: lagoon.type: custom lagoon.template: harbor-portal/harbor-portal.yml - lagoon.image: amazeeiolagoon/harbor-portal:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-portal:v1-7-1 harbor-redis: image: ${IMAGE_REPO:-lagoon}/harbor-redis labels: lagoon.type: custom lagoon.template: harbor-redis/harbor-redis.yml - lagoon.image: amazeeiolagoon/harbor-redis:v1-7-0 + lagoon.image: amazeeiolagoon/harbor-redis:v1-7-1 harborregistry: image: ${IMAGE_REPO:-lagoon}/harborregistry labels: lagoon.type: custom lagoon.template: harborregistry/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistry:v1-7-0 + lagoon.image: amazeeiolagoon/harborregistry:v1-7-1 harborregistryctl: image: ${IMAGE_REPO:-lagoon}/harborregistryctl labels: lagoon.type: custom lagoon.template: harborregistryctl/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-0 + lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-1 From cbc1d5c8513edbf2171d1976ac52524d4cdd47f0 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Wed, 24 Jun 2020 10:31:18 +1200 Subject: [PATCH 160/280] Uses kubernetes namespace name to identify environment --- .../problems/harborScanningCompleted.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts index e1709c89cf..c2da3ed533 100644 --- a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts +++ b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts @@ -11,6 +11,9 @@ import { getProjectByName, getEnvironmentByName, getProblemHarborScanMatches, + getEnvironmentByOpenshiftProjectName, + sanitizeProjectName, + sanitizeGroupName, } from '@lagoon/commons/dist/api'; const HARBOR_WEBHOOK_SUCCESSFUL_SCAN = "Success"; @@ -55,14 +58,22 @@ const DEFAULT_REPO_DETAILS_MATCHER = { return; } - let vulnerabilities = await getVulnerabilitiesFromHarbor(harborScanId); + let vulnerabilities = []; + try { + vulnerabilities = await getVulnerabilitiesFromHarbor(harborScanId); + } catch(error) { + console.log(error); + throw error; + } + let { id: lagoonProjectId } = await getProjectByName(lagoonProjectName); - let { environmentByName: environmentDetails } = await getEnvironmentByName( - lagoonEnvironmentName, - lagoonProjectId - ); + + let openshiftProjectName = sanitizeProjectName(`${lagoonProjectName}-${lagoonEnvironmentName}`); + const environmentResult = await getEnvironmentByOpenshiftProjectName(openshiftProjectName); + const environmentDetails: any = R.prop('environmentByOpenshiftProjectName', environmentResult) + let messageBody = { lagoonProjectId, From df581d0de965929908e36e9e96229402fb983aec Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Wed, 24 Jun 2020 11:18:10 +1200 Subject: [PATCH 161/280] Moves healthcheck build into staged build --- images/php/fpm/Dockerfile | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/images/php/fpm/Dockerfile b/images/php/fpm/Dockerfile index b92deaaf4b..fb432394ae 100644 --- a/images/php/fpm/Dockerfile +++ b/images/php/fpm/Dockerfile @@ -3,6 +3,21 @@ ARG PHP_IMAGE_VERSION ARG ALPINE_VERSION ARG IMAGE_REPO FROM ${IMAGE_REPO:-lagoon}/commons as commons + +FROM php:${PHP_IMAGE_VERSION}-fpm-alpine${ALPINE_VERSION} as healthcheckbuilder + +# Defining Versions - Composer +# @see https://getcomposer.org/download/ +ENV COMPOSER_VERSION=1.10.7 \ + COMPOSER_HASH_SHA256=b94b872729668de5b5fbf62f16ff588d2a23480dda88c0e45cb43b721b75ae29 + +RUN curl -L -o /tmp/composer https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \ + && echo "$COMPOSER_HASH_SHA256 /tmp/composer" | sha256sum \ + && chmod +x /tmp/composer \ + && php -d memory_limit=-1 /tmp/composer create-project amazeeio/healthz-php /healthz-php \ + && rm /tmp/composer + + FROM php:${PHP_IMAGE_VERSION}-fpm-alpine${ALPINE_VERSION} LABEL maintainer="amazee.io" @@ -17,6 +32,10 @@ COPY --from=commons /bin/fix-permissions /bin/ep /bin/docker-sleep /bin/ COPY --from=commons /sbin/tini /sbin/ COPY --from=commons /home /home +# Copy healthcheck files + +COPY --from=healthcheckbuilder /healthz-php /healthz-php + RUN chmod g+w /etc/passwd \ && mkdir -p /home @@ -102,19 +121,6 @@ RUN apk add --no-cache fcgi \ && fix-permissions /app \ && fix-permissions /etc/ssmtp/ssmtp.conf - -# Defining Versions - Composer -# @see https://getcomposer.org/download/ -ENV COMPOSER_VERSION=1.10.7 \ - COMPOSER_HASH_SHA256=b94b872729668de5b5fbf62f16ff588d2a23480dda88c0e45cb43b721b75ae29 - -RUN curl -L -o /tmp/composer https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \ - && echo "$COMPOSER_HASH_SHA256 /tmp/composer" | sha256sum \ - && chmod +x /tmp/composer \ - && php -d memory_limit=-1 /tmp/composer create-project amazeeio/healthz-php /healthz-php \ - && rm /tmp/composer - - EXPOSE 9000 ENV AMAZEEIO_DB_HOST=mariadb \ From 1f7233634e5604fe230fbfe2480a8cf8a0f097f6 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 24 Jun 2020 14:05:01 +1000 Subject: [PATCH 162/280] only show the button on the standby environment, and when creating the task, add it to the standby environment tasks list as this is where the UI redirects --- .../api/src/resources/deployment/resolvers.ts | 15 ++++++++------- services/ui/src/components/Environment/index.js | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/services/api/src/resources/deployment/resolvers.ts b/services/api/src/resources/deployment/resolvers.ts index 6fb62f5bc3..3fc893555b 100644 --- a/services/api/src/resources/deployment/resolvers.ts +++ b/services/api/src/resources/deployment/resolvers.ts @@ -805,13 +805,6 @@ export const switchActiveStandby: ResolverFn = async ( project: project.id, }); - const environmentRows = await query( - sqlClient, - environmentSql.selectEnvironmentByNameAndProject(project.productionEnvironment, project.id), - ); - const environment = environmentRows[0]; - var environmentId = parseInt(environment.id); - if (project.standbyProductionEnvironment == null) { sendToLagoonLogs( 'error', @@ -824,6 +817,14 @@ export const switchActiveStandby: ResolverFn = async ( return `Error: no standbyProductionEnvironment configured`; } + // we want the task to show in the standby environment, as this is where the task will be initiated. + const environmentRows = await query( + sqlClient, + environmentSql.selectEnvironmentByNameAndProject(project.standbyProductionEnvironment, project.id), + ); + const environment = environmentRows[0]; + var environmentId = parseInt(environment.id); + // construct the data for the misc task let uuid = uuid4(); diff --git a/services/ui/src/components/Environment/index.js b/services/ui/src/components/Environment/index.js index 2a8de471c9..753a755679 100644 --- a/services/ui/src/components/Environment/index.js +++ b/services/ui/src/components/Environment/index.js @@ -119,7 +119,7 @@ const Environment = ({ environment }) => {
- {environment.project.standbyProductionEnvironment && environment.environmentType == 'production' && ( + {environment.project.standbyProductionEnvironment == environment.name && environment.environmentType == 'production' && ( {(switchActiveStandby, { loading, called, error, data }) => { const switchActiveBranch = () => { From 57c99ab4df58f3fb858914452b18ed13e0bbc57f Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 24 Jun 2020 14:50:09 +1000 Subject: [PATCH 163/280] same logic as route/label showing --- services/ui/src/components/Environment/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ui/src/components/Environment/index.js b/services/ui/src/components/Environment/index.js index 753a755679..016919fdbb 100644 --- a/services/ui/src/components/Environment/index.js +++ b/services/ui/src/components/Environment/index.js @@ -119,7 +119,7 @@ const Environment = ({ environment }) => {
- {environment.project.standbyProductionEnvironment == environment.name && environment.environmentType == 'production' && ( + {environment.project.productionEnvironment && environment.project.standbyProductionEnvironment && environment.environmentType == 'production' && environment.project.standbyProductionEnvironment == environment.name && ( {(switchActiveStandby, { loading, called, error, data }) => { const switchActiveBranch = () => { From 6a54f3465d92656e6b3b20f4108375d537307002 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Wed, 24 Jun 2020 06:52:45 -0400 Subject: [PATCH 164/280] better naming --- services/ui/src/components/ActiveStandbyConfirm/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ui/src/components/ActiveStandbyConfirm/index.js b/services/ui/src/components/ActiveStandbyConfirm/index.js index ef2eb99f78..1331fe9805 100644 --- a/services/ui/src/components/ActiveStandbyConfirm/index.js +++ b/services/ui/src/components/ActiveStandbyConfirm/index.js @@ -20,7 +20,7 @@ export const ActiveStandbyConfirm = ({ return (
Date: Wed, 24 Jun 2020 14:55:52 +0100 Subject: [PATCH 165/280] Problem permission check if user can access project --- services/api/src/resources/problem/resolvers.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/services/api/src/resources/problem/resolvers.ts b/services/api/src/resources/problem/resolvers.ts index 6fe7eeebb5..1b77ee891a 100644 --- a/services/api/src/resources/problem/resolvers.ts +++ b/services/api/src/resources/problem/resolvers.ts @@ -9,7 +9,7 @@ const logger = require('../../logger'); export const getAllProblems: ResolverFn = async ( root, args, - { sqlClient } + { sqlClient, hasPermission } ) => { let rows = []; @@ -28,12 +28,17 @@ export const getAllProblems: ResolverFn = async ( } } - const problems = rows && rows.map(problem => { + const problems = rows && rows.map(async problem => { const { environment: envId, name, project, environmentType, openshiftProjectName, ...rest} = problem; - return { ...rest, environment: { id: envId, name, project, environmentType, openshiftProjectName }}; + + await hasPermission('problem', 'view', { + project: project, + }); + + return { ...rest, environment: { id: envId, name, project, environmentType, openshiftProjectName }}; }); - const sorted = R.sort(R.descend(R.prop('severity')), problems); + const sorted = R.sort(R.descend(R.prop('severity')), await problems); return sorted.map((row: any) => ({ ...(row as Object) })); }; From 363589214008245f1ec06043a21e47b77061b5a4 Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Wed, 24 Jun 2020 18:19:16 +0200 Subject: [PATCH 166/280] Removed eval --- helpers/check_acme_routes.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/helpers/check_acme_routes.sh b/helpers/check_acme_routes.sh index 476a07390f..135bc71872 100755 --- a/helpers/check_acme_routes.sh +++ b/helpers/check_acme_routes.sh @@ -198,21 +198,20 @@ function notify_customer() { NOTIFICATION_DATA=$(lagoon list $NOTIFICATION -p "$1" --no-header|head -n1|awk '{print $3";"$4}') CHANNEL=$(echo "$NOTIFICATION_DATA"|cut -f1 -d ";") WEBHOOK=$(echo "$NOTIFICATION_DATA"|cut -f2 -d ";") - MESSAGE="Your $ROUTE_HOSTNAME route is configured in the \`.lagoon.yml\` file to issue an TLS certificate from Lets Encrypt. Unfortunately Lagoon is unable to issue a certificate as $DNS_ERROR.\nTo be issued correctly, the DNS records for $ROUTE_HOSTNAME should point to $CLUSTER_HOSTNAME with an CNAME record (preferred) or to ${CLUSTER_IPS[*]} via an A record (also possible but not preferred).\nIf you don'\''t need the SSL certificate or you are using a CDN that provides you with an TLS certificate, please update your .lagoon.yml file by setting the tls-acme parameter to false for $ROUTE_HOSTNAME, as described here: https://lagoon.readthedocs.io/en/latest/using_lagoon/lagoon_yml/#ssl-configuration-tls-acme.\nWe have now administratively disabled the issuing of Lets Encrypt certificate for $ROUTE_HOSTNAME in order to protect the cluster, this will be reset during the next deployment, therefore we suggest to resolve this issue as soon as possible. Feel free to reach out to us for further information.\nThanks you.\namazee.io team" + MESSAGE="Your $ROUTE_HOSTNAME route is configured in the \`.lagoon.yml\` file to issue an TLS certificate from Lets Encrypt. Unfortunately Lagoon is unable to issue a certificate as $DNS_ERROR.\nTo be issued correctly, the DNS records for $ROUTE_HOSTNAME should point to $CLUSTER_HOSTNAME with an CNAME record (preferred) or to ${CLUSTER_IPS[*]} via an A record (also possible but not preferred).\nIf you don't need the SSL certificate or you are using a CDN that provides you with an TLS certificate, please update your .lagoon.yml file by setting the tls-acme parameter to false for $ROUTE_HOSTNAME, as described here: https://lagoon.readthedocs.io/en/latest/using_lagoon/lagoon_yml/#ssl-configuration-tls-acme.\nWe have now administratively disabled the issuing of Lets Encrypt certificate for $ROUTE_HOSTNAME in order to protect the cluster, this will be reset during the next deployment, therefore we suggest to resolve this issue as soon as possible. Feel free to reach out to us for further information.\nThanks you.\namazee.io team" # json Payload - JSON=\'"{\"channel\": \"$CHANNEL\", \"text\": \"${MESSAGE}\"}"\' + PAYLOAD="\"channel\": \"$CHANNEL\", \"text\": \"${MESSAGE}\"" echo -e "Sending notification into ${CHANNEL}" # Execute curl to send message into the channel if [[ $DRYRUN = true ]]; then - echo "DRYRUN on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data "$JSON" "$WEBHOOK"" + echo "DRYRUN on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" "$WEBHOOK"" else - eval "curl -X POST -H 'Content-type: application/json' --data "${JSON}" ${WEBHOOK}" + curl --trace-ascii /dev/stdout -X POST -H 'Content-type: application/json' --data '{'"${PAYLOAD}"'}' ${WEBHOOK} fi } - # Main function function main() { From 4731696e6a09d44602f4d92c430130fcff87d55d Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Wed, 24 Jun 2020 14:57:36 -0400 Subject: [PATCH 167/280] initial redis add --- docker-compose.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 00a598c87c..65b44c8ba0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -759,3 +759,9 @@ services: lagoon.template: services/harborregistryctl/harborregistry.yml lagoon.name: harborregistry lagoon.image: amazeeiolagoon/harborregistryctl:v1-6-0 + api-redis: + image: ${IMAGE_REPO:-lagoon}/redis + labels: + lagoon.type: redis + ports: + - '6397:6397' \ No newline at end of file From 49886041493f678206d1828966355a79afa69160 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Wed, 24 Jun 2020 14:58:05 -0400 Subject: [PATCH 168/280] adding in redis nodejs library --- services/api/package.json | 1 + yarn.lock | 265 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 256 insertions(+), 10 deletions(-) diff --git a/services/api/package.json b/services/api/package.json index 5743e8a86e..d92f4951ad 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -69,6 +69,7 @@ "newrelic": "^6.9.0", "node-cache": "^4.2.1", "ramda": "^0.25.0", + "redis": "^3.0.2", "snakecase-keys": "^1.2.0", "sshpk": "^1.14.2", "validator": "^10.8.0", diff --git a/yarn.lock b/yarn.lock index 005d3a546b..311c83c8b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,20 +21,20 @@ "@types/node" "^10.1.0" long "^4.0.0" -"@apollo/react-common@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@apollo/react-common/-/react-common-3.1.3.tgz#ddc34f6403f55d47c0da147fd4756dfd7c73dac5" - integrity sha512-Q7ZjDOeqjJf/AOGxUMdGxKF+JVClRXrYBGVq+SuVFqANRpd68MxtVV2OjCWavsFAN0eqYnRqRUrl7vtUCiJqeg== +"@apollo/react-common@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@apollo/react-common/-/react-common-3.1.4.tgz#ec13c985be23ea8e799c9ea18e696eccc97be345" + integrity sha512-X5Kyro73bthWSCBJUC5XYQqMnG0dLWuDZmVkzog9dynovhfiVCV4kPSdgSIkqnb++cwCzOVuQ4rDKVwo2XRzQA== dependencies: ts-invariant "^0.4.4" tslib "^1.10.0" -"@apollo/react-hooks@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@apollo/react-hooks/-/react-hooks-3.1.3.tgz#ad42c7af78e81fee0f30e53242640410d5bd0293" - integrity sha512-reIRO9xKdfi+B4gT/o/hnXuopUnm7WED/ru8VQydPw+C/KG/05Ssg1ZdxFKHa3oxwiTUIDnevtccIH35POanbA== +"@apollo/react-hooks@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@apollo/react-hooks/-/react-hooks-3.1.5.tgz#7e710be52461255ae7fc0b3b9c2ece64299c10e6" + integrity sha512-y0CJ393DLxIIkksRup4nt+vSjxalbZBXnnXxYbviq/woj+zKa431zy0yT4LqyRKpFy9ahMIwxBnBwfwIoupqLQ== dependencies: - "@apollo/react-common" "^3.1.3" + "@apollo/react-common" "^3.1.4" "@wry/equality" "^0.1.9" ts-invariant "^0.4.4" tslib "^1.10.0" @@ -1514,6 +1514,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.8.4": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" + integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@7.0.0-beta.44": version "7.0.0-beta.44" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" @@ -1665,6 +1672,16 @@ "@emotion/utils" "0.11.2" "@emotion/weak-memoize" "0.2.4" +"@emotion/cache@^10.0.27": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + "@emotion/core@^10.0.20", "@emotion/core@^10.0.9": version "10.0.22" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.22.tgz#2ac7bcf9b99a1979ab5b0a876fbf37ab0688b177" @@ -1677,6 +1694,18 @@ "@emotion/sheet" "0.9.3" "@emotion/utils" "0.11.2" +"@emotion/core@^10.0.28": + version "10.0.28" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.28.tgz#bb65af7262a234593a9e952c041d0f1c9b9bef3d" + integrity sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/cache" "^10.0.27" + "@emotion/css" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + "@emotion/css@^10.0.22", "@emotion/css@^10.0.9": version "10.0.22" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.22.tgz#37b1abb6826759fe8ac0af0ac0034d27de6d1793" @@ -1686,11 +1715,25 @@ "@emotion/utils" "0.11.2" babel-plugin-emotion "^10.0.22" +"@emotion/css@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" + integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw== + dependencies: + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + babel-plugin-emotion "^10.0.27" + "@emotion/hash@0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.3.tgz#a166882c81c0c6040975dd30df24fae8549bd96f" integrity sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw== +"@emotion/hash@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + "@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": version "0.6.6" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" @@ -1703,11 +1746,23 @@ dependencies: "@emotion/memoize" "0.7.3" +"@emotion/is-prop-valid@0.8.8": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" + integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== + dependencies: + "@emotion/memoize" "0.7.4" + "@emotion/memoize@0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78" integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow== +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + "@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": version "0.6.6" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" @@ -1724,6 +1779,17 @@ "@emotion/utils" "0.11.2" csstype "^2.5.7" +"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": + version "0.11.16" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" + integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg== + dependencies: + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + "@emotion/serialize@^0.9.1": version "0.9.1" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" @@ -1739,6 +1805,11 @@ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a" integrity sha512-c3Q6V7Df7jfwSq5AzQWbXHa5soeE4F5cbqi40xn0CzXxWW9/6Mxq48WJEtqfWzbZtW9odZdnRAkwCQwN12ob4A== +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== + "@emotion/styled-base@^10.0.23": version "10.0.24" resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.24.tgz#9497efd8902dfeddee89d24b0eeb26b0665bfe8b" @@ -1749,6 +1820,16 @@ "@emotion/serialize" "^0.11.14" "@emotion/utils" "0.11.2" +"@emotion/styled-base@^10.0.27": + version "10.0.31" + resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a" + integrity sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/is-prop-valid" "0.8.8" + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + "@emotion/styled@^10.0.17": version "10.0.23" resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.23.tgz#2f8279bd59b99d82deade76d1046249ddfab7c1b" @@ -1757,11 +1838,24 @@ "@emotion/styled-base" "^10.0.23" babel-plugin-emotion "^10.0.23" +"@emotion/styled@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf" + integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q== + dependencies: + "@emotion/styled-base" "^10.0.27" + babel-plugin-emotion "^10.0.27" + "@emotion/stylis@0.8.4": version "0.8.4" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.4.tgz#6c51afdf1dd0d73666ba09d2eb6c25c220d6fe4c" integrity sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ== +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + "@emotion/stylis@^0.7.0": version "0.7.1" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" @@ -1772,6 +1866,11 @@ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.4.tgz#a87b4b04e5ae14a88d48ebef15015f6b7d1f5677" integrity sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ== +"@emotion/unitless@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + "@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": version "0.6.7" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" @@ -1782,6 +1881,11 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183" integrity sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA== +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== + "@emotion/utils@^0.8.2": version "0.8.2" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" @@ -1792,6 +1896,11 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" integrity sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA== +"@emotion/weak-memoize@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@grpc/grpc-js@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.0.3.tgz#7fa2ba293ccc1e91b24074c2628c8c68336e18c4" @@ -3075,6 +3184,11 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== +"@types/long@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + "@types/mariasql@^0.1.30": version "0.1.30" resolved "https://registry.yarnpkg.com/@types/mariasql/-/mariasql-0.1.30.tgz#944446a351452169e10a68fbff7f20c6e0bc5b34" @@ -3108,6 +3222,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.15.tgz#bfff4e23e9e70be6eec450419d51e18de1daf8e7" integrity sha512-daFGV9GSs6USfPgxceDA8nlSe48XrVCJfDeYm7eokxq/ye7iuOH87hKXgMtEAVLFapkczbZsx868PMDT1Y0a6A== +"@types/node@^13.7.0": + version "13.13.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.12.tgz#9c72e865380a7dc99999ea0ef20fc9635b503d20" + integrity sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw== + "@types/p-cancelable@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-1.0.1.tgz#4f0ce8aa3ee0007c2768b9b3e6e22af20a6eecbd" @@ -4676,6 +4795,22 @@ babel-plugin-emotion@^10.0.20, babel-plugin-emotion@^10.0.22, babel-plugin-emoti find-root "^1.1.0" source-map "^0.5.7" +babel-plugin-emotion@^10.0.27: + version "10.0.33" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz#ce1155dcd1783bbb9286051efee53f4e2be63e03" + integrity sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.16" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + babel-plugin-emotion@^9.2.11: version "9.2.11" resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" @@ -6362,6 +6497,13 @@ crypto-random-string@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-loader@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.0.tgz#9f46aaa5ca41dbe31860e3b62b8e23c42916bf56" @@ -6960,6 +7102,11 @@ denodeify@^1.2.1: resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= +denque@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" + integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== + depd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -11420,7 +11567,7 @@ memoize-one@^4.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906" integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA== -memoize-one@^5.0.0: +memoize-one@^5.0.0, memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== @@ -13657,6 +13804,25 @@ property-information@^5.0.0, property-information@^5.3.0: dependencies: xtend "^4.0.1" +protobufjs@^6.8.6: + version "6.9.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.9.0.tgz#c08b2bf636682598e6fabbf0edb0b1256ff090bd" + integrity sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" "^13.7.0" + long "^4.0.0" + protocols@^1.1.0, protocols@^1.4.0: version "1.4.7" resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32" @@ -13933,6 +14099,11 @@ rabbitmq-pub-sub@^0.2.5: "@types/bunyan" "0.0.35" amqplib "^0.5.1" +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== + raf@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -14016,6 +14187,19 @@ react-apollo@^2.1.11: ts-invariant "^0.4.2" tslib "^1.9.3" +react-beautiful-dnd@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" + integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg== + dependencies: + "@babel/runtime" "^7.8.4" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-clientside-effect@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837" @@ -14276,6 +14460,17 @@ react-redux@^7.0.2: prop-types "^15.7.2" react-is "^16.9.0" +react-redux@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" + integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-select@^2.1.1: version "2.4.4" resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.4.4.tgz#ba72468ef1060c7d46fbb862b0748f96491f1f73" @@ -14568,6 +14763,33 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" +redis-commands@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" + integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + +redis@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/redis/-/redis-3.0.2.tgz#bd47067b8a4a3e6a2e556e57f71cc82c7360150a" + integrity sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ== + dependencies: + denque "^1.4.1" + redis-commands "^1.5.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + redux@^4.0.1: version "4.0.4" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" @@ -14576,6 +14798,14 @@ redux@^4.0.1: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -14617,6 +14847,11 @@ regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" @@ -16341,6 +16576,11 @@ tiny-emitter@^2.0.0: resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== +tiny-invariant@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + tinycolor2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" @@ -17056,6 +17296,11 @@ use-callback-ref@^1.2.1: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.1.tgz#898759ccb9e14be6c7a860abafa3ffbd826c89bb" integrity sha512-C3nvxh0ZpaOxs9RCnWwAJ+7bJPwQI8LHF71LzbQ3BvzH5XkdtlkMadqElGevg5bYBDFip4sAnD4m06zAKebg1w== +use-memo-one@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== + use-sidecar@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6" From 49b038e995d5eb4075549dc0b3dfd6fe0f7265cd Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Wed, 24 Jun 2020 15:12:17 -0400 Subject: [PATCH 169/280] hello world --- services/api/src/util/auth.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 0406935fa9..9d01754148 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -4,10 +4,22 @@ import * as logger from '../logger'; import { keycloakGrantManager } from'../clients/keycloakClient'; import { User } from '../models/user'; import { Group } from '../models/group'; +import redis from "redis"; const { JWTSECRET, JWTAUDIENCE } = process.env; +const client = redis.createClient({ + host: 'api-redis', +}); + +client.on("error", function(error) { + console.error(error); +}); + +client.set("key", "value", redis.print); +client.get("key", redis.print); + interface ILegacyToken { aud: string, role: string, From 76c65e77a07a83215072d5b13137dcb4e0861c38 Mon Sep 17 00:00:00 2001 From: Bastian Widmer Date: Wed, 24 Jun 2020 22:07:23 +0200 Subject: [PATCH 170/280] Update Composer to v1.10.8 Update NewRelic to v9.11.0.267 --- helpers/update-versions.yml | 4 ++-- images/php/cli/Dockerfile | 4 ++-- images/php/fpm/Dockerfile | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/helpers/update-versions.yml b/helpers/update-versions.yml index f16d4c042d..33d5180709 100644 --- a/helpers/update-versions.yml +++ b/helpers/update-versions.yml @@ -11,8 +11,8 @@ # Newrelic - https://docs.newrelic.com/docs/release-notes/agent-release-notes/php-release-notes/ NEWRELIC_VERSION: '9.11.0.267' # Composer - https://getcomposer.org/download/ - COMPOSER_VERSION: '1.10.7' - COMPOSER_HASH_SHA256: 'b94b872729668de5b5fbf62f16ff588d2a23480dda88c0e45cb43b721b75ae29' + COMPOSER_VERSION: '1.10.8' + COMPOSER_HASH_SHA256: '4c40737f5d5f36d04f8b2df37171c6a1ff520efcadcb8626cc7c30bd4c5178e5' # Drupal Console Launcher - https://github.com/hechoendrupal/drupal-console-launcher/releases DRUPAL_CONSOLE_LAUNCHER_VERSION: 1.9.4 DRUPAL_CONSOLE_LAUNCHER_SHA: b7759279668caf915b8e9f3352e88f18e4f20659 diff --git a/images/php/cli/Dockerfile b/images/php/cli/Dockerfile index 2e704b4004..07254ebb97 100644 --- a/images/php/cli/Dockerfile +++ b/images/php/cli/Dockerfile @@ -8,8 +8,8 @@ ENV LAGOON=cli # Defining Versions - Composer # @see https://getcomposer.org/download/ -ENV COMPOSER_VERSION=1.10.7 \ - COMPOSER_HASH_SHA256=b94b872729668de5b5fbf62f16ff588d2a23480dda88c0e45cb43b721b75ae29 +ENV COMPOSER_VERSION=1.10.8 \ + COMPOSER_HASH_SHA256=4c40737f5d5f36d04f8b2df37171c6a1ff520efcadcb8626cc7c30bd4c5178e5 RUN apk add --no-cache git \ unzip \ diff --git a/images/php/fpm/Dockerfile b/images/php/fpm/Dockerfile index 2473a3a753..3e0d6e0d67 100644 --- a/images/php/fpm/Dockerfile +++ b/images/php/fpm/Dockerfile @@ -39,7 +39,7 @@ COPY ssmtp.conf /etc/ssmtp/ssmtp.conf # New Relic PHP Agent. # @see https://docs.newrelic.com/docs/release-notes/agent-release-notes/php-release-notes/ # @see https://docs.newrelic.com/docs/agents/php-agent/getting-started/php-agent-compatibility-requirements -ENV NEWRELIC_VERSION=9.10.1.263 +ENV NEWRELIC_VERSION=9.11.0.267 RUN apk add --no-cache curl --repository http://dl-cdn.alpinelinux.org/alpine/edge/main/ From aab0a9288d25e7f7289d06a5bd19e633d7bd1658 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 25 Jun 2020 10:26:41 +1000 Subject: [PATCH 171/280] add some notes around the switch --- docs/using_lagoon/active_standby.md | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/using_lagoon/active_standby.md b/docs/using_lagoon/active_standby.md index ea479a0566..c92183be43 100644 --- a/docs/using_lagoon/active_standby.md +++ b/docs/using_lagoon/active_standby.md @@ -23,6 +23,7 @@ mutation updateProject { } } ``` + ### `.lagoon.yml` - `production_routes` To configure a project for active/standby in the `.lagoon.yml` file, you'll need to configure the `production_routes` section with any routes you want to attach to the `active` environment, and any routes to the `standby` environment. During an Active/Standby switch, these routes will migrate between the two environments. @@ -112,4 +113,39 @@ mutation updateProject { standbyAlias } } +``` + +## Notes + +When the active/standby trigger has been executed, the `productionEnvironment` and `standbyProductionEnvironments` will switch within the Lagoon API. Both environments are still classed as `production` environment types. We use the `productionEnvironment` to determine which one is labelled as `active`. + +``` +query projectByName { + projectByName(name:"drupal-example"){ + productionEnvironment + standbyProductionEnvironment + } +} +``` +Before switching environments +``` +{ + "data": { + "projectByName": { + "productionEnvironment": "production-brancha", + "standbyProductionEnvironment": "production-branchb" + } + } +} +``` +After switching environments +``` +{ + "data": { + "projectByName": { + "productionEnvironment": "production-branchb", + "standbyProductionEnvironment": "production-brancha" + } + } +} ``` \ No newline at end of file From 3b091dbf3ae19dd8bf61d0a8ccb44e11e80802f0 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 25 Jun 2020 10:59:43 +1000 Subject: [PATCH 172/280] reword a few bits --- docs/using_lagoon/active_standby.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/using_lagoon/active_standby.md b/docs/using_lagoon/active_standby.md index c92183be43..054779f304 100644 --- a/docs/using_lagoon/active_standby.md +++ b/docs/using_lagoon/active_standby.md @@ -6,7 +6,7 @@ Lagoon supports Active/Standby (also known as blue/green) deployments. To change an existing project to support active/standby you'll need to configure some project settings in the Lagoon API `productionEnviromment` should be set to the branch name of the current environment that is active -`standbyProductionEnvironment` should be set to the branch name of the current environment that is in standby +`standbyProductionEnvironment` should be set to the branch name of the environment that will be in standby ``` mutation updateProject { @@ -53,10 +53,14 @@ production_routes: > Note: Any routes that are under the section `environments..routes` will not be moved as part of active/standby, these routes will always be attached to the environment as defined. Ensure that if you do need a specific route to be migrated during an active/standby switch, that you remove them from the `environments` section and place them under the `production_routes` section specific to if it should be an `active` or `standby` route. -## Triggering a switch event +## Triggering the active/standby switch +### via the UI +To trigger the switching of environment routes, you can visit the standby environment in the Lagoon UI and click on the button labeled `Switch Active/Standby environments`. You will be prompted to confirm your action. -To trigger an event to switch the environments, you can run the following graphQL mutation, this will inform lagoon to begin the process. +Once confirmed, it will take you to the tasks page where you can view the progress of the switch. +### via the API +The following graphQL mutation can be executed which will start the process of switching the environment routes. ``` mutation ActiveStandby { switchActiveStandby( @@ -95,7 +99,9 @@ By default, projects will be created with the following aliases that will be ava * `lagoon-production` * `lagoon-standby` -The `lagoon-production` alias will resolve point to whichever site is defined as `productionEnvironment`, where `lagoon-standby` will always resolve to the site that is defined as `standbyProductionEnvironment` +The `lagoon-production` alias will resolve to whichever environment is currently in the API as `productionEnvironment`, where `lagoon-standby` will always resolve to the environment that is defined as `standbyProductionEnvironment`. + +> As the active/standby switch updates these as required, `lagoon-production` will always be the `active` environment. These alias are configurable by updating the project, but be aware that changing them may require you to update any scripts that rely on them. From 2a2d88e6c4f53e83b75dfbafa6a05b4ad9937983 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Thu, 25 Jun 2020 13:41:30 -0400 Subject: [PATCH 173/280] Initial Alpha Redis Cache --- services/api/package.json | 1 + services/api/src/util/auth.ts | 25 +++++++++++++++++++------ yarn.lock | 7 +++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/services/api/package.json b/services/api/package.json index d92f4951ad..b9266c048d 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -39,6 +39,7 @@ "license": "MIT", "dependencies": { "@lagoon/commons": "4.0.0", + "@types/redis": "^2.8.22", "apollo-server-express": "^2.14.2", "aws-sdk": "^2.378.0", "body-parser": "^1.18.2", diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 9d01754148..bc635c2ba1 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -5,20 +5,20 @@ import { keycloakGrantManager } from'../clients/keycloakClient'; import { User } from '../models/user'; import { Group } from '../models/group'; import redis from "redis"; - +import { promisify } from 'util'; const { JWTSECRET, JWTAUDIENCE } = process.env; -const client = redis.createClient({ +const redisClient = redis.createClient({ host: 'api-redis', }); -client.on("error", function(error) { +redisClient.on("error", function(error) { console.error(error); }); -client.set("key", "value", redis.print); -client.get("key", redis.print); +let redisGetAsync = promisify(redisClient.get).bind(redisClient); + interface ILegacyToken { aud: string, @@ -117,7 +117,6 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) return async (resource, scope, attributes: IKeycloakAuthAttributes = {}) => { const currentUserId: string = grant.access_token.content.sub; - const currentUser = await UserModel.loadUserById(currentUserId); // Check if the same set of permissions has been granted already for this // api query. @@ -129,6 +128,17 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) return cachedPermissions; } + const key = await redisGetAsync(cacheKey); + console.log(key); + if (key && key === 'TRUE') { + console.log('Redis cache allowed'); + return; + } else { + console.log('Redis cache denied'); + throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); + } + + const currentUser = await UserModel.loadUserById(currentUserId); const serviceAccount = await keycloakGrantManager.obtainFromClientCredentials(); let claims: { @@ -261,6 +271,8 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) if (newGrant.access_token.hasPermission(resource, scope)) { requestCache.set(cacheKey, true); + + const redisSet = await redisClient.set(cacheKey, 'TRUE'); return; } } catch (err) { @@ -270,6 +282,7 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) } requestCache.set(cacheKey, false); + const redisSet = await redisClient.set(cacheKey, 'FALSE'); throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); }; }; diff --git a/yarn.lock b/yarn.lock index 311c83c8b5..d9e670c42e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3312,6 +3312,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/redis@^2.8.22": + version "2.8.22" + resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.22.tgz#8935227cbe39080506b625276d64974ddbcb9ea4" + integrity sha512-O21YLcAtcSzax8wy4CfxMNjIMNf5X2c1pKTXDWLMa2p77Igvy7wuNjWVv+Db93wTvRvLLev6oq3IE7gxNKFZyg== + dependencies: + "@types/node" "*" + "@types/request@^2.47.1": version "2.48.4" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.4.tgz#df3d43d7b9ed3550feaa1286c6eabf0738e6cf7e" From 3e19ba453df2e1ece0f1a563155af404dc863d4a Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Thu, 25 Jun 2020 14:15:55 -0400 Subject: [PATCH 174/280] kinda working with redis --- services/api/src/util/auth.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index bc635c2ba1..0c19a554e6 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -128,14 +128,16 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) return cachedPermissions; } - const key = await redisGetAsync(cacheKey); + const key = JSON.parse(await redisGetAsync(cacheKey)); console.log(key); - if (key && key === 'TRUE') { - console.log('Redis cache allowed'); - return; - } else { - console.log('Redis cache denied'); - throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); + if (key) { + if (key && key.allowed && key.allowed === true) { + console.log('Redis cache allowed'); + return; + } else { + console.log('Redis cache denied'); + throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); + } } const currentUser = await UserModel.loadUserById(currentUserId); @@ -271,8 +273,8 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) if (newGrant.access_token.hasPermission(resource, scope)) { requestCache.set(cacheKey, true); - - const redisSet = await redisClient.set(cacheKey, 'TRUE'); + console.log('Setting redis key') + const redisSet = await redisClient.set(cacheKey, JSON.stringify({ allowed: true })); return; } } catch (err) { @@ -282,7 +284,7 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) } requestCache.set(cacheKey, false); - const redisSet = await redisClient.set(cacheKey, 'FALSE'); + const redisSet = await redisClient.set(cacheKey, JSON.stringify({ allowed: false })); throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); }; }; From 4c4a1d3a11e7c437a6b33fdc950449ece71d2e2b Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Thu, 25 Jun 2020 17:33:15 -0400 Subject: [PATCH 175/280] refactoring to use hashes a bit bettter --- services/api/src/util/auth.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 0c19a554e6..6dc3488e8f 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -18,6 +18,7 @@ redisClient.on("error", function(error) { }); let redisGetAsync = promisify(redisClient.get).bind(redisClient); +let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); interface ILegacyToken { @@ -128,10 +129,13 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) return cachedPermissions; } - const key = JSON.parse(await redisGetAsync(cacheKey)); - console.log(key); - if (key) { - if (key && key.allowed && key.allowed === true) { + const redisHash = await redisHMGetAllAsync(`cache:authz:${currentUserId}`); + const redisHashKey = `${resource}:${attributes.project ? `${attributes.project}:`: ''}${attributes.group ? `${attributes.group}:`: ''}${scope}`; + + if (redisHash && redisHash[redisHashKey]) { + + console.log('redisHash[redisHashKey]: ', redisHash[redisHashKey]); + if (redisHash && redisHash[redisHashKey] === 'allowed:true') { console.log('Redis cache allowed'); return; } else { @@ -273,8 +277,7 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) if (newGrant.access_token.hasPermission(resource, scope)) { requestCache.set(cacheKey, true); - console.log('Setting redis key') - const redisSet = await redisClient.set(cacheKey, JSON.stringify({ allowed: true })); + await redisClient.hmset(`cache:authz:${currentUserId}`, redisHashKey, 'allowed:true', ); return; } } catch (err) { @@ -284,7 +287,7 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) } requestCache.set(cacheKey, false); - const redisSet = await redisClient.set(cacheKey, JSON.stringify({ allowed: false })); + await redisClient.hmset(`cache:authz:${currentUserId}`, redisHashKey, 'allowed:false', ); throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); }; }; From e6c1d60a0e1d4e9abbfdfaed50c4d32432e9048b Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Fri, 26 Jun 2020 10:01:19 +1200 Subject: [PATCH 176/280] Adds Luascript healthz fallback --- images/nginx/Dockerfile | 1 + images/nginx/docker-entrypoint | 8 +++++++- images/nginx/healthcheck/README.md | 10 ++++++++++ images/nginx/healthcheck/healthz.locations | 8 ++++++++ images/nginx/healthcheck/healthz.locations.php.disable | 10 ++++++++++ 5 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 images/nginx/healthcheck/README.md create mode 100644 images/nginx/healthcheck/healthz.locations create mode 100644 images/nginx/healthcheck/healthz.locations.php.disable diff --git a/images/nginx/Dockerfile b/images/nginx/Dockerfile index ceb5c8c1b2..311bf2625f 100644 --- a/images/nginx/Dockerfile +++ b/images/nginx/Dockerfile @@ -36,6 +36,7 @@ COPY fastcgi.conf /etc/nginx/fastcgi_params COPY helpers/ /etc/nginx/helpers/ COPY static-files.conf /etc/nginx/conf.d/app.conf COPY redirects-map.conf /etc/nginx/redirects-map.conf +COPY healthcheck/healthz.locations healthcheck/healthz.locations.php.disable /etc/nginx/conf.d/ RUN mkdir -p /app \ && rm -f /etc/nginx/conf.d/default.conf \ diff --git a/images/nginx/docker-entrypoint b/images/nginx/docker-entrypoint index 602171d227..8130667d12 100755 --- a/images/nginx/docker-entrypoint +++ b/images/nginx/docker-entrypoint @@ -15,8 +15,14 @@ ep /etc/nginx/* # Find all folders within /etc/nginx/conf.d/ find /etc/nginx/conf.d/ -type d | while read DIR; do # envplate if found folder is not empty - if find $DIR -mindepth 1 | read; then + if find $DIR -mindepth 1 | read; then ep $DIR/*; fi done ep /etc/nginx/helpers/* + +# If PHP is enabled, we override the Luascript /healthz check +if [ ! -z "$NGINX_FASTCGI_PASS" ]; then + echo "Setting up Healthz-php:" + mv /etc/nginx/conf.d/healthz.locations.php.disable /etc/nginx/conf.d/healthz.locations +fi diff --git a/images/nginx/healthcheck/README.md b/images/nginx/healthcheck/README.md new file mode 100644 index 0000000000..73109acdcf --- /dev/null +++ b/images/nginx/healthcheck/README.md @@ -0,0 +1,10 @@ +# Healthcheck + +In this directory you'll find two files + +- healthz.locations.php.disable +- healthz.locations + +Both are designed to expose a `/healthz` location from the nginx service. The difference being that the `.php.disable` file is used to point to the [healthz-php](https://github.com/amazeeio/healthz-php) application _if_ there is a PHP service attached to this application. + +The logic for which of the two files are enabled are contained in this image's `docker-entrypoint` file - there we check for the existence of the env var `NGINX_FASTCGI_PASS`, which indicates (or should indicate) the presence of a PHP-fpm service. \ No newline at end of file diff --git a/images/nginx/healthcheck/healthz.locations b/images/nginx/healthcheck/healthz.locations new file mode 100644 index 0000000000..b3bb78d84a --- /dev/null +++ b/images/nginx/healthcheck/healthz.locations @@ -0,0 +1,8 @@ +location /healthz { + content_by_lua_block { + ngx.status = ngx.HTTP_OK; + ngx.header.content_type = 'application/json'; + ngx.say('{"check_nginx":"pass"}'); + ngx.exit(ngx.OK); + } +} \ No newline at end of file diff --git a/images/nginx/healthcheck/healthz.locations.php.disable b/images/nginx/healthcheck/healthz.locations.php.disable new file mode 100644 index 0000000000..04bb045b47 --- /dev/null +++ b/images/nginx/healthcheck/healthz.locations.php.disable @@ -0,0 +1,10 @@ +location /healthz { + rewrite ^/healthz/(.*)$ /healthz/index.php; + + location ~* \.php(/|$) { + include /etc/nginx/fastcgi.conf; + fastcgi_param SCRIPT_NAME /index.php; + fastcgi_param SCRIPT_FILENAME /healthz-php/index.php; + fastcgi_pass ${NGINX_FASTCGI_PASS:-php}:9000; + } +} From 0e0c12c25b4dba0a7e23736e8f2bb04e91acf4ea Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 26 Jun 2020 09:54:02 +1000 Subject: [PATCH 177/280] feedback tweaks --- docs/using_lagoon/active_standby.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/using_lagoon/active_standby.md b/docs/using_lagoon/active_standby.md index 054779f304..e31567308c 100644 --- a/docs/using_lagoon/active_standby.md +++ b/docs/using_lagoon/active_standby.md @@ -3,10 +3,10 @@ Lagoon supports Active/Standby (also known as blue/green) deployments. ## Configuration -To change an existing project to support active/standby you'll need to configure some project settings in the Lagoon API +To change an existing project to support active/standby you'll need to configure some project settings in the Lagoon API. -`productionEnviromment` should be set to the branch name of the current environment that is active -`standbyProductionEnvironment` should be set to the branch name of the environment that will be in standby +* `productionEnviromment` should be set to the branch name of the current environment that is active. +* `standbyProductionEnvironment` should be set to the branch name of the environment that will be in standby. ``` mutation updateProject { @@ -123,7 +123,7 @@ mutation updateProject { ## Notes -When the active/standby trigger has been executed, the `productionEnvironment` and `standbyProductionEnvironments` will switch within the Lagoon API. Both environments are still classed as `production` environment types. We use the `productionEnvironment` to determine which one is labelled as `active`. +When the active/standby trigger has been executed, the `productionEnvironment` and `standbyProductionEnvironments` will switch within the Lagoon API. Both environments are still classed as `production` environment types. We use the `productionEnvironment` to determine which one is labelled as `active`. For more information on the differences between environment types, read the [documentation for `environment types`](environment_types.md#environment-types) ``` query projectByName { From 80f806d0f42fba200d70eae6855cedd2af2b71bd Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Thu, 25 Jun 2020 20:25:31 -0400 Subject: [PATCH 178/280] refactoring out to a redisClient --- services/api/src/clients/redisClient.ts | 56 +++++++++++++++++++++++++ services/api/src/util/auth.ts | 41 +++++++----------- 2 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 services/api/src/clients/redisClient.ts diff --git a/services/api/src/clients/redisClient.ts b/services/api/src/clients/redisClient.ts new file mode 100644 index 0000000000..3392abbfbf --- /dev/null +++ b/services/api/src/clients/redisClient.ts @@ -0,0 +1,56 @@ +import redis from "redis"; +import { promisify } from 'util'; + + +const redisClient = redis.createClient({ + host: 'api-redis', +}); + +redisClient.on("error", function(error) { + console.error(error); +}); + +let redisGetAsync = promisify(redisClient.get).bind(redisClient); +let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); +let redisHDelAsync = promisify(redisClient.hdel).bind(redisClient); + +interface IUserResourceScope { + resource: string, + scope: string, + currentUserId: string, + project?: number, + group?: string, + users?: number[] +} + +const hashKey = ({ resource, project, group, scope }: IUserResourceScope) => + `${resource}:${project ? `${project}:`: ''}${group ? `${group}:`: ''}${scope}`; + + +export const isRedisCacheAllowed = async (resourceScope: IUserResourceScope) => { + const redisHash = await redisHMGetAllAsync(`cache:authz:${resourceScope.currentUserId}`); + const key = hashKey(resourceScope); + + if (redisHash && !redisHash[key]) { + return null; + } + + return (redisHash && redisHash[key] === 1) ? true : false; +} + +export const saveRedisCache = async (resourceScope: IUserResourceScope, value: number|string) => { + const key = hashKey(resourceScope); + await redisClient.hmset(`cache:authz:${resourceScope.currentUserId}`, key, value); +} + +export const deleteRedisCacheForScope = async (resourceScope: IUserResourceScope) => { + const key = hashKey(resourceScope); + const result = await redisHDelAsync(`cache:authz:${resourceScope.currentUserId}`, key); + return result; +} + +export default { + isRedisCacheAllowed, + saveRedisCache, + deleteRedisCacheForScope +}; \ No newline at end of file diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 6dc3488e8f..5b1679d5fa 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -1,24 +1,25 @@ import * as R from 'ramda'; +// import redis from "redis"; +// import { promisify } from 'util'; +import { isRedisCacheAllowed, saveRedisCache } from '../clients/redisClient'; import { verify } from 'jsonwebtoken'; import * as logger from '../logger'; import { keycloakGrantManager } from'../clients/keycloakClient'; import { User } from '../models/user'; import { Group } from '../models/group'; -import redis from "redis"; -import { promisify } from 'util'; const { JWTSECRET, JWTAUDIENCE } = process.env; -const redisClient = redis.createClient({ - host: 'api-redis', -}); +// const redisClient = redis.createClient({ +// host: 'api-redis', +// }); -redisClient.on("error", function(error) { - console.error(error); -}); +// redisClient.on("error", function(error) { +// console.error(error); +// }); -let redisGetAsync = promisify(redisClient.get).bind(redisClient); -let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); +// let redisGetAsync = promisify(redisClient.get).bind(redisClient); +// let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); interface ILegacyToken { @@ -129,19 +130,9 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) return cachedPermissions; } - const redisHash = await redisHMGetAllAsync(`cache:authz:${currentUserId}`); - const redisHashKey = `${resource}:${attributes.project ? `${attributes.project}:`: ''}${attributes.group ? `${attributes.group}:`: ''}${scope}`; - - if (redisHash && redisHash[redisHashKey]) { - - console.log('redisHash[redisHashKey]: ', redisHash[redisHashKey]); - if (redisHash && redisHash[redisHashKey] === 'allowed:true') { - console.log('Redis cache allowed'); - return; - } else { - console.log('Redis cache denied'); - throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); - } + const resourceScope = {resource, scope, currentUserId, ...attributes }; + if (!isRedisCacheAllowed(resourceScope)){ + throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); } const currentUser = await UserModel.loadUserById(currentUserId); @@ -277,7 +268,7 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) if (newGrant.access_token.hasPermission(resource, scope)) { requestCache.set(cacheKey, true); - await redisClient.hmset(`cache:authz:${currentUserId}`, redisHashKey, 'allowed:true', ); + saveRedisCache(resourceScope, 1); return; } } catch (err) { @@ -287,7 +278,7 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) } requestCache.set(cacheKey, false); - await redisClient.hmset(`cache:authz:${currentUserId}`, redisHashKey, 'allowed:false', ); + saveRedisCache(resourceScope, 0); throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); }; }; From f234769a9cf25de3d910077810fbc855be7165a9 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Fri, 26 Jun 2020 15:22:05 +0800 Subject: [PATCH 179/280] Fix typo in variable name This enables prerollout disabling from environment variables. --- .../kubectl-build-deploy-dind/build-deploy-docker-compose.sh | 4 ++-- images/oc-build-deploy-dind/build-deploy-docker-compose.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 34732be852..684f66d10e 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -183,8 +183,8 @@ if [[ ( "$BUILD_TYPE" == "pullrequest" || "$BUILD_TYPE" == "branch" ) && ! $TH LAGOON_POSTROLLOUT_DISABLED=($(echo $LAGOON_PROJECT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_POSTROLLOUT_DISABLED") | "\(.value)"')) fi if [ ! -z "$LAGOON_ENVIRONMENT_VARIABLES" ]; then - LAGOON_PREROLLOUT_DISABLED=($(echo $LAGOON_PROJECT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_PREROLLOUT_DISABLED") | "\(.value)"')) - LAGOON_POSTROLLOUT_DISABLED=($(echo $LAGOON_PROJECT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_POSTROLLOUT_DISABLED") | "\(.value)"')) + LAGOON_PREROLLOUT_DISABLED=($(echo $LAGOON_ENVIRONMENT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_PREROLLOUT_DISABLED") | "\(.value)"')) + LAGOON_POSTROLLOUT_DISABLED=($(echo $LAGOON_ENVIRONMENT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_POSTROLLOUT_DISABLED") | "\(.value)"')) fi set -x diff --git a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh index 9c240ffedb..3b5ce231a1 100755 --- a/images/oc-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/oc-build-deploy-dind/build-deploy-docker-compose.sh @@ -272,8 +272,8 @@ if [[ ( "$TYPE" == "pullrequest" || "$TYPE" == "branch" ) && ! $THIS_IS_TUG == LAGOON_POSTROLLOUT_DISABLED=($(echo $LAGOON_PROJECT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_POSTROLLOUT_DISABLED") | "\(.value)"')) fi if [ ! -z "$LAGOON_ENVIRONMENT_VARIABLES" ]; then - LAGOON_PREROLLOUT_DISABLED=($(echo $LAGOON_PROJECT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_PREROLLOUT_DISABLED") | "\(.value)"')) - LAGOON_POSTROLLOUT_DISABLED=($(echo $LAGOON_PROJECT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_POSTROLLOUT_DISABLED") | "\(.value)"')) + LAGOON_PREROLLOUT_DISABLED=($(echo $LAGOON_ENVIRONMENT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_PREROLLOUT_DISABLED") | "\(.value)"')) + LAGOON_POSTROLLOUT_DISABLED=($(echo $LAGOON_ENVIRONMENT_VARIABLES | jq -r '.[] | select(.name == "LAGOON_POSTROLLOUT_DISABLED") | "\(.value)"')) fi set -x From 966513267163100855f0a3ea71952d49809ab880 Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Fri, 26 Jun 2020 15:16:25 -0500 Subject: [PATCH 180/280] Oops, I a set of quotes --- services/harbor-core/harbor-core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/harbor-core/harbor-core.yml b/services/harbor-core/harbor-core.yml index dab6c5797b..8b9626d6c4 100644 --- a/services/harbor-core/harbor-core.yml +++ b/services/harbor-core/harbor-core.yml @@ -190,7 +190,7 @@ objects: CLAIR_HEALTH_CHECK_SERVER_URL: "http://harborclair:6061" WITH_TRIVY: "true" TRIVY_ADAPTER_URL: "harbor-trivy:8080" - ROBOT_TOKEN_DURATION: 500 + ROBOT_TOKEN_DURATION: "500" HTTP_PROXY: "" HTTPS_PROXY: "" NO_PROXY: "harbor-core,harbor-jobservice,harbor-database,harborclair,harborclairadapter,harborregistry,harbor-portal,harbor-trivy,127.0.0.1,localhost,.local,.internal" From bc343f635439169797e5947404c367c98ff9bf41 Mon Sep 17 00:00:00 2001 From: Chandeep Khosa Date: Fri, 26 Jun 2020 22:03:57 +0100 Subject: [PATCH 181/280] Update solr.md with some helpful additions --- docs/using_lagoon/drupal/services/solr.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/using_lagoon/drupal/services/solr.md b/docs/using_lagoon/drupal/services/solr.md index e865fc1c4c..f06216f5d2 100644 --- a/docs/using_lagoon/drupal/services/solr.md +++ b/docs/using_lagoon/drupal/services/solr.md @@ -1,10 +1,14 @@ # Solr ## Standard use - For Solr 5.5 and 6.6 we ship the default schema files provided by [search_api_solr](https://www.drupal.org/project/search_api_solr) version 8.x-1.2. Add the Solr version you would like to use in your docker-compose.yml file, following [our example](https://github.com/amazeeio/drupal-example/blob/master/docker-compose.yml#L103-L111). +We provide you with the default schema files provided by [search_api_solr](https://www.drupal.org/project/search_api_solr) version 8.x-1.2. This works for Solr 5.5 and 6.6 + +Specify the Solr version you would like to use in your docker-compose.yml file, following [our example](https://github.com/amazeeio/drupal-example/blob/master/docker-compose.yml#L103-L111). ## Custom schema -To implement schema customizations for Solr in your project, look to how Lagoon [creates our standard images](https://github.com/amazeeio/lagoon/blob/master/images/solr-drupal/Dockerfile). +If you use a different version of the search_api_solr module, you may need to add your own custom schema. The module allows you to download an easy config.zip file containing what you need. + +Also if for any other reason you would like to implement schema customizations for Solr in your project, look to how Lagoon [creates our standard images](https://github.com/amazeeio/lagoon/blob/master/images/solr-drupal/Dockerfile). * In the `solr` section of your docker-compose file replace `image: amazeeio/solr:6.6` with: From aabd3a1e52766c410e732eeabad7bea93cd7261a Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Sat, 27 Jun 2020 00:26:15 +0200 Subject: [PATCH 182/280] Added DEBUG option to print debug information --- helpers/check_acme_routes.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/helpers/check_acme_routes.sh b/helpers/check_acme_routes.sh index 135bc71872..f695bcf807 100755 --- a/helpers/check_acme_routes.sh +++ b/helpers/check_acme_routes.sh @@ -18,6 +18,9 @@ COMMAND=${1:-"help"} # Set DRYRUN variable to true to run in dry-run mode DRYRUN="${DRYRUN:-"false"}" +# Set DEBUG variable to true, to print echo messages +DEBUG="${DEBUG:-"false"}" + # Set a REGEX variable to filter the execution of the script REGEX=${REGEX:-".*"} @@ -116,6 +119,10 @@ function create_routes_array() { # Create a sorted array of unique route to check ROUTES_ARRAY_SORTED=($(sort -u -k 2 -t ";"<<<"${ROUTES_ARRAY[*]}")) + + if [[ "${DEBUG}" == true ]]; then + echo -e "===== DEBUG INFORMATION =====\n${ROUTES_ARRAY_SORTED[*]}" + fi } # Function to check the routes, update them and delete the exposer's routes @@ -137,6 +144,10 @@ function check_routes() { # Get route DNS record(s) ROUTE_HOSTNAME_IP=$(dig +short "$ROUTE_HOSTNAME") + if [[ "${DEBUG}" == true ]]; then + echo -e "===== DEBUG INFORMATION =====\n${route[*]}\n$ROUTE_HOSTNAME_IP" + fi + # Check if the route matches the Cluster's IP(s) if echo "$ROUTE_HOSTNAME_IP" | grep -E -q -v "${CLUSTER_IPS[*]}"; then @@ -154,7 +165,6 @@ function check_routes() { # Now once the main route is updated, it's time to get rid of exposers' routes for j in $(oc get -n "$ROUTE_NAMESPACE" route|grep exposer|grep -E '(^|\s)'"$ROUTE_HOSTNAME"'($|\s)'|awk '{print $1";"$2}') - #for j in $(oc get -n $ROUTE_NAMESPACE route|grep exposer|awk '{print $1";"$2}') do ocroute=($(echo "$j" | tr ";" "\n")) OCROUTE_NAME=${ocroute[0]} @@ -167,6 +177,8 @@ function check_routes() { done fi echo -e "\n" + + done } @@ -208,7 +220,7 @@ function notify_customer() { if [[ $DRYRUN = true ]]; then echo "DRYRUN on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" "$WEBHOOK"" else - curl --trace-ascii /dev/stdout -X POST -H 'Content-type: application/json' --data '{'"${PAYLOAD}"'}' ${WEBHOOK} + curl -X POST -H 'Content-type: application/json' --data '{'"${PAYLOAD}"'}' ${WEBHOOK} fi } From a67983e8bbe8045226fd3290dc0ae68e91670edd Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 26 Jun 2020 17:45:11 -0500 Subject: [PATCH 183/280] Fix nslookup returns non-zero error code on 'host.docker.internal' --- images/php/fpm/entrypoints/60-php-xdebug.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/php/fpm/entrypoints/60-php-xdebug.sh b/images/php/fpm/entrypoints/60-php-xdebug.sh index 5bd0959a0e..1d05a274f1 100755 --- a/images/php/fpm/entrypoints/60-php-xdebug.sh +++ b/images/php/fpm/entrypoints/60-php-xdebug.sh @@ -3,7 +3,7 @@ # Tries to find the Dockerhost get_dockerhost() { # https://docs.docker.com/docker-for-mac/networking/#known-limitations-use-cases-and-workarounds - if busybox timeout 1 busybox nslookup host.docker.internal &> /dev/null; then + if busybox timeout 1 busybox nslookup -query=A host.docker.internal &> /dev/null; then echo "host.docker.internal" return fi From c1db66b8c5aafeefd3eec451203bb5abc874a02d Mon Sep 17 00:00:00 2001 From: Tim Clifford Date: Sun, 28 Jun 2020 13:00:08 +0100 Subject: [PATCH 184/280] Some small refactoring --- .../api/src/resources/problem/resolvers.ts | 8 +- .../ProblemsByProject/Honeycomb/index.js | 18 +- .../ProblemsByProject/Honeycomb/styling.css | 1 + .../src/components/ProblemsByProject/index.js | 3 + services/ui/src/pages/problems-dashboard.js | 14 +- yarn.lock | 211 ++++++++++++++++-- 6 files changed, 221 insertions(+), 34 deletions(-) diff --git a/services/api/src/resources/problem/resolvers.ts b/services/api/src/resources/problem/resolvers.ts index 1b77ee891a..fe10ee8121 100644 --- a/services/api/src/resources/problem/resolvers.ts +++ b/services/api/src/resources/problem/resolvers.ts @@ -28,7 +28,7 @@ export const getAllProblems: ResolverFn = async ( } } - const problems = rows && rows.map(async problem => { + const problems: any = rows && rows.map(async problem => { const { environment: envId, name, project, environmentType, openshiftProjectName, ...rest} = problem; await hasPermission('problem', 'view', { @@ -38,8 +38,10 @@ export const getAllProblems: ResolverFn = async ( return { ...rest, environment: { id: envId, name, project, environmentType, openshiftProjectName }}; }); - const sorted = R.sort(R.descend(R.prop('severity')), await problems); - return sorted.map((row: any) => ({ ...(row as Object) })); + return Promise.all(problems).then((completed) => { + const sorted = R.sort(R.descend(R.prop('severity')), completed); + return sorted.map((row: any) => ({ ...(row as Object) })); + }); }; export const getSeverityOptions = async ( diff --git a/services/ui/src/components/ProblemsByProject/Honeycomb/index.js b/services/ui/src/components/ProblemsByProject/Honeycomb/index.js index 9219633ba1..69adfd3dc8 100644 --- a/services/ui/src/components/ProblemsByProject/Honeycomb/index.js +++ b/services/ui/src/components/ProblemsByProject/Honeycomb/index.js @@ -114,10 +114,22 @@ const Honeycomb = ({ data, filter }) => { const critical = problemsPerProject.filter(p => p.severity === 'CRITICAL').length; const problemCount = problemsPerProject.length || 0; + const HexText = () => { + const classes = display.type !== "normal" ? "no-text" : 'text'; + + if (problemsPerProject.length) { + return ( + {`P: ${problemCount}, C: ${critical}`} + ); + } + else { + return {`P: ${problemCount}`} + } + }; + return ( handleHexClick(project)}> - {problemsPerProject.length ? P: {problemCount}, C: {critical} - : P: {problemCount}} + )})} @@ -129,7 +141,7 @@ const Honeycomb = ({ data, filter }) => { <>
{projectInView.environments && projectInView.environments.map(environment => ( -
+
diff --git a/services/ui/src/components/ProblemsByProject/Honeycomb/styling.css b/services/ui/src/components/ProblemsByProject/Honeycomb/styling.css index be4e4bd1a4..e262451e60 100644 --- a/services/ui/src/components/ProblemsByProject/Honeycomb/styling.css +++ b/services/ui/src/components/ProblemsByProject/Honeycomb/styling.css @@ -38,6 +38,7 @@ svg.grid g text { fill: #000; fill-opacity: 0.5; transition: fill-opacity .2s; + cursor: pointer; } svg.grid g text.no-text { font-size: 0; diff --git a/services/ui/src/components/ProblemsByProject/index.js b/services/ui/src/components/ProblemsByProject/index.js index fb10237391..56ed6fd0a7 100644 --- a/services/ui/src/components/ProblemsByProject/index.js +++ b/services/ui/src/components/ProblemsByProject/index.js @@ -197,6 +197,9 @@ const ProblemsByProject = ({ problems }) => { .fieldWrapper { padding-bottom: 1em; + display: flex; + flex-direction: column; + width: 100%; } .left-content, diff --git a/services/ui/src/pages/problems-dashboard.js b/services/ui/src/pages/problems-dashboard.js index e3df2e9e9c..debe900cff 100644 --- a/services/ui/src/pages/problems-dashboard.js +++ b/services/ui/src/pages/problems-dashboard.js @@ -44,7 +44,7 @@ const ProblemsDashboardPage = () => { return enums && enums.map(s => ({ value: s.name, label: s.name})); }; - const groupByProblemIdentifier = (problems) => problems.reduce((arr, problem) => { + const groupByProblemIdentifier = (problems) => problems && problems.reduce((arr, problem) => { arr[problem.identifier] = arr[problem.identifier] || []; arr[problem.identifier].push(problem); return arr; @@ -122,17 +122,17 @@ const ProblemsDashboardPage = () => { )(({data: {problems} }) => { // Group problems by identifier - const problemsById = groupByProblemIdentifier(problems); - const problemIdentifiers = Object.keys(problemsById).map(p => { + const problemsById = groupByProblemIdentifier(problems) || []; + const problemIdentifiers = problemsById && Object.keys(problemsById).map(p => { const problem = problemsById[p][0]; return {identifier: p, source: problem.source, severity: problem.severity, problems: problemsById[p]}; }, []); - const critical = problems.filter(p => p.severity === 'CRITICAL').length; - const high = problems.filter(p => p.severity === 'HIGH').length; - const medium = problems.filter(p => p.severity === 'MEDIUM').length; - const low = problems.filter(p => p.severity === 'LOW').length; + const critical = problems && problems.filter(p => p.severity === 'CRITICAL').length; + const high = problems && problems.filter(p => p.severity === 'HIGH').length; + const medium = problems && problems.filter(p => p.severity === 'MEDIUM').length; + const low = problems && problems.filter(p => p.severity === 'LOW').length; return ( <> diff --git a/yarn.lock b/yarn.lock index bbc6732d59..437e072fca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,20 +21,20 @@ "@types/node" "^10.1.0" long "^4.0.0" -"@apollo/react-common@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@apollo/react-common/-/react-common-3.1.3.tgz#ddc34f6403f55d47c0da147fd4756dfd7c73dac5" - integrity sha512-Q7ZjDOeqjJf/AOGxUMdGxKF+JVClRXrYBGVq+SuVFqANRpd68MxtVV2OjCWavsFAN0eqYnRqRUrl7vtUCiJqeg== +"@apollo/react-common@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@apollo/react-common/-/react-common-3.1.4.tgz#ec13c985be23ea8e799c9ea18e696eccc97be345" + integrity sha512-X5Kyro73bthWSCBJUC5XYQqMnG0dLWuDZmVkzog9dynovhfiVCV4kPSdgSIkqnb++cwCzOVuQ4rDKVwo2XRzQA== dependencies: ts-invariant "^0.4.4" tslib "^1.10.0" -"@apollo/react-hooks@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@apollo/react-hooks/-/react-hooks-3.1.3.tgz#ad42c7af78e81fee0f30e53242640410d5bd0293" - integrity sha512-reIRO9xKdfi+B4gT/o/hnXuopUnm7WED/ru8VQydPw+C/KG/05Ssg1ZdxFKHa3oxwiTUIDnevtccIH35POanbA== +"@apollo/react-hooks@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@apollo/react-hooks/-/react-hooks-3.1.5.tgz#7e710be52461255ae7fc0b3b9c2ece64299c10e6" + integrity sha512-y0CJ393DLxIIkksRup4nt+vSjxalbZBXnnXxYbviq/woj+zKa431zy0yT4LqyRKpFy9ahMIwxBnBwfwIoupqLQ== dependencies: - "@apollo/react-common" "^3.1.3" + "@apollo/react-common" "^3.1.4" "@wry/equality" "^0.1.9" ts-invariant "^0.4.4" tslib "^1.10.0" @@ -1638,6 +1638,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.8.4": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" + integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.8.7": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.1.tgz#b6eb75cac279588d3100baecd1b9894ea2840822" @@ -1817,6 +1824,16 @@ "@emotion/utils" "0.11.2" "@emotion/weak-memoize" "0.2.4" +"@emotion/cache@^10.0.27": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + "@emotion/core@^10.0.20", "@emotion/core@^10.0.9": version "10.0.22" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.22.tgz#2ac7bcf9b99a1979ab5b0a876fbf37ab0688b177" @@ -1829,6 +1846,18 @@ "@emotion/sheet" "0.9.3" "@emotion/utils" "0.11.2" +"@emotion/core@^10.0.28": + version "10.0.28" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.28.tgz#bb65af7262a234593a9e952c041d0f1c9b9bef3d" + integrity sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/cache" "^10.0.27" + "@emotion/css" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + "@emotion/css@^10.0.22", "@emotion/css@^10.0.9": version "10.0.22" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.22.tgz#37b1abb6826759fe8ac0af0ac0034d27de6d1793" @@ -1838,11 +1867,25 @@ "@emotion/utils" "0.11.2" babel-plugin-emotion "^10.0.22" +"@emotion/css@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" + integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw== + dependencies: + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + babel-plugin-emotion "^10.0.27" + "@emotion/hash@0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.3.tgz#a166882c81c0c6040975dd30df24fae8549bd96f" integrity sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw== +"@emotion/hash@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + "@emotion/is-prop-valid@0.8.5": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.5.tgz#2dda0791f0eafa12b7a0a5b39858405cc7bde983" @@ -1850,11 +1893,23 @@ dependencies: "@emotion/memoize" "0.7.3" +"@emotion/is-prop-valid@0.8.8": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" + integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== + dependencies: + "@emotion/memoize" "0.7.4" + "@emotion/memoize@0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78" integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow== +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + "@emotion/serialize@^0.11.12", "@emotion/serialize@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.14.tgz#56a6d8d04d837cc5b0126788b2134c51353c6488" @@ -1866,11 +1921,27 @@ "@emotion/utils" "0.11.2" csstype "^2.5.7" +"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": + version "0.11.16" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" + integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg== + dependencies: + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + "@emotion/sheet@0.9.3": version "0.9.3" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a" integrity sha512-c3Q6V7Df7jfwSq5AzQWbXHa5soeE4F5cbqi40xn0CzXxWW9/6Mxq48WJEtqfWzbZtW9odZdnRAkwCQwN12ob4A== +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== + "@emotion/styled-base@^10.0.23": version "10.0.24" resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.24.tgz#9497efd8902dfeddee89d24b0eeb26b0665bfe8b" @@ -1881,6 +1952,16 @@ "@emotion/serialize" "^0.11.14" "@emotion/utils" "0.11.2" +"@emotion/styled-base@^10.0.27": + version "10.0.31" + resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a" + integrity sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/is-prop-valid" "0.8.8" + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + "@emotion/styled@^10.0.17": version "10.0.23" resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.23.tgz#2f8279bd59b99d82deade76d1046249ddfab7c1b" @@ -1889,26 +1970,54 @@ "@emotion/styled-base" "^10.0.23" babel-plugin-emotion "^10.0.23" +"@emotion/styled@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf" + integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q== + dependencies: + "@emotion/styled-base" "^10.0.27" + babel-plugin-emotion "^10.0.27" + "@emotion/stylis@0.8.4": version "0.8.4" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.4.tgz#6c51afdf1dd0d73666ba09d2eb6c25c220d6fe4c" integrity sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ== +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + "@emotion/unitless@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.4.tgz#a87b4b04e5ae14a88d48ebef15015f6b7d1f5677" integrity sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ== +"@emotion/unitless@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + "@emotion/utils@0.11.2": version "0.11.2" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183" integrity sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA== +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== + "@emotion/weak-memoize@0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" integrity sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA== +"@emotion/weak-memoize@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -4780,6 +4889,22 @@ babel-plugin-emotion@^10.0.20, babel-plugin-emotion@^10.0.22, babel-plugin-emoti find-root "^1.1.0" source-map "^0.5.7" +babel-plugin-emotion@^10.0.27: + version "10.0.33" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz#ce1155dcd1783bbb9286051efee53f4e2be63e03" + integrity sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.16" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + babel-plugin-extract-import-names@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.5.1.tgz#79fb8550e3e0a9e8654f9461ccade56c9a669a74" @@ -6435,6 +6560,13 @@ crypto-random-string@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-loader@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.0.tgz#9f46aaa5ca41dbe31860e3b62b8e23c42916bf56" @@ -11495,7 +11627,7 @@ memoize-one@^4.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906" integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA== -memoize-one@^5.0.0: +memoize-one@^5.0.0, memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== @@ -13989,6 +14121,11 @@ rabbitmq-pub-sub@^0.2.5: "@types/bunyan" "0.0.35" amqplib "^0.5.1" +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== + ramda@^0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" @@ -14065,6 +14202,19 @@ react-apollo@^2.1.11: ts-invariant "^0.4.2" tslib "^1.9.3" +react-beautiful-dnd@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" + integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg== + dependencies: + "@babel/runtime" "^7.8.4" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-clientside-effect@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837" @@ -14325,6 +14475,17 @@ react-redux@^7.0.2: prop-types "^15.7.2" react-is "^16.9.0" +react-redux@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" + integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-select@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.0.tgz#ab098720b2e9fe275047c993f0d0caf5ded17c27" @@ -14627,6 +14788,14 @@ redux@^4.0.1: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -15347,17 +15516,7 @@ serialised-error@1.1.3: stack-trace "0.0.9" uuid "^3.0.0" -serialize-javascript@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" - integrity sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw== - -serialize-javascript@^1.7.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" - integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== - -serialize-javascript@^2.1.0: +serialize-javascript@1.6.1, serialize-javascript@^1.7.0, serialize-javascript@^2.1.0, serialize-javascript@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== @@ -16397,6 +16556,11 @@ tiny-emitter@^2.0.0: resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== +tiny-invariant@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + tinycolor2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" @@ -17116,6 +17280,11 @@ use-callback-ref@^1.2.1: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.1.tgz#898759ccb9e14be6c7a860abafa3ffbd826c89bb" integrity sha512-C3nvxh0ZpaOxs9RCnWwAJ+7bJPwQI8LHF71LzbQ3BvzH5XkdtlkMadqElGevg5bYBDFip4sAnD4m06zAKebg1w== +use-memo-one@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== + use-sidecar@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6" From b7449be9818e5bda1adaaa2cf768b11c906da6e0 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Mon, 29 Jun 2020 06:02:47 +1200 Subject: [PATCH 185/280] Adds fallback logic for Lua into entrypoint --- images/nginx/Dockerfile | 2 +- images/nginx/docker-entrypoint | 6 +++++- images/nginx/healthcheck/healthz.locations.lua.disable | 8 ++++++++ images/nginx/helpers/90_healthz.conf | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 images/nginx/healthcheck/healthz.locations.lua.disable create mode 100644 images/nginx/helpers/90_healthz.conf diff --git a/images/nginx/Dockerfile b/images/nginx/Dockerfile index 311bf2625f..beed56fdba 100644 --- a/images/nginx/Dockerfile +++ b/images/nginx/Dockerfile @@ -36,7 +36,7 @@ COPY fastcgi.conf /etc/nginx/fastcgi_params COPY helpers/ /etc/nginx/helpers/ COPY static-files.conf /etc/nginx/conf.d/app.conf COPY redirects-map.conf /etc/nginx/redirects-map.conf -COPY healthcheck/healthz.locations healthcheck/healthz.locations.php.disable /etc/nginx/conf.d/ +COPY healthcheck/healthz.locations healthcheck/healthz.locations.php.disable healthcheck/healthz.locations.lua.disable /etc/nginx/conf.d/ RUN mkdir -p /app \ && rm -f /etc/nginx/conf.d/default.conf \ diff --git a/images/nginx/docker-entrypoint b/images/nginx/docker-entrypoint index 8130667d12..448a8fafe0 100755 --- a/images/nginx/docker-entrypoint +++ b/images/nginx/docker-entrypoint @@ -22,7 +22,11 @@ done ep /etc/nginx/helpers/* # If PHP is enabled, we override the Luascript /healthz check +echo "Setting up Healthz routing" if [ ! -z "$NGINX_FASTCGI_PASS" ]; then - echo "Setting up Healthz-php:" + echo "Setting up Healthz routing - using PHP" mv /etc/nginx/conf.d/healthz.locations.php.disable /etc/nginx/conf.d/healthz.locations +else + echo "Setting up Healthz routing - using Lua as fallback" + mv /etc/nginx/conf.d/healthz.locations.lua.disable /etc/nginx/conf.d/healthz.locations fi diff --git a/images/nginx/healthcheck/healthz.locations.lua.disable b/images/nginx/healthcheck/healthz.locations.lua.disable new file mode 100644 index 0000000000..b3bb78d84a --- /dev/null +++ b/images/nginx/healthcheck/healthz.locations.lua.disable @@ -0,0 +1,8 @@ +location /healthz { + content_by_lua_block { + ngx.status = ngx.HTTP_OK; + ngx.header.content_type = 'application/json'; + ngx.say('{"check_nginx":"pass"}'); + ngx.exit(ngx.OK); + } +} \ No newline at end of file diff --git a/images/nginx/helpers/90_healthz.conf b/images/nginx/helpers/90_healthz.conf new file mode 100644 index 0000000000..33356eda06 --- /dev/null +++ b/images/nginx/helpers/90_healthz.conf @@ -0,0 +1 @@ +include /etc/nginx/conf.d/healthz.locations; From acb676e13398b3a920e0c713b99fd814acc94022 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Mon, 29 Jun 2020 06:54:49 +1200 Subject: [PATCH 186/280] Changes base healthcheck directory --- images/nginx/healthcheck/healthz.locations | 4 ++-- images/nginx/healthcheck/healthz.locations.lua.disable | 4 ++-- images/nginx/healthcheck/healthz.locations.php.disable | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/images/nginx/healthcheck/healthz.locations b/images/nginx/healthcheck/healthz.locations index b3bb78d84a..95cf2ed753 100644 --- a/images/nginx/healthcheck/healthz.locations +++ b/images/nginx/healthcheck/healthz.locations @@ -1,8 +1,8 @@ -location /healthz { +location /.lagoonhealthz { content_by_lua_block { ngx.status = ngx.HTTP_OK; ngx.header.content_type = 'application/json'; ngx.say('{"check_nginx":"pass"}'); ngx.exit(ngx.OK); } -} \ No newline at end of file +} diff --git a/images/nginx/healthcheck/healthz.locations.lua.disable b/images/nginx/healthcheck/healthz.locations.lua.disable index b3bb78d84a..95cf2ed753 100644 --- a/images/nginx/healthcheck/healthz.locations.lua.disable +++ b/images/nginx/healthcheck/healthz.locations.lua.disable @@ -1,8 +1,8 @@ -location /healthz { +location /.lagoonhealthz { content_by_lua_block { ngx.status = ngx.HTTP_OK; ngx.header.content_type = 'application/json'; ngx.say('{"check_nginx":"pass"}'); ngx.exit(ngx.OK); } -} \ No newline at end of file +} diff --git a/images/nginx/healthcheck/healthz.locations.php.disable b/images/nginx/healthcheck/healthz.locations.php.disable index 04bb045b47..ba72dc4791 100644 --- a/images/nginx/healthcheck/healthz.locations.php.disable +++ b/images/nginx/healthcheck/healthz.locations.php.disable @@ -1,5 +1,5 @@ -location /healthz { - rewrite ^/healthz/(.*)$ /healthz/index.php; +location /.lagoonhealthz { + rewrite ^/.lagoonhealthz/(.*)$ /healthz/index.php; location ~* \.php(/|$) { include /etc/nginx/fastcgi.conf; From 1d127d075e13b021abfbc963f7bb8b313d9614e9 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Mon, 29 Jun 2020 07:31:04 +1200 Subject: [PATCH 187/280] Changes mv to cp for locations, changes base rewrite for lagoonhealthz --- images/nginx/docker-entrypoint | 4 ++-- images/nginx/healthcheck/healthz.locations.php.disable | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/images/nginx/docker-entrypoint b/images/nginx/docker-entrypoint index 448a8fafe0..2894825d79 100755 --- a/images/nginx/docker-entrypoint +++ b/images/nginx/docker-entrypoint @@ -25,8 +25,8 @@ ep /etc/nginx/helpers/* echo "Setting up Healthz routing" if [ ! -z "$NGINX_FASTCGI_PASS" ]; then echo "Setting up Healthz routing - using PHP" - mv /etc/nginx/conf.d/healthz.locations.php.disable /etc/nginx/conf.d/healthz.locations + cp /etc/nginx/conf.d/healthz.locations.php.disable /etc/nginx/conf.d/healthz.locations else echo "Setting up Healthz routing - using Lua as fallback" - mv /etc/nginx/conf.d/healthz.locations.lua.disable /etc/nginx/conf.d/healthz.locations + cp /etc/nginx/conf.d/healthz.locations.lua.disable /etc/nginx/conf.d/healthz.locations fi diff --git a/images/nginx/healthcheck/healthz.locations.php.disable b/images/nginx/healthcheck/healthz.locations.php.disable index ba72dc4791..f6f5d08bd8 100644 --- a/images/nginx/healthcheck/healthz.locations.php.disable +++ b/images/nginx/healthcheck/healthz.locations.php.disable @@ -1,5 +1,5 @@ location /.lagoonhealthz { - rewrite ^/.lagoonhealthz/(.*)$ /healthz/index.php; + rewrite ^/.lagoonhealthz/(.*)$ /.lagoonhealthz/index.php; location ~* \.php(/|$) { include /etc/nginx/fastcgi.conf; From bf2d0c04c352bfb77fc4de8b2b6bf3a0da6ed5b8 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Mon, 29 Jun 2020 09:41:46 +1200 Subject: [PATCH 188/280] Changes php rewrite rule for healthcheck --- images/nginx/healthcheck/healthz.locations.php.disable | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/nginx/healthcheck/healthz.locations.php.disable b/images/nginx/healthcheck/healthz.locations.php.disable index f6f5d08bd8..8af6022171 100644 --- a/images/nginx/healthcheck/healthz.locations.php.disable +++ b/images/nginx/healthcheck/healthz.locations.php.disable @@ -1,5 +1,5 @@ location /.lagoonhealthz { - rewrite ^/.lagoonhealthz/(.*)$ /.lagoonhealthz/index.php; + rewrite ^/.lagoonhealthz(/.*)$ /.lagoonhealthz/index.php; location ~* \.php(/|$) { include /etc/nginx/fastcgi.conf; From 058906853a6050b1527bbfada7993c6061428b72 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Mon, 29 Jun 2020 10:20:31 +1200 Subject: [PATCH 189/280] Fixes regex on lagoonhealthzphp --- images/nginx/healthcheck/healthz.locations.php.disable | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/nginx/healthcheck/healthz.locations.php.disable b/images/nginx/healthcheck/healthz.locations.php.disable index 8af6022171..dd6be8e7ea 100644 --- a/images/nginx/healthcheck/healthz.locations.php.disable +++ b/images/nginx/healthcheck/healthz.locations.php.disable @@ -1,5 +1,5 @@ location /.lagoonhealthz { - rewrite ^/.lagoonhealthz(/.*)$ /.lagoonhealthz/index.php; + rewrite ^/.lagoonhealthz(/.*)?$ /.lagoonhealthz/index.php; location ~* \.php(/|$) { include /etc/nginx/fastcgi.conf; From 08b5b5c2b3693986f3cac3293c361ee244409da0 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 23 Jun 2020 14:52:29 +0800 Subject: [PATCH 190/280] Enable annotations on the logs-concentrator service --- charts/lagoon-logs-concentrator/templates/service.yaml | 4 ++++ charts/lagoon-logs-concentrator/values.yaml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/charts/lagoon-logs-concentrator/templates/service.yaml b/charts/lagoon-logs-concentrator/templates/service.yaml index 46597e79cd..ffaaa3cc9a 100644 --- a/charts/lagoon-logs-concentrator/templates/service.yaml +++ b/charts/lagoon-logs-concentrator/templates/service.yaml @@ -1,6 +1,10 @@ apiVersion: v1 kind: Service metadata: +{{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} name: {{ include "lagoon-logs-concentrator.fullname" . }} labels: {{- include "lagoon-logs-concentrator.labels" . | nindent 4 }} diff --git a/charts/lagoon-logs-concentrator/values.yaml b/charts/lagoon-logs-concentrator/values.yaml index aa682a41cb..dab9683d97 100644 --- a/charts/lagoon-logs-concentrator/values.yaml +++ b/charts/lagoon-logs-concentrator/values.yaml @@ -39,6 +39,8 @@ securityContext: {} service: type: ClusterIP port: 24224 + # Annotations to add to the service + annotations: {} ingress: enabled: false From a2dc4f8e661f1b3e70b1d3f275cfeaf934088de4 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Mon, 29 Jun 2020 19:16:52 -0400 Subject: [PATCH 191/280] deleting user cache --- services/api/src/apolloServer.js | 7 ++++--- services/api/src/clients/redisClient.ts | 12 ++++-------- services/api/src/models/group.ts | 9 ++++++++- services/api/src/models/user.ts | 12 +++++++++++- services/api/src/resources/user/resolvers.ts | 3 ++- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index bb266f7445..a14b110346 100644 --- a/services/api/src/apolloServer.js +++ b/services/api/src/apolloServer.js @@ -14,6 +14,7 @@ const { keycloakHasPermission } = require('./util/auth'); const { getSqlClient } = require('./clients/sqlClient'); +const redisClient = require('./clients/redisClient'); const { getKeycloakAdminClient } = require('./clients/keycloak-admin'); const logger = require('./logger'); const typeDefs = require('./typeDefs'); @@ -83,8 +84,8 @@ const apolloServer = new ApolloServer({ keycloakGrant: grant, requestCache, models: { - UserModel: User.User({ keycloakAdminClient }), - GroupModel: Group.Group({ keycloakAdminClient }), + UserModel: User.User({ keycloakAdminClient, redisClient }), + GroupModel: Group.Group({ keycloakAdminClient, redisClient }), BillingModel: BillingModel.BillingModel({ keycloakAdminClient, sqlClient @@ -138,7 +139,7 @@ const apolloServer = new ApolloServer({ keycloakGrant: req.kauth ? req.kauth.grant : null, requestCache, models: { - UserModel: User.User({ keycloakAdminClient }), + UserModel: User.User({ keycloakAdminClient, redisClient }), GroupModel: Group.Group({ keycloakAdminClient, sqlClient }), BillingModel: BillingModel.BillingModel({ keycloakAdminClient, diff --git a/services/api/src/clients/redisClient.ts b/services/api/src/clients/redisClient.ts index 3392abbfbf..2748b217c2 100644 --- a/services/api/src/clients/redisClient.ts +++ b/services/api/src/clients/redisClient.ts @@ -10,9 +10,9 @@ redisClient.on("error", function(error) { console.error(error); }); -let redisGetAsync = promisify(redisClient.get).bind(redisClient); +// let redisGetAsync = promisify(redisClient.get).bind(redisClient); let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); -let redisHDelAsync = promisify(redisClient.hdel).bind(redisClient); +let redisDelAsync = promisify(redisClient.del).bind(redisClient); interface IUserResourceScope { resource: string, @@ -43,14 +43,10 @@ export const saveRedisCache = async (resourceScope: IUserResourceScope, value: n await redisClient.hmset(`cache:authz:${resourceScope.currentUserId}`, key, value); } -export const deleteRedisCacheForScope = async (resourceScope: IUserResourceScope) => { - const key = hashKey(resourceScope); - const result = await redisHDelAsync(`cache:authz:${resourceScope.currentUserId}`, key); - return result; -} +export const deleteRedisUserCache = (userId) => redisDelAsync(`cache:authz:${userId}`); export default { isRedisCacheAllowed, saveRedisCache, - deleteRedisCacheForScope + deleteRedisUserCache }; \ No newline at end of file diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index d184e63865..32bde13a24 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -98,7 +98,7 @@ const attributeKVOrNull = (key: string, group: GroupRepresentation) => String(R.pathOr(null, ['attributes', key], group)); export const Group = (clients) => { - const { keycloakAdminClient } = clients; + const { keycloakAdminClient, redisClient } = clients; const transformKeycloakGroups = async ( keycloakGroups: GroupRepresentation[], @@ -523,6 +523,13 @@ export const Group = (clients) => { } catch (err) { throw new Error(`Could not remove user from group: ${err.message}`); } + + try { + await redisClient.deleteRedisUserCache(user.id) + } catch(err) { + throw new Error(`Error deleting user cache ${user.id}: ${err}`); + } + } return await loadGroupById(group.id); diff --git a/services/api/src/models/user.ts b/services/api/src/models/user.ts index 659fede608..eacf1afbc8 100644 --- a/services/api/src/models/user.ts +++ b/services/api/src/models/user.ts @@ -37,6 +37,7 @@ interface UserModel { addUser: (userInput: User) => Promise; updateUser: (userInput: UserEdit) => Promise; deleteUser: (id: string) => Promise; + deleteRedisKeys: (id: string) => Promise; } export class UsernameExistsError extends Error { @@ -64,7 +65,7 @@ const attrCommentLens = R.compose( ); export const User = (clients): UserModel => { - const { keycloakAdminClient } = clients; + const { keycloakAdminClient, redisClient } = clients; const fetchGitlabId = async (user: User): Promise => { const identities = await keycloakAdminClient.users.listFederatedIdentities({ @@ -353,6 +354,14 @@ export const User = (clients): UserModel => { } }; + const deleteRedisKeys = async (id: string): Promise => { + try { + await redisClient.deleteRedisUserCache(id) + } catch(err) { + throw new Error(`Error deleting user cache ${id}: ${err}`); + } + } + return { loadAllUsers, loadUserById, @@ -364,5 +373,6 @@ export const User = (clients): UserModel => { addUser, updateUser, deleteUser, + deleteRedisKeys, } }; diff --git a/services/api/src/resources/user/resolvers.ts b/services/api/src/resources/user/resolvers.ts index dcc4619b58..a4577e972e 100644 --- a/services/api/src/resources/user/resolvers.ts +++ b/services/api/src/resources/user/resolvers.ts @@ -96,8 +96,8 @@ export const deleteUser: ResolverFn = async ( users: [user.id], }); + await models.UserModel.deleteRedisKeys(user.id) await models.UserModel.deleteUser(user.id); - // TODO remove user ssh keys return 'success'; @@ -116,6 +116,7 @@ export const deleteAllUsers: ResolverFn = async ( for (const user of users) { try { await models.UserModel.deleteUser(user.id) + await models.UserModel.deleteRedisKeys(user.id) } catch (err) { deleteErrors = [ ...deleteErrors, From f00c48e70bf6f271016d728986e06b269756d4d8 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Tue, 30 Jun 2020 13:31:40 +1200 Subject: [PATCH 192/280] Updates healthz-php version to 0.0.3 --- images/php/fpm/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/php/fpm/Dockerfile b/images/php/fpm/Dockerfile index fb432394ae..cc318751e0 100644 --- a/images/php/fpm/Dockerfile +++ b/images/php/fpm/Dockerfile @@ -14,7 +14,7 @@ ENV COMPOSER_VERSION=1.10.7 \ RUN curl -L -o /tmp/composer https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \ && echo "$COMPOSER_HASH_SHA256 /tmp/composer" | sha256sum \ && chmod +x /tmp/composer \ - && php -d memory_limit=-1 /tmp/composer create-project amazeeio/healthz-php /healthz-php \ + && php -d memory_limit=-1 /tmp/composer create-project amazeeio/healthz-php /healthz-php v0.0.3 \ && rm /tmp/composer From 7ca112b7391aa687f622886b9db22d187519250f Mon Sep 17 00:00:00 2001 From: Bastian Widmer Date: Tue, 30 Jun 2020 16:50:30 +0200 Subject: [PATCH 193/280] Update to new webhook url --- docs/using_lagoon/configure_webhooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using_lagoon/configure_webhooks.md b/docs/using_lagoon/configure_webhooks.md index 0f3dfe440e..27f6c5b2ef 100644 --- a/docs/using_lagoon/configure_webhooks.md +++ b/docs/using_lagoon/configure_webhooks.md @@ -7,7 +7,7 @@ Your Lagoon administrator will also give you the route to the webhook-handler. Y - [Bitbucket](#bitbucket) !!!hint - If you are an amazee.io customer, the route to the webhook-handler is: [`https://hooks.lagoon.amazeeio.cloud`](https://hooks.lagoon.amazeeio.cloud). + If you are an amazee.io customer, the route to the webhook-handler is: [`https://webhook.amazeeio.cloud`](https://hooks.lagoon.amazeeio.cloud). !!!warning Managing the following settings will require you to have a high level of access to these repositories, which will be controlled by your organization. If you cannot access these settings, please contact your systems administrator or the appropriate person within your organization . From 28edcbe57c60a7fd42ddbc1d8cb84fa9bf4cbda4 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Tue, 30 Jun 2020 14:53:08 -0400 Subject: [PATCH 194/280] Fixing inconsistent abstraction --- services/api/src/models/group.ts | 1 - services/api/src/models/user.ts | 9 ++------- services/api/src/resources/user/resolvers.ts | 1 - 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index 32bde13a24..57265139dc 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -529,7 +529,6 @@ export const Group = (clients) => { } catch(err) { throw new Error(`Error deleting user cache ${user.id}: ${err}`); } - } return await loadGroupById(group.id); diff --git a/services/api/src/models/user.ts b/services/api/src/models/user.ts index eacf1afbc8..3f041c8842 100644 --- a/services/api/src/models/user.ts +++ b/services/api/src/models/user.ts @@ -37,7 +37,6 @@ interface UserModel { addUser: (userInput: User) => Promise; updateUser: (userInput: UserEdit) => Promise; deleteUser: (id: string) => Promise; - deleteRedisKeys: (id: string) => Promise; } export class UsernameExistsError extends Error { @@ -352,15 +351,12 @@ export const User = (clients): UserModel => { throw new Error(`Error deleting user ${id}: ${err}`); } } - }; - - const deleteRedisKeys = async (id: string): Promise => { try { await redisClient.deleteRedisUserCache(id) } catch(err) { throw new Error(`Error deleting user cache ${id}: ${err}`); } - } + }; return { loadAllUsers, @@ -372,7 +368,6 @@ export const User = (clients): UserModel => { getUserRolesForProject, addUser, updateUser, - deleteUser, - deleteRedisKeys, + deleteUser } }; diff --git a/services/api/src/resources/user/resolvers.ts b/services/api/src/resources/user/resolvers.ts index a4577e972e..8f2c4b34d8 100644 --- a/services/api/src/resources/user/resolvers.ts +++ b/services/api/src/resources/user/resolvers.ts @@ -116,7 +116,6 @@ export const deleteAllUsers: ResolverFn = async ( for (const user of users) { try { await models.UserModel.deleteUser(user.id) - await models.UserModel.deleteRedisKeys(user.id) } catch (err) { deleteErrors = [ ...deleteErrors, From 5f7f0cb1feb23bf415633ecfaafb9e4b90654841 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Tue, 30 Jun 2020 16:22:58 -0400 Subject: [PATCH 195/280] cleanup --- services/api/src/util/auth.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 5b1679d5fa..6b016f44c2 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -10,18 +10,6 @@ import { Group } from '../models/group'; const { JWTSECRET, JWTAUDIENCE } = process.env; -// const redisClient = redis.createClient({ -// host: 'api-redis', -// }); - -// redisClient.on("error", function(error) { -// console.error(error); -// }); - -// let redisGetAsync = promisify(redisClient.get).bind(redisClient); -// let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); - - interface ILegacyToken { aud: string, role: string, From 65e2872a42788c3ffed1b59b1527fbeea37b46e0 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Wed, 1 Jul 2020 08:55:53 +1200 Subject: [PATCH 196/280] Adds fast health check (#10) --- images/nginx/docker-entrypoint | 5 +++++ .../helpers/90_healthz_fast_check.conf.disabled | 13 +++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 images/nginx/helpers/90_healthz_fast_check.conf.disabled diff --git a/images/nginx/docker-entrypoint b/images/nginx/docker-entrypoint index 2894825d79..c7bdac8ae1 100755 --- a/images/nginx/docker-entrypoint +++ b/images/nginx/docker-entrypoint @@ -30,3 +30,8 @@ else echo "Setting up Healthz routing - using Lua as fallback" cp /etc/nginx/conf.d/healthz.locations.lua.disable /etc/nginx/conf.d/healthz.locations fi + +if [ "$FAST_HEALTH_CHECK" == "on" ]; then + echo "FAST HEALTH CHECK ENABLED" + cp /etc/nginx/helpers/90_healthz_fast_check.conf.disabled /etc/nginx/helpers/90_health_fast_check.conf +fi \ No newline at end of file diff --git a/images/nginx/helpers/90_healthz_fast_check.conf.disabled b/images/nginx/helpers/90_healthz_fast_check.conf.disabled new file mode 100644 index 0000000000..78cb43761e --- /dev/null +++ b/images/nginx/helpers/90_healthz_fast_check.conf.disabled @@ -0,0 +1,13 @@ +set $fhcc none; + +if ( $http_user_agent ~* "StatusCake|Pingdom|Site25x7|Uptime|nagios" ) { + set $fhcc "A"; +} + +if ( $request_method = 'GET' ) { + set $fhcc "$fhcc G"; +} + +if ( $fhcc = 'A G' ) { + rewrite ~* /.lagoonhealthz last; +} \ No newline at end of file From 4520bfb8dc3c38c885f7f819c512e2649428691e Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Wed, 1 Jul 2020 09:48:56 +1200 Subject: [PATCH 197/280] Fixes documentation to reflect correct endpoint --- images/nginx/healthcheck/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/nginx/healthcheck/README.md b/images/nginx/healthcheck/README.md index 73109acdcf..43751e2e11 100644 --- a/images/nginx/healthcheck/README.md +++ b/images/nginx/healthcheck/README.md @@ -5,6 +5,6 @@ In this directory you'll find two files - healthz.locations.php.disable - healthz.locations -Both are designed to expose a `/healthz` location from the nginx service. The difference being that the `.php.disable` file is used to point to the [healthz-php](https://github.com/amazeeio/healthz-php) application _if_ there is a PHP service attached to this application. +Both are designed to expose a `/.lagoonhealthz` location from the nginx service. The difference being that the `.php.disable` file is used to point to the [healthz-php](https://github.com/amazeeio/healthz-php) application _if_ there is a PHP service attached to this application. The logic for which of the two files are enabled are contained in this image's `docker-entrypoint` file - there we check for the existence of the env var `NGINX_FASTCGI_PASS`, which indicates (or should indicate) the presence of a PHP-fpm service. \ No newline at end of file From 5c4c5c428942696fb15fd4f242bef9b36199bdc2 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Wed, 1 Jul 2020 10:08:19 +1200 Subject: [PATCH 198/280] Changes health check builder to use the composer image, adds environment variable documentation --- docs/using_lagoon/docker_images/nginx.md | 1 + images/php/fpm/Dockerfile | 14 ++------------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/docs/using_lagoon/docker_images/nginx.md b/docs/using_lagoon/docker_images/nginx.md index 24472d9a8b..60850e0233 100644 --- a/docs/using_lagoon/docker_images/nginx.md +++ b/docs/using_lagoon/docker_images/nginx.md @@ -60,3 +60,4 @@ Environment variables are meant to contain common information for the `Nginx` co | `BASIC_AUTH` | `restricted` | By not setting `BASIC_AUTH` this will instruct Lagoon to automatically enable basic authentication if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are set. To disable basic authentication even if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are set, set `BASIC_AUTH` to `off`. | | `BASIC_AUTH_USERNAME` | \(not set\) | Username for basic authentication | | `BASIC_AUTH_PASSWORD` | \(not set\) | Password for basic authentication \(unencrypted\) | +| `FAST_HEALTH_CHECK` | \(not set\) | If set to `on` this will redirect GET requests from certain user agents (StatusCake, Pingdom, Site25x7, Uptime, nagios) to the lightweight Lagoon service healthcheck. | diff --git a/images/php/fpm/Dockerfile b/images/php/fpm/Dockerfile index cc318751e0..28eec074af 100644 --- a/images/php/fpm/Dockerfile +++ b/images/php/fpm/Dockerfile @@ -4,19 +4,9 @@ ARG ALPINE_VERSION ARG IMAGE_REPO FROM ${IMAGE_REPO:-lagoon}/commons as commons -FROM php:${PHP_IMAGE_VERSION}-fpm-alpine${ALPINE_VERSION} as healthcheckbuilder - -# Defining Versions - Composer -# @see https://getcomposer.org/download/ -ENV COMPOSER_VERSION=1.10.7 \ - COMPOSER_HASH_SHA256=b94b872729668de5b5fbf62f16ff588d2a23480dda88c0e45cb43b721b75ae29 - -RUN curl -L -o /tmp/composer https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \ - && echo "$COMPOSER_HASH_SHA256 /tmp/composer" | sha256sum \ - && chmod +x /tmp/composer \ - && php -d memory_limit=-1 /tmp/composer create-project amazeeio/healthz-php /healthz-php v0.0.3 \ - && rm /tmp/composer +FROM composer:latest as healthcheckbuilder +RUN composer create-project --no-dev amazeeio/healthz-php /healthz-php v0.0.3 FROM php:${PHP_IMAGE_VERSION}-fpm-alpine${ALPINE_VERSION} From b7b5df2a0a2155f20f72ad19f91c5293e1e6690f Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 30 Jun 2020 17:24:49 -0500 Subject: [PATCH 199/280] Allow setting REDIS_PASSWORD environment variable to protect lagoon redis base images --- images/redis/conf/redis.conf | 2 ++ images/redis/docker-entrypoint | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/images/redis/conf/redis.conf b/images/redis/conf/redis.conf index 1c82d74c42..06425ea1c6 100644 --- a/images/redis/conf/redis.conf +++ b/images/redis/conf/redis.conf @@ -11,4 +11,6 @@ maxmemory-policy allkeys-lru protected-mode no bind 0.0.0.0 +${REQUIREPASS_CONF:-} + include /etc/redis/${FLAVOR:-ephemeral}.conf diff --git a/images/redis/docker-entrypoint b/images/redis/docker-entrypoint index 93bcd95616..fafbb758ef 100755 --- a/images/redis/docker-entrypoint +++ b/images/redis/docker-entrypoint @@ -1,5 +1,13 @@ #!/bin/sh +if [[ -n "${REDIS_PASSWORD}" ]]; then + export REQUIREPASS_CONF="# Enable basic/simple authentication +# Warning: since Redis is pretty fast an outside user can try up to +# 150k passwords per second against a good box. This means that you should +# use a very strong password otherwise it will be very easy to break. +requirepass ${REDIS_PASSWORD}" +fi + ep /etc/redis/* exec "$@" From ddbc806f10358d2fa1c14ecdc4c56e2437179bff Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 30 Jun 2020 20:35:58 -0500 Subject: [PATCH 200/280] Update docs for redis environment variables --- docs/using_lagoon/docker_images/redis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using_lagoon/docker_images/redis.md b/docs/using_lagoon/docker_images/redis.md index 635ef54e02..ee89cf3896 100644 --- a/docs/using_lagoon/docker_images/redis.md +++ b/docs/using_lagoon/docker_images/redis.md @@ -25,7 +25,7 @@ Environment variables defined in Redis base image. See also [https://raw.githubu | Environment Variable | Default | Description | | :--- | :--- | :--- | -| `LOGLEVEL` | notice | Define the level of logs | | `DATABASES` | -1 | Default number of databases created at startup | +| `LOGLEVEL` | notice | Define the level of logs | | `MAXMEMORY` | 100mb | Maximum amount of memory | - +| `REDIS_PASSWORD` | disabled | Enables [authentication feature](https://redis.io/topics/security#authentication-feature) | From b96213aaf9dd62deb3c8c6b53451ee1fcde4a44b Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Wed, 1 Jul 2020 16:33:53 +0200 Subject: [PATCH 201/280] Added NOTIFYONLY option Added DEBUG option Changed how PROJECTNAME variable is get --- helpers/check_acme_routes.sh | 54 ++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/helpers/check_acme_routes.sh b/helpers/check_acme_routes.sh index f695bcf807..5b6346dc5e 100755 --- a/helpers/check_acme_routes.sh +++ b/helpers/check_acme_routes.sh @@ -5,6 +5,10 @@ # by disabling the tls-acme, removing other acme related annotations and add # an interal one for filtering purpose +if [ "$DEBUG" = "true" ]; then + set -x +fi + set -eu -o pipefail # Some variables @@ -24,6 +28,9 @@ DEBUG="${DEBUG:-"false"}" # Set a REGEX variable to filter the execution of the script REGEX=${REGEX:-".*"} +# Set NOTIFYONLY to true if you want to only notify customers about their failed routes +NOTIFYONLY=${NOTIFYONLY:-"false"} + # Help function function usage() { echo -e "The available commands are: @@ -108,7 +115,9 @@ function create_routes_array() { # Get the list of namespaces with broker routes, according to REGEX for namespace in $(oc get routes --all-namespaces|grep exposer|awk '{print $1}'|sort -u|grep -E "$REGEX") do - PROJECTNAME=$(oc get project "$namespace" -o=jsonpath="{.metadata.labels.lagoon\.sh/project}") + #PROJECTNAME=$(oc get project "$namespace" -o=jsonpath="{.metadata.labels.lagoon\.sh/project}") + PROJECTNAME=$(oc get project "$namespace" -o json|grep display-name|awk -F'[][]' '{print $2}'|tr "_" "-") + # Get the list of broken unique routes for each namespace for routelist in $(oc get -n "$namespace" route|grep exposer|awk -vNAMESPACE="$namespace" -vPROJECTNAME="$PROJECTNAME" '{print $1";"$2";"NAMESPACE";"PROJECTNAME}'|sort -u -k2 -t ";") do @@ -142,7 +151,11 @@ function check_routes() { ROUTE_PROJECTNAME=${route[3]} # Get route DNS record(s) - ROUTE_HOSTNAME_IP=$(dig +short "$ROUTE_HOSTNAME") + if [[ $(dig +short "$ROUTE_HOSTNAME" &> /dev/null; echo $?) -ne 0 ]]; then + ROUTE_HOSTNAME_IP="null" + else + ROUTE_HOSTNAME_IP=$(dig +short "$ROUTE_HOSTNAME") + fi if [[ "${DEBUG}" == true ]]; then echo -e "===== DEBUG INFORMATION =====\n${route[*]}\n$ROUTE_HOSTNAME_IP" @@ -159,22 +172,29 @@ function check_routes() { fi echo "$DNS_ERROR" - # Call the update function to update the route - update_annotation "$ROUTE_HOSTNAME" "$ROUTE_NAMESPACE" - notify_customer "$ROUTE_PROJECTNAME" - # Now once the main route is updated, it's time to get rid of exposers' routes - for j in $(oc get -n "$ROUTE_NAMESPACE" route|grep exposer|grep -E '(^|\s)'"$ROUTE_HOSTNAME"'($|\s)'|awk '{print $1";"$2}') - do - ocroute=($(echo "$j" | tr ";" "\n")) - OCROUTE_NAME=${ocroute[0]} - if [[ $DRYRUN = true ]]; then - echo -e "DRYRUN oc delete -n $ROUTE_NAMESPACE route $OCROUTE_NAME" - else - echo -e "\nDelete route $OCROUTE_NAME" - oc delete -n "$ROUTE_NAMESPACE" route "$OCROUTE_NAME" - fi - done + if [[ "$NOTIFYONLY" = "true" ]]; then + echo $NOTIFYONLY + notify_customer "$ROUTE_PROJECTNAME" + else + echo $NOTIFYONLY + # Call the update function to update the route + update_annotation "$ROUTE_HOSTNAME" "$ROUTE_NAMESPACE" + notify_customer "$ROUTE_PROJECTNAME" + + # Now once the main route is updated, it's time to get rid of exposers' routes + for j in $(oc get -n "$ROUTE_NAMESPACE" route|grep exposer|grep -E '(^|\s)'"$ROUTE_HOSTNAME"'($|\s)'|awk '{print $1";"$2}') + do + ocroute=($(echo "$j" | tr ";" "\n")) + OCROUTE_NAME=${ocroute[0]} + if [[ $DRYRUN = true ]]; then + echo -e "DRYRUN oc delete -n $ROUTE_NAMESPACE route $OCROUTE_NAME" + else + echo -e "\nDelete route $OCROUTE_NAME" + oc delete -n "$ROUTE_NAMESPACE" route "$OCROUTE_NAME" + fi + done + fi fi echo -e "\n" From 4fccdef02e4ee021ea54dcf6403147e5f43abe50 Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Wed, 1 Jul 2020 16:50:14 +0200 Subject: [PATCH 202/280] Fixed the downgit link --- docs/using_lagoon/drupal/lagoonize.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using_lagoon/drupal/lagoonize.md b/docs/using_lagoon/drupal/lagoonize.md index 83b99a36f5..90826dc72b 100644 --- a/docs/using_lagoon/drupal/lagoonize.md +++ b/docs/using_lagoon/drupal/lagoonize.md @@ -4,7 +4,7 @@ In order for Drupal to work with Lagoon, we need to teach Drupal about Lagoon and Lagoon about Drupal. This happens by copying specific YAML and PHP Files into your Git repository. -You find [these Files in our GitHub repository](https://github.com/amazeeio/lagoon/tree/master/docs/using_lagoon/drupal); the easiest way is to [download these files as a ZIP file](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/amazeeio/lagoon/tree/master/docs/using_lagoon/drupal) and copy them into your Git repository. For each Drupal version and database type you will find an individual folder. A short overview of what they are: +You find [these Files in our GitHub repository](https://github.com/amazeeio/lagoon/tree/master/docs/using_lagoon/drupal); the easiest way is to [download these files as a ZIP file](https://downgit.github.io/#/home?url=https://github.com/amazeeio/lagoon/tree/master/docs/using_lagoon/drupal) and copy them into your Git repository. For each Drupal version and database type you will find an individual folder. A short overview of what they are: - `.lagoon.yml` - The main file that will be used by Lagoon to understand what should be deployed and many more things. This file has some sensible Drupal defaults, if you would like to edit or modify, please check the specific [Documentation for .lagoon.yml](../lagoon_yml.md) - `docker-compose.yml`, `.dockerignore`, and `*.dockerfile` (or `Dockerfile`) - These files are used to run your local Drupal development environment, they tell Docker which services to start and how to build them. They contain sensible defaults and many commented lines. iWe hope that it's well-commented enough to be self-describing. If you would like to find out more, see [Documentation for docker-compose.yml](../docker-compose_yml.md) From 85d8e7fe92d5e0196f25583761c243051daebcdf Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Thu, 2 Jul 2020 16:31:59 +0200 Subject: [PATCH 203/280] Minor fixes --- helpers/check_acme_routes.sh | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/helpers/check_acme_routes.sh b/helpers/check_acme_routes.sh index 5b6346dc5e..788d5cb568 100755 --- a/helpers/check_acme_routes.sh +++ b/helpers/check_acme_routes.sh @@ -5,13 +5,16 @@ # by disabling the tls-acme, removing other acme related annotations and add # an interal one for filtering purpose +set -eu -o pipefail + +# Set DEBUG variable to true, to start bash in debug mode +DEBUG="${DEBUG:-"false"}" if [ "$DEBUG" = "true" ]; then set -x fi -set -eu -o pipefail - # Some variables + # Cluster full hostname and API hostname CLUSTER_HOSTNAME="${CLUSTER_HOSTNAME:-""}" CLUSTER_API_HOSTNAME="${CLUSTER_API_HOSTNAME:-"$CLUSTER_HOSTNAME"}" @@ -22,13 +25,14 @@ COMMAND=${1:-"help"} # Set DRYRUN variable to true to run in dry-run mode DRYRUN="${DRYRUN:-"false"}" -# Set DEBUG variable to true, to print echo messages -DEBUG="${DEBUG:-"false"}" # Set a REGEX variable to filter the execution of the script REGEX=${REGEX:-".*"} -# Set NOTIFYONLY to true if you want to only notify customers about their failed routes +# Set NOTIFYONLY to true if you want to send customers a notification +# explaining why Lagoon is not able to issue Let'S Encrypt certificate for +# some routes defined in customer's .lagoon.yml file. +# If set to true, no other action rather than notification is done (ie: no annotation or deletion) NOTIFYONLY=${NOTIFYONLY:-"false"} # Help function @@ -115,7 +119,6 @@ function create_routes_array() { # Get the list of namespaces with broker routes, according to REGEX for namespace in $(oc get routes --all-namespaces|grep exposer|awk '{print $1}'|sort -u|grep -E "$REGEX") do - #PROJECTNAME=$(oc get project "$namespace" -o=jsonpath="{.metadata.labels.lagoon\.sh/project}") PROJECTNAME=$(oc get project "$namespace" -o json|grep display-name|awk -F'[][]' '{print $2}'|tr "_" "-") # Get the list of broken unique routes for each namespace @@ -128,10 +131,6 @@ function create_routes_array() { # Create a sorted array of unique route to check ROUTES_ARRAY_SORTED=($(sort -u -k 2 -t ";"<<<"${ROUTES_ARRAY[*]}")) - - if [[ "${DEBUG}" == true ]]; then - echo -e "===== DEBUG INFORMATION =====\n${ROUTES_ARRAY_SORTED[*]}" - fi } # Function to check the routes, update them and delete the exposer's routes @@ -157,10 +156,6 @@ function check_routes() { ROUTE_HOSTNAME_IP=$(dig +short "$ROUTE_HOSTNAME") fi - if [[ "${DEBUG}" == true ]]; then - echo -e "===== DEBUG INFORMATION =====\n${route[*]}\n$ROUTE_HOSTNAME_IP" - fi - # Check if the route matches the Cluster's IP(s) if echo "$ROUTE_HOSTNAME_IP" | grep -E -q -v "${CLUSTER_IPS[*]}"; then @@ -171,13 +166,12 @@ function check_routes() { DNS_ERROR="$ROUTE_HOSTNAME in $ROUTE_NAMESPACE has no DNS record poiting to ${CLUSTER_IPS[*]} and going to disable tls-acme" fi + # Print the error on stdout echo "$DNS_ERROR" if [[ "$NOTIFYONLY" = "true" ]]; then - echo $NOTIFYONLY notify_customer "$ROUTE_PROJECTNAME" else - echo $NOTIFYONLY # Call the update function to update the route update_annotation "$ROUTE_HOSTNAME" "$ROUTE_NAMESPACE" notify_customer "$ROUTE_PROJECTNAME" @@ -238,7 +232,7 @@ function notify_customer() { echo -e "Sending notification into ${CHANNEL}" # Execute curl to send message into the channel if [[ $DRYRUN = true ]]; then - echo "DRYRUN on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" "$WEBHOOK"" + echo "DRYRUN Sending notification on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data '{'"$PAYLOAD"'}' "$WEBHOOK"" else curl -X POST -H 'Content-type: application/json' --data '{'"${PAYLOAD}"'}' ${WEBHOOK} fi From 39e69a880ad17052a5dd88971b3de963cccf40c4 Mon Sep 17 00:00:00 2001 From: Tyler Ward Date: Thu, 2 Jul 2020 11:27:53 -0700 Subject: [PATCH 204/280] provide separate query for looking up billingGroup and status page ID --- node-packages/commons/src/api.ts | 8 ++++++++ services/kubernetesbuilddeploy/src/index.ts | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/node-packages/commons/src/api.ts b/node-packages/commons/src/api.ts index 494913d359..293d9df28f 100644 --- a/node-packages/commons/src/api.ts +++ b/node-packages/commons/src/api.ts @@ -1018,6 +1018,14 @@ export const getOpenShiftInfoForProject = (project: string): Promise => value scope } + } + } +`); + +export const getBillingGroupForProject = (project: string): Promise => + graphqlapi.query(` + { + project:projectByName(name: "${project}"){ groups { ... on BillingGroup { type diff --git a/services/kubernetesbuilddeploy/src/index.ts b/services/kubernetesbuilddeploy/src/index.ts index 86bfa82c80..d701d16802 100644 --- a/services/kubernetesbuilddeploy/src/index.ts +++ b/services/kubernetesbuilddeploy/src/index.ts @@ -6,7 +6,7 @@ import sha1 from 'sha1'; import crypto from 'crypto'; import moment from 'moment'; import { logger } from '@lagoon/commons/dist/local-logging'; -import { getOpenShiftInfoForProject, addOrUpdateEnvironment, getEnvironmentByName, addDeployment } from '@lagoon/commons/dist/api'; +import { getOpenShiftInfoForProject, addOrUpdateEnvironment, getEnvironmentByName, addDeployment, getBillingGroupForProject } from '@lagoon/commons/dist/api'; import { sendToLagoonLogs, initSendToLagoonLogs } from '@lagoon/commons/dist/logs'; import { consumeTasks, initSendToLagoonTasks, createTaskMonitor } from '@lagoon/commons/dist/tasks'; @@ -44,6 +44,8 @@ const messageConsumer = async msg => { const result = await getOpenShiftInfoForProject(projectName); const projectOpenShift = result.project + const billingGroupResult = await getBillingGroupForProject(projectName); + const projectBillingGroup = billingGroupResult.project try { @@ -94,7 +96,7 @@ const messageConsumer = async msg => { alertContactSA = monitoringConfig.uptimerobot.alertContactSA || "" } var availability = projectOpenShift.availability || "STANDARD" - const billingGroup = projectOpenShift.groups.find(i => i.type == "billing" ) || "" + const billingGroup = projectBillingGroup.groups.find(i => i.type == "billing" ) || "" var uptimeRobotStatusPageId = billingGroup.uptimeRobotStatusPageId || "" } catch(error) { logger.error(`Error while loading information for project ${projectName}`) From ec39bc2ae41cbbbb5ebf71e2adba57a98f20effd Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Thu, 2 Jul 2020 14:28:40 -0400 Subject: [PATCH 205/280] Translation issues --- .../ui/src/components/BillingGroupInvoice/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/ui/src/components/BillingGroupInvoice/index.js b/services/ui/src/components/BillingGroupInvoice/index.js index 9b8793c4bb..e81de14156 100644 --- a/services/ui/src/components/BillingGroupInvoice/index.js +++ b/services/ui/src/components/BillingGroupInvoice/index.js @@ -87,7 +87,7 @@ const Invoice = ({ cost, language }) => {
:
- Total: {cost.environmentCostDescription.prod.quantity.toFixed(2).toLocaleString()} Std. + Total: {cost.environmentCostDescription.prod.quantity} Std.
}
@@ -136,13 +136,13 @@ const Invoice = ({ cost, language }) => { { lang === LANGS.ENGLISH ?
Additional Storage Fee
- Storage per GB/day: {currencyChar} {cost.storageCostDescription.unitPrice}
+ Storage per GB/day: {currencyChar} {cost.storageCostDescription.unitPrice}

Average Storage per Environment per day:
:
Zusätzliche Storagegebühren
- Storage GB/Tag: {currencyChar} {cost.storageCostDescription.unitPrice}
+ Storage GB/Tag: {currencyChar} {cost.storageCostDescription.unitPrice}

Durchschnittlicher Storage pro Environment pro Tag:
} @@ -191,7 +191,7 @@ const Invoice = ({ cost, language }) => { additional > 0 &&
{name} - {hours} { lang === LANGS.ENGLISH ? `h` : `Std.` } -
Included hours - {included} { lang === LANGS.ENGLISH ? `h` : `Std.` }
+
{ lang === LANGS.ENGLISH ? `Included hours` : `Zusätzliche Stunden` } - {included} { lang === LANGS.ENGLISH ? `h` : `Std.` }
{ additional !== 0 &&
{ lang === LANGS.ENGLISH ? `Additional hours` : `Zusätzliche Stunden` } - {additional} { lang === LANGS.ENGLISH ? `h` : `Std.` }
}
) @@ -317,7 +317,7 @@ const Invoice = ({ cost, language }) => { width: 100%; } - + .qty, .unitPrice, .amt, .data-cell.total { text-align: right; padding-right: 20px; From 4c4184c2c6497f3e4cb50d565290a6fa0422a443 Mon Sep 17 00:00:00 2001 From: Tyler Ward Date: Thu, 2 Jul 2020 11:31:00 -0700 Subject: [PATCH 206/280] do the same for openshiftbuilddeploy too --- services/openshiftbuilddeploy/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/openshiftbuilddeploy/src/index.ts b/services/openshiftbuilddeploy/src/index.ts index 313938950f..b0f3b50db3 100644 --- a/services/openshiftbuilddeploy/src/index.ts +++ b/services/openshiftbuilddeploy/src/index.ts @@ -5,7 +5,7 @@ import R from 'ramda'; import sha1 from 'sha1'; import crypto from 'crypto'; import { logger } from '@lagoon/commons/dist/local-logging'; -import { getOpenShiftInfoForProject, addOrUpdateEnvironment, getEnvironmentByName, addDeployment } from '@lagoon/commons/dist/api'; +import { getOpenShiftInfoForProject, addOrUpdateEnvironment, getEnvironmentByName, addDeployment, getBillingGroupForProject } from '@lagoon/commons/dist/api'; import { sendToLagoonLogs, initSendToLagoonLogs } from '@lagoon/commons/dist/logs'; import { consumeTasks, initSendToLagoonTasks, createTaskMonitor } from '@lagoon/commons/dist/tasks'; @@ -39,6 +39,8 @@ const messageConsumer = async msg => { const result = await getOpenShiftInfoForProject(projectName); const projectOpenShift = result.project + const billingGroupResult = await getBillingGroupForProject(projectName); + const projectBillingGroup = billingGroupResult.project const ocsafety = string => string.toLocaleLowerCase().replace(/[^0-9a-z-]/g,'-') @@ -95,7 +97,7 @@ const messageConsumer = async msg => { alertContactSA = monitoringConfig.uptimerobot.alertContactSA || "" } var availability = projectOpenShift.availability || "STANDARD" - const billingGroup = projectOpenShift.groups.find(i => i.type == "billing" ) || "" + const billingGroup = projectBillingGroup.groups.find(i => i.type == "billing" ) || "" var uptimeRobotStatusPageId = billingGroup.uptimeRobotStatusPageId || "" } catch(error) { logger.error(`Error while loading information for project ${projectName}`) From de9194b35867c49ab9be82f2af262b198c9be9f5 Mon Sep 17 00:00:00 2001 From: Tyler Ward Date: Thu, 2 Jul 2020 13:47:32 -0700 Subject: [PATCH 207/280] #1976 - always explicitly use the k3s-lagoon kubeconfig --- Makefile | 58 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index 2d206067d3..c19e88ea6c 100644 --- a/Makefile +++ b/Makefile @@ -1028,31 +1028,31 @@ endif --volume $$PWD/local-dev/k3d-nginx-ingress.yaml:/var/lib/rancher/k3s/server/manifests/k3d-nginx-ingress.yaml echo "$(K3D_NAME)" > $@ export KUBECONFIG="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')"; \ - local-dev/kubectl apply -f $$PWD/local-dev/k3d-storageclass-bulk.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f $$PWD/local-dev/k3d-storageclass-bulk.yaml; \ docker tag $(CI_BUILD_TAG)/docker-host localhost:5000/lagoon/docker-host; \ docker push localhost:5000/lagoon/docker-host; \ - local-dev/kubectl create namespace k8up; \ - local-dev/helm/helm repo add appuio https://charts.appuio.ch; \ - local-dev/helm/helm upgrade --install -n k8up k8up appuio/k8up; \ - local-dev/kubectl create namespace dioscuri; \ - local-dev/helm/helm repo add dioscuri https://raw.githubusercontent.com/amazeeio/dioscuri/ingress/charts ; \ - local-dev/helm/helm upgrade --install -n dioscuri dioscuri dioscuri/dioscuri ; \ - local-dev/kubectl create namespace dbaas-operator; \ - local-dev/helm/helm repo add dbaas-operator https://raw.githubusercontent.com/amazeeio/dbaas-operator/master/charts ; \ - local-dev/helm/helm upgrade --install -n dbaas-operator dbaas-operator dbaas-operator/dbaas-operator ; \ - local-dev/helm/helm upgrade --install -n dbaas-operator mariadbprovider dbaas-operator/mariadbprovider -f local-dev/helm-values-mariadbprovider.yml ; \ - local-dev/kubectl create namespace lagoon; \ - local-dev/helm/helm upgrade --install -n lagoon lagoon-remote ./charts/lagoon-remote --set dockerHost.image.name=172.17.0.1:5000/lagoon/docker-host --set dockerHost.registry=172.17.0.1:5000; \ - local-dev/kubectl -n lagoon rollout status deployment docker-host -w; + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" create namespace k8up; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" repo add appuio https://charts.appuio.ch; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n k8up k8up appuio/k8up; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" create namespace dioscuri; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" repo add dioscuri https://raw.githubusercontent.com/amazeeio/dioscuri/ingress/charts ; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n dioscuri dioscuri dioscuri/dioscuri ; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" create namespace dbaas-operator; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" repo add dbaas-operator https://raw.githubusercontent.com/amazeeio/dbaas-operator/master/charts ; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n dbaas-operator dbaas-operator dbaas-operator/dbaas-operator ; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n dbaas-operator mariadbprovider dbaas-operator/mariadbprovider -f local-dev/helm-values-mariadbprovider.yml ; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" create namespace lagoon; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n lagoon lagoon-remote ./charts/lagoon-remote --set dockerHost.image.name=172.17.0.1:5000/lagoon/docker-host --set dockerHost.registry=172.17.0.1:5000; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon rollout status deployment docker-host -w; ifeq ($(ARCH), darwin) export KUBECONFIG="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')"; \ - KUBERNETESBUILDDEPLOY_TOKEN=$$(local-dev/kubectl -n lagoon describe secret $$(local-dev/kubectl -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'); \ + KUBERNETESBUILDDEPLOY_TOKEN=$$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'); \ sed -i '' -e "s/\".*\" # make-kubernetes-token/\"$${KUBERNETESBUILDDEPLOY_TOKEN}\" # make-kubernetes-token/g" local-dev/api-data/03-populate-api-data-kubernetes.gql; \ DOCKER_IP="$$(docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}')"; \ sed -i '' -e "s/172\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/$${DOCKER_IP}/g" local-dev/api-data/03-populate-api-data-kubernetes.gql docker-compose.yaml; else export KUBECONFIG="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')"; \ - KUBERNETESBUILDDEPLOY_TOKEN=$$(local-dev/kubectl -n lagoon describe secret $$(local-dev/kubectl -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'); \ + KUBERNETESBUILDDEPLOY_TOKEN=$$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'); \ sed -i "s/\".*\" # make-kubernetes-token/\"$${KUBERNETESBUILDDEPLOY_TOKEN}\" # make-kubernetes-token/g" local-dev/api-data/03-populate-api-data-kubernetes.gql; \ DOCKER_IP="$$(docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}')"; \ sed -i "s/172\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/$${DOCKER_IP}/g" local-dev/api-data/03-populate-api-data-kubernetes.gql docker-compose.yaml; @@ -1074,25 +1074,25 @@ k3d-kubeconfig: k3d-dashboard: export KUBECONFIG="$$(./local-dev/k3d get-kubeconfig --name=$$(cat k3d))"; \ - local-dev/kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/00_dashboard-namespace.yaml; \ - local-dev/kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/01_dashboard-serviceaccount.yaml; \ - local-dev/kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/02_dashboard-service.yaml; \ - local-dev/kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/03_dashboard-secret.yaml; \ - local-dev/kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/04_dashboard-configmap.yaml; \ - echo '{"apiVersion": "rbac.authorization.k8s.io/v1","kind": "ClusterRoleBinding","metadata": {"name": "kubernetes-dashboard","namespace": "kubernetes-dashboard"},"roleRef": {"apiGroup": "rbac.authorization.k8s.io","kind": "ClusterRole","name": "cluster-admin"},"subjects": [{"kind": "ServiceAccount","name": "kubernetes-dashboard","namespace": "kubernetes-dashboard"}]}' | local-dev/kubectl -n kubernetes-dashboard apply -f - ; \ - local-dev/kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/06_dashboard-deployment.yaml; \ - local-dev/kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/07_scraper-service.yaml; \ - local-dev/kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/08_scraper-deployment.yaml; \ - local-dev/kubectl -n kubernetes-dashboard patch deployment kubernetes-dashboard --patch '{"spec": {"template": {"spec": {"containers": [{"name": "kubernetes-dashboard","args": ["--auto-generate-certificates","--namespace=kubernetes-dashboard","--enable-skip-login"]}]}}}}'; \ - local-dev/kubectl -n kubernetes-dashboard rollout status deployment kubernetes-dashboard -w; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/00_dashboard-namespace.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/01_dashboard-serviceaccount.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/02_dashboard-service.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/03_dashboard-secret.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/04_dashboard-configmap.yaml; \ + echo '{"apiVersion": "rbac.authorization.k8s.io/v1","kind": "ClusterRoleBinding","metadata": {"name": "kubernetes-dashboard","namespace": "kubernetes-dashboard"},"roleRef": {"apiGroup": "rbac.authorization.k8s.io","kind": "ClusterRole","name": "cluster-admin"},"subjects": [{"kind": "ServiceAccount","name": "kubernetes-dashboard","namespace": "kubernetes-dashboard"}]}' | local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n kubernetes-dashboard apply -f - ; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/06_dashboard-deployment.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/07_scraper-service.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/08_scraper-deployment.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n kubernetes-dashboard patch deployment kubernetes-dashboard --patch '{"spec": {"template": {"spec": {"containers": [{"name": "kubernetes-dashboard","args": ["--auto-generate-certificates","--namespace=kubernetes-dashboard","--enable-skip-login"]}]}}}}'; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n kubernetes-dashboard rollout status deployment kubernetes-dashboard -w; \ open http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ ; \ - local-dev/kubectl proxy + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" proxy k8s-dashboard: kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended.yaml; \ kubectl -n kubernetes-dashboard rollout status deployment kubernetes-dashboard -w; \ echo -e "\nUse this token:"; \ - kubectl -n lagoon describe secret $$(local-dev/kubectl -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'; \ + kubectl -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'; \ open http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ ; \ kubectl proxy From a72601eb427566e7227d791a32d0c1654abb46ed Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Thu, 2 Jul 2020 18:24:39 -0500 Subject: [PATCH 208/280] Updating kubectl-build-deploy-dind to use the correct tag when deploying image SHA's with more than one tag associated with them. --- .../build-deploy-docker-compose.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 34732be852..4c6b1679f3 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -916,10 +916,15 @@ elif [ "$BUILD_TYPE" == "pullrequest" ] || [ "$BUILD_TYPE" == "branch" ]; then parallel --retries 4 < /kubectl-build-deploy/lagoon/push fi + + # load the image hashes for just pushed Images for IMAGE_NAME in "${!IMAGES_BUILD[@]}" do - IMAGE_HASHES[${IMAGE_NAME}]=$(docker inspect ${REGISTRY}/${PROJECT}/${ENVIRONMENT}/${IMAGE_NAME}:${IMAGE_TAG:-latest} --format '{{index .RepoDigests 0}}') + JQ_QUERY=(jq -r ".[]|select(test(\"${REGISTRY}/${PROJECT}/${ENVIRONMENT}/${IMAGE_NAME}\"))") + docker inspect ${REGISTRY}/${PROJECT}/${ENVIRONMENT}/${IMAGE_NAME}:${IMAGE_TAG:-latest} --format '{{json .RepoDigests}}' + echo $JQ_QUERY + IMAGE_HASHES[${IMAGE_NAME}]=$(docker inspect ${REGISTRY}/${PROJECT}/${ENVIRONMENT}/${IMAGE_NAME}:${IMAGE_TAG:-latest} --format '{{json .RepoDigests}}' | "${JQ_QUERY[@]}") done # elif [ "$BUILD_TYPE" == "promote" ]; then From 6785a90109f43b5706ef8e3f74fcce40103855cd Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Thu, 2 Jul 2020 18:27:13 -0500 Subject: [PATCH 209/280] Removing testing print statements --- images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 4c6b1679f3..65c4a0f742 100755 --- a/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh +++ b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh @@ -922,8 +922,6 @@ elif [ "$BUILD_TYPE" == "pullrequest" ] || [ "$BUILD_TYPE" == "branch" ]; then for IMAGE_NAME in "${!IMAGES_BUILD[@]}" do JQ_QUERY=(jq -r ".[]|select(test(\"${REGISTRY}/${PROJECT}/${ENVIRONMENT}/${IMAGE_NAME}\"))") - docker inspect ${REGISTRY}/${PROJECT}/${ENVIRONMENT}/${IMAGE_NAME}:${IMAGE_TAG:-latest} --format '{{json .RepoDigests}}' - echo $JQ_QUERY IMAGE_HASHES[${IMAGE_NAME}]=$(docker inspect ${REGISTRY}/${PROJECT}/${ENVIRONMENT}/${IMAGE_NAME}:${IMAGE_TAG:-latest} --format '{{json .RepoDigests}}' | "${JQ_QUERY[@]}") done From 51aed8e94df73db4c2597d38390c080884ff270c Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Fri, 3 Jul 2020 13:32:27 +1200 Subject: [PATCH 210/280] Adds openshift project name lookup to Drutiny problems processor --- .../src/handlers/problems/processDrutinyResults.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts b/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts index 61404cbb88..b99521011e 100644 --- a/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts +++ b/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts @@ -12,8 +12,11 @@ const DRUTINY_PACKAGE_NAME = '' import { getProjectByName, getEnvironmentByName, + getEnvironmentByOpenshiftProjectName, + sanitizeProjectName, } from '@lagoon/commons/dist/api'; import { generateProblemsWebhookEventName } from "./webhookHelpers"; +import * as R from 'ramda'; const ERROR_STATES = ["error", "failure"]; const SEVERITY_LEVELS = [ @@ -58,9 +61,9 @@ export async function processDrutinyResultset( lagoonProjectName ); - const { - environmentByName: environmentDetails, - } = await getEnvironmentByName(lagoonEnvironmentName, lagoonProjectId); + let openshiftProjectName = sanitizeProjectName(`${lagoonProjectName}-${lagoonEnvironmentName}`); + const environmentResult = await getEnvironmentByOpenshiftProjectName(openshiftProjectName); + const environmentDetails: any = R.prop('environmentByOpenshiftProjectName', environmentResult) const lagoonEnvironmentId = environmentDetails.id; const lagoonServiceName = DRUTINY_SERVICE_NAME; From f68762e07f0cdba4ef43a64c75c5d7dc9afb5339 Mon Sep 17 00:00:00 2001 From: Tyler Ward Date: Fri, 3 Jul 2020 10:16:10 -0700 Subject: [PATCH 211/280] #1976 - also add the context --- Makefile | 62 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index c19e88ea6c..9468a71a5f 100644 --- a/Makefile +++ b/Makefile @@ -1031,28 +1031,28 @@ endif local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f $$PWD/local-dev/k3d-storageclass-bulk.yaml; \ docker tag $(CI_BUILD_TAG)/docker-host localhost:5000/lagoon/docker-host; \ docker push localhost:5000/lagoon/docker-host; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" create namespace k8up; \ - local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" repo add appuio https://charts.appuio.ch; \ - local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n k8up k8up appuio/k8up; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" create namespace dioscuri; \ - local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" repo add dioscuri https://raw.githubusercontent.com/amazeeio/dioscuri/ingress/charts ; \ - local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n dioscuri dioscuri dioscuri/dioscuri ; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" create namespace dbaas-operator; \ - local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" repo add dbaas-operator https://raw.githubusercontent.com/amazeeio/dbaas-operator/master/charts ; \ - local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n dbaas-operator dbaas-operator dbaas-operator/dbaas-operator ; \ - local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n dbaas-operator mariadbprovider dbaas-operator/mariadbprovider -f local-dev/helm-values-mariadbprovider.yml ; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" create namespace lagoon; \ - local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" upgrade --install -n lagoon lagoon-remote ./charts/lagoon-remote --set dockerHost.image.name=172.17.0.1:5000/lagoon/docker-host --set dockerHost.registry=172.17.0.1:5000; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon rollout status deployment docker-host -w; + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' create namespace k8up; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --kube-context='$(K3D_NAME)' repo add appuio https://charts.appuio.ch; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --kube-context='$(K3D_NAME)' upgrade --install -n k8up k8up appuio/k8up; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' create namespace dioscuri; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --kube-context='$(K3D_NAME)' repo add dioscuri https://raw.githubusercontent.com/amazeeio/dioscuri/ingress/charts ; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --kube-context='$(K3D_NAME)' upgrade --install -n dioscuri dioscuri dioscuri/dioscuri ; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' create namespace dbaas-operator; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --kube-context='$(K3D_NAME)' repo add dbaas-operator https://raw.githubusercontent.com/amazeeio/dbaas-operator/master/charts ; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --kube-context='$(K3D_NAME)' upgrade --install -n dbaas-operator dbaas-operator dbaas-operator/dbaas-operator ; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --kube-context='$(K3D_NAME)' upgrade --install -n dbaas-operator mariadbprovider dbaas-operator/mariadbprovider -f local-dev/helm-values-mariadbprovider.yml ; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' create namespace lagoon; \ + local-dev/helm/helm --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --kube-context='$(K3D_NAME)' upgrade --install -n lagoon lagoon-remote ./charts/lagoon-remote --set dockerHost.image.name=172.17.0.1:5000/lagoon/docker-host --set dockerHost.registry=172.17.0.1:5000; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n lagoon rollout status deployment docker-host -w; ifeq ($(ARCH), darwin) export KUBECONFIG="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')"; \ - KUBERNETESBUILDDEPLOY_TOKEN=$$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'); \ + KUBERNETESBUILDDEPLOY_TOKEN=$$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'); \ sed -i '' -e "s/\".*\" # make-kubernetes-token/\"$${KUBERNETESBUILDDEPLOY_TOKEN}\" # make-kubernetes-token/g" local-dev/api-data/03-populate-api-data-kubernetes.gql; \ DOCKER_IP="$$(docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}')"; \ sed -i '' -e "s/172\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/$${DOCKER_IP}/g" local-dev/api-data/03-populate-api-data-kubernetes.gql docker-compose.yaml; else export KUBECONFIG="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')"; \ - KUBERNETESBUILDDEPLOY_TOKEN=$$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'); \ + KUBERNETESBUILDDEPLOY_TOKEN=$$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'); \ sed -i "s/\".*\" # make-kubernetes-token/\"$${KUBERNETESBUILDDEPLOY_TOKEN}\" # make-kubernetes-token/g" local-dev/api-data/03-populate-api-data-kubernetes.gql; \ DOCKER_IP="$$(docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}')"; \ sed -i "s/172\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/$${DOCKER_IP}/g" local-dev/api-data/03-populate-api-data-kubernetes.gql docker-compose.yaml; @@ -1074,27 +1074,27 @@ k3d-kubeconfig: k3d-dashboard: export KUBECONFIG="$$(./local-dev/k3d get-kubeconfig --name=$$(cat k3d))"; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/00_dashboard-namespace.yaml; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/01_dashboard-serviceaccount.yaml; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/02_dashboard-service.yaml; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/03_dashboard-secret.yaml; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/04_dashboard-configmap.yaml; \ - echo '{"apiVersion": "rbac.authorization.k8s.io/v1","kind": "ClusterRoleBinding","metadata": {"name": "kubernetes-dashboard","namespace": "kubernetes-dashboard"},"roleRef": {"apiGroup": "rbac.authorization.k8s.io","kind": "ClusterRole","name": "cluster-admin"},"subjects": [{"kind": "ServiceAccount","name": "kubernetes-dashboard","namespace": "kubernetes-dashboard"}]}' | local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n kubernetes-dashboard apply -f - ; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/06_dashboard-deployment.yaml; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/07_scraper-service.yaml; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/08_scraper-deployment.yaml; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n kubernetes-dashboard patch deployment kubernetes-dashboard --patch '{"spec": {"template": {"spec": {"containers": [{"name": "kubernetes-dashboard","args": ["--auto-generate-certificates","--namespace=kubernetes-dashboard","--enable-skip-login"]}]}}}}'; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n kubernetes-dashboard rollout status deployment kubernetes-dashboard -w; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/00_dashboard-namespace.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/01_dashboard-serviceaccount.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/02_dashboard-service.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/03_dashboard-secret.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/04_dashboard-configmap.yaml; \ + echo '{"apiVersion": "rbac.authorization.k8s.io/v1","kind": "ClusterRoleBinding","metadata": {"name": "kubernetes-dashboard","namespace": "kubernetes-dashboard"},"roleRef": {"apiGroup": "rbac.authorization.k8s.io","kind": "ClusterRole","name": "cluster-admin"},"subjects": [{"kind": "ServiceAccount","name": "kubernetes-dashboard","namespace": "kubernetes-dashboard"}]}' | local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n kubernetes-dashboard apply -f - ; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/06_dashboard-deployment.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/07_scraper-service.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended/08_scraper-deployment.yaml; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n kubernetes-dashboard patch deployment kubernetes-dashboard --patch '{"spec": {"template": {"spec": {"containers": [{"name": "kubernetes-dashboard","args": ["--auto-generate-certificates","--namespace=kubernetes-dashboard","--enable-skip-login"]}]}}}}'; \ + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n kubernetes-dashboard rollout status deployment kubernetes-dashboard -w; \ open http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ ; \ - local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" proxy + local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' proxy k8s-dashboard: - kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended.yaml; \ - kubectl -n kubernetes-dashboard rollout status deployment kubernetes-dashboard -w; \ + kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc2/aio/deploy/recommended.yaml; \ + kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n kubernetes-dashboard rollout status deployment kubernetes-dashboard -w; \ echo -e "\nUse this token:"; \ - kubectl -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'; \ + kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n lagoon describe secret $$(local-dev/kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' -n lagoon get secret | grep kubernetesbuilddeploy | awk '{print $$1}') | grep token: | awk '{print $$2}'; \ open http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ ; \ - kubectl proxy + kubectl --kubeconfig="$$(./local-dev/k3d get-kubeconfig --name='$(K3D_NAME)')" --context='$(K3D_NAME)' proxy # Stop k3d .PHONY: k3d/stop From 9f16532e7881f96158592a5bb5fcb569d7fbf1ad Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 6 Jul 2020 09:56:24 +1000 Subject: [PATCH 212/280] add api-development make command --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 2d206067d3..b7ce3db2bd 100644 --- a/Makefile +++ b/Makefile @@ -1140,3 +1140,7 @@ rebuild-push-oc-build-deploy-dind: .PHONY: ui-development ui-development: build/api build/api-db build/local-api-data-watcher-pusher build/ui build/keycloak build/keycloak-db build/broker build/broker-single IMAGE_REPO=$(CI_BUILD_TAG) docker-compose -p $(CI_BUILD_TAG) --compatibility up -d api api-db local-api-data-watcher-pusher ui keycloak keycloak-db broker + +.PHONY: api-development +api-development: build/api build/api-db build/local-api-data-watcher-pusher build/keycloak build/keycloak-db build/broker build/broker-single + IMAGE_REPO=$(CI_BUILD_TAG) docker-compose -p $(CI_BUILD_TAG) --compatibility up -d api api-db local-api-data-watcher-pusher keycloak keycloak-db broker From cbbdccc8300ee7466ae34d2a912cb10fd1d1373c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 6 Jul 2020 02:47:41 -0500 Subject: [PATCH 213/280] Get redis connection info from environment variables --- docker-compose.yaml | 2 - services/api/src/clients/redisClient.ts | 66 ++++++++++++++++--------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 7712a3ac13..e6f6178f58 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -763,5 +763,3 @@ services: image: ${IMAGE_REPO:-lagoon}/redis labels: lagoon.type: redis - ports: - - '6397:6397' diff --git a/services/api/src/clients/redisClient.ts b/services/api/src/clients/redisClient.ts index 2748b217c2..4b92fc6bad 100644 --- a/services/api/src/clients/redisClient.ts +++ b/services/api/src/clients/redisClient.ts @@ -1,12 +1,21 @@ -import redis from "redis"; +import redis, { ClientOpts } from 'redis'; import { promisify } from 'util'; +const { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } = process.env; -const redisClient = redis.createClient({ - host: 'api-redis', -}); +let clientOptions: ClientOpts = { + host: REDIS_HOST || 'api-redis', + port: parseInt(REDIS_PORT, 10) || 6379, + enable_offline_queue: false +}; + +if (typeof REDIS_PASSWORD !== undefined) { + clientOptions.password = REDIS_PASSWORD; +} + +const redisClient = redis.createClient(clientOptions); -redisClient.on("error", function(error) { +redisClient.on('error', function(error) { console.error(error); }); @@ -15,38 +24,51 @@ let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); let redisDelAsync = promisify(redisClient.del).bind(redisClient); interface IUserResourceScope { - resource: string, - scope: string, - currentUserId: string, - project?: number, - group?: string, - users?: number[] + resource: string; + scope: string; + currentUserId: string; + project?: number; + group?: string; + users?: number[]; } const hashKey = ({ resource, project, group, scope }: IUserResourceScope) => - `${resource}:${project ? `${project}:`: ''}${group ? `${group}:`: ''}${scope}`; + `${resource}:${project ? `${project}:` : ''}${ + group ? `${group}:` : '' + }${scope}`; - -export const isRedisCacheAllowed = async (resourceScope: IUserResourceScope) => { - const redisHash = await redisHMGetAllAsync(`cache:authz:${resourceScope.currentUserId}`); +export const isRedisCacheAllowed = async ( + resourceScope: IUserResourceScope +) => { + const redisHash = await redisHMGetAllAsync( + `cache:authz:${resourceScope.currentUserId}` + ); const key = hashKey(resourceScope); if (redisHash && !redisHash[key]) { return null; } - return (redisHash && redisHash[key] === 1) ? true : false; -} + return redisHash && redisHash[key] === 1 ? true : false; +}; -export const saveRedisCache = async (resourceScope: IUserResourceScope, value: number|string) => { +export const saveRedisCache = async ( + resourceScope: IUserResourceScope, + value: number | string +) => { const key = hashKey(resourceScope); - await redisClient.hmset(`cache:authz:${resourceScope.currentUserId}`, key, value); -} + await redisClient.hmset( + `cache:authz:${resourceScope.currentUserId}`, + key, + value + ); +}; -export const deleteRedisUserCache = (userId) => redisDelAsync(`cache:authz:${userId}`); +export const deleteRedisUserCache = userId => + redisDelAsync(`cache:authz:${userId}`); export default { isRedisCacheAllowed, saveRedisCache, deleteRedisUserCache -}; \ No newline at end of file +}; From 6985a18ddfe8448507c6c7afb781e99a7c6d71a8 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 6 Jul 2020 03:33:46 -0500 Subject: [PATCH 214/280] Cache authz allow in redis. Don't error on redis errors. Code cleanup --- services/api/src/apolloServer.js | 2 +- services/api/src/clients/redisClient.ts | 11 +++-------- services/api/src/models/group.ts | 2 +- services/api/src/models/user.ts | 3 ++- services/api/src/resources/user/resolvers.ts | 1 - services/api/src/util/auth.ts | 16 ++++++++++------ 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index a14b110346..480ecf1845 100644 --- a/services/api/src/apolloServer.js +++ b/services/api/src/apolloServer.js @@ -140,7 +140,7 @@ const apolloServer = new ApolloServer({ requestCache, models: { UserModel: User.User({ keycloakAdminClient, redisClient }), - GroupModel: Group.Group({ keycloakAdminClient, sqlClient }), + GroupModel: Group.Group({ keycloakAdminClient, redisClient }), BillingModel: BillingModel.BillingModel({ keycloakAdminClient, sqlClient diff --git a/services/api/src/clients/redisClient.ts b/services/api/src/clients/redisClient.ts index 4b92fc6bad..c5b55113ba 100644 --- a/services/api/src/clients/redisClient.ts +++ b/services/api/src/clients/redisClient.ts @@ -19,7 +19,6 @@ redisClient.on('error', function(error) { console.error(error); }); -// let redisGetAsync = promisify(redisClient.get).bind(redisClient); let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); let redisDelAsync = promisify(redisClient.del).bind(redisClient); @@ -37,7 +36,7 @@ const hashKey = ({ resource, project, group, scope }: IUserResourceScope) => group ? `${group}:` : '' }${scope}`; -export const isRedisCacheAllowed = async ( +export const getRedisCache = async ( resourceScope: IUserResourceScope ) => { const redisHash = await redisHMGetAllAsync( @@ -45,11 +44,7 @@ export const isRedisCacheAllowed = async ( ); const key = hashKey(resourceScope); - if (redisHash && !redisHash[key]) { - return null; - } - - return redisHash && redisHash[key] === 1 ? true : false; + return redisHash[key]; }; export const saveRedisCache = async ( @@ -68,7 +63,7 @@ export const deleteRedisUserCache = userId => redisDelAsync(`cache:authz:${userId}`); export default { - isRedisCacheAllowed, + getRedisCache, saveRedisCache, deleteRedisUserCache }; diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index 57265139dc..538046d5cb 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -527,7 +527,7 @@ export const Group = (clients) => { try { await redisClient.deleteRedisUserCache(user.id) } catch(err) { - throw new Error(`Error deleting user cache ${user.id}: ${err}`); + logger.error(`Error deleting user cache ${user.id}: ${err}`); } } diff --git a/services/api/src/models/user.ts b/services/api/src/models/user.ts index 3f041c8842..c1262625e7 100644 --- a/services/api/src/models/user.ts +++ b/services/api/src/models/user.ts @@ -1,5 +1,6 @@ import * as R from 'ramda'; import pickNonNil from '../util/pickNonNil'; +import * as logger from '../logger'; import UserRepresentation from 'keycloak-admin/lib/defs/userRepresentation'; import { Group, isRoleSubgroup } from './group'; @@ -354,7 +355,7 @@ export const User = (clients): UserModel => { try { await redisClient.deleteRedisUserCache(id) } catch(err) { - throw new Error(`Error deleting user cache ${id}: ${err}`); + logger.error(`Error deleting user cache ${id}: ${err}`); } }; diff --git a/services/api/src/resources/user/resolvers.ts b/services/api/src/resources/user/resolvers.ts index 8f2c4b34d8..bc40c3436b 100644 --- a/services/api/src/resources/user/resolvers.ts +++ b/services/api/src/resources/user/resolvers.ts @@ -96,7 +96,6 @@ export const deleteUser: ResolverFn = async ( users: [user.id], }); - await models.UserModel.deleteRedisKeys(user.id) await models.UserModel.deleteUser(user.id); // TODO remove user ssh keys diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 6b016f44c2..49603e4230 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -1,7 +1,5 @@ import * as R from 'ramda'; -// import redis from "redis"; -// import { promisify } from 'util'; -import { isRedisCacheAllowed, saveRedisCache } from '../clients/redisClient'; +import { getRedisCache, saveRedisCache } from '../clients/redisClient'; import { verify } from 'jsonwebtoken'; import * as logger from '../logger'; import { keycloakGrantManager } from'../clients/keycloakClient'; @@ -114,12 +112,18 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) // or group context) and cache a single query instead? const cacheKey = `${currentUserId}:${resource}:${scope}:${JSON.stringify(attributes)}`; const cachedPermissions = requestCache.get(cacheKey); - if (cachedPermissions !== undefined) { - return cachedPermissions; + if (cachedPermissions === true) { + return true; + } else if (!cachedPermissions === false) { + throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); } + // Check a redis cache before doing a full keycloak lookup. const resourceScope = {resource, scope, currentUserId, ...attributes }; - if (!isRedisCacheAllowed(resourceScope)){ + const redisCache = await getRedisCache(resourceScope); + if (redisCache == 1) { + return true; + } else if (redisCache == 0) { throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); } From 05312104ce332a54506c02f2394ba71d7355bc19 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 1 Jul 2020 14:41:52 +0800 Subject: [PATCH 215/280] Update for lagoon-logs-concentrator to use internal CA --- charts/lagoon-logs-concentrator/README.md | 116 +++--------------- .../lagoon-logs-concentrator/ca-config.json | 25 ++++ charts/lagoon-logs-concentrator/ca-csr.json | 13 ++ charts/lagoon-logs-concentrator/client.json | 10 ++ charts/lagoon-logs-concentrator/server.json | 10 ++ 5 files changed, 76 insertions(+), 98 deletions(-) create mode 100644 charts/lagoon-logs-concentrator/ca-config.json create mode 100644 charts/lagoon-logs-concentrator/ca-csr.json create mode 100644 charts/lagoon-logs-concentrator/client.json create mode 100644 charts/lagoon-logs-concentrator/server.json diff --git a/charts/lagoon-logs-concentrator/README.md b/charts/lagoon-logs-concentrator/README.md index 24973317ab..4335be7c4f 100644 --- a/charts/lagoon-logs-concentrator/README.md +++ b/charts/lagoon-logs-concentrator/README.md @@ -13,121 +13,41 @@ Clients connect to this service via TLS. Mutual TLS authentication is performed Important notes: -* Certificates are generated on the cluster `logs-concentrator` runs in so they share the same CA. +* We run our own CA since the in-cluster CA signs certificates with only one year expiry. * The instructions below require [cfssl](https://github.com/cloudflare/cfssl). -* Refer to [this documentation](https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/#create-a-certificate-signing-request) for further details. +* Refer to [this documentation](https://coreos.com/os/docs/latest/generate-self-signed-certificates.html) for further details. -### Generate a server certificate - -This will be the certificate used by the `logs-concentrator`. - -NOTE: update `CN`/`hosts` as requried. - -Generate a `server.csr` and `server-key.pem`: -``` -cat < server.crt +cfssl gencert -initca ca-csr.json | cfssljson -bare ca - +rm ca.csr ``` -### Generate a client certificate - -This will be the certificate used by the `logs-dispatcher`. - -NOTE: update `CN`/`hosts` as requried. +You'll end up with `ca-key.pem` and `ca.pem`, which are the CA key and certificate. Store these somewhere safe, they'll be used to generate all future certificates. -Generate a `client.csr` and `client-key.pem` (replace `test1` with the cluster name: -``` -cat < client.crt -``` +This will be the certificate used by the `lagoon-logging` chart's `logs-dispatcher`. -### Get the cluster CA certificate +Edit the `client.json` as required and run this command: ``` -$ kubectl run --rm -it --quiet --restart=Never --image=busybox catca -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt | tee ca.crt +cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client.json | cfssljson -bare client +rm client.csr ``` diff --git a/charts/lagoon-logs-concentrator/ca-config.json b/charts/lagoon-logs-concentrator/ca-config.json new file mode 100644 index 0000000000..213ea49e67 --- /dev/null +++ b/charts/lagoon-logs-concentrator/ca-config.json @@ -0,0 +1,25 @@ +{ + "signing": { + "default": { + "expiry": "87600h" + }, + "profiles": { + "server": { + "expiry": "87600h", + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + }, + "client": { + "expiry": "87600h", + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + } + } + } +} diff --git a/charts/lagoon-logs-concentrator/ca-csr.json b/charts/lagoon-logs-concentrator/ca-csr.json new file mode 100644 index 0000000000..91122dfc61 --- /dev/null +++ b/charts/lagoon-logs-concentrator/ca-csr.json @@ -0,0 +1,13 @@ +{ + "CN": "logs-ca.cluster1.example.com", + "hosts": [ + "logs-ca.cluster1.example.com" + ], + "key": { + "algo": "ecdsa", + "size": 256 + }, + "ca": { + "expiry": "87600h" + } +} diff --git a/charts/lagoon-logs-concentrator/client.json b/charts/lagoon-logs-concentrator/client.json new file mode 100644 index 0000000000..4813dad0cc --- /dev/null +++ b/charts/lagoon-logs-concentrator/client.json @@ -0,0 +1,10 @@ +{ + "hosts": [ + "logs-dispatcher.cluster2.example.com" + ], + "CN": "logs-dispatcher.cluster2.example.com", + "key": { + "algo": "ecdsa", + "size": 256 + } +} diff --git a/charts/lagoon-logs-concentrator/server.json b/charts/lagoon-logs-concentrator/server.json new file mode 100644 index 0000000000..326e3580a0 --- /dev/null +++ b/charts/lagoon-logs-concentrator/server.json @@ -0,0 +1,10 @@ +{ + "hosts": [ + "logs-concentrator.cluster1.example.com" + ], + "CN": "logs-concentrator.cluster1.example.com", + "key": { + "algo": "ecdsa", + "size": 256 + } +} From a0fdc91f4fcfdeb5511067615cd6dff4bc7280f4 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Jul 2020 15:02:15 +0800 Subject: [PATCH 216/280] Bump minreplicas for logs-concentrator --- charts/lagoon-logs-concentrator/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/lagoon-logs-concentrator/values.yaml b/charts/lagoon-logs-concentrator/values.yaml index dab9683d97..11ec47fbf8 100644 --- a/charts/lagoon-logs-concentrator/values.yaml +++ b/charts/lagoon-logs-concentrator/values.yaml @@ -62,7 +62,7 @@ resources: autoscaling: enabled: true - minReplicas: 1 + minReplicas: 2 maxReplicas: 4 targetCPUUtilizationPercentage: 80 From 7093cbd45fd92db14c3614e73acaa0e4f5a80356 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Fri, 3 Jul 2020 15:28:37 +0800 Subject: [PATCH 217/280] Bump logs-concentrator chart version --- charts/lagoon-logs-concentrator/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/lagoon-logs-concentrator/Chart.yaml b/charts/lagoon-logs-concentrator/Chart.yaml index a2094f312f..54bab0d507 100644 --- a/charts/lagoon-logs-concentrator/Chart.yaml +++ b/charts/lagoon-logs-concentrator/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to From b83a4e30da94c02b8d03075a7a3eb3de0317f6b5 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 6 Jul 2020 05:01:14 -0500 Subject: [PATCH 218/280] Make fewer keycloak requests when using loadGroupsByAttribute --- services/api/src/helpers/billingGroups.ts | 2 +- services/api/src/models/group.ts | 25 +++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/services/api/src/helpers/billingGroups.ts b/services/api/src/helpers/billingGroups.ts index 26e040dce3..6808daf9ca 100644 --- a/services/api/src/helpers/billingGroups.ts +++ b/services/api/src/helpers/billingGroups.ts @@ -65,7 +65,7 @@ export const getAllBillingGroupsWithoutProjects = async () => { const GroupModel = Group({keycloakAdminClient }); // Get All Billing Groups - const groupTypeFilterFn = ({ name, value }, group) => { + const groupTypeFilterFn = ({ name, value }) => { return name === 'type' && value[0] === 'billing'; }; const groups = await GroupModel.loadGroupsByAttribute(groupTypeFilterFn); diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index 538046d5cb..3d4c7e633a 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -62,7 +62,7 @@ interface GroupEdit { } interface AttributeFilterFn { - (attribute: { name: string; value: string[] }, group: Group): boolean; + (attribute: { name: string; value: string[] }): boolean; } export class GroupExistsError extends Error { @@ -121,11 +121,10 @@ export const Group = (clients) => { let groupsWithGroupsAndMembers = []; for (const group of groups) { + const subGroups = R.reject(isRoleSubgroup)(group.subGroups); groupsWithGroupsAndMembers.push({ ...group, - groups: await transformKeycloakGroups( - R.reject(isRoleSubgroup)(group.subGroups), - ), + groups: R.isEmpty(subGroups) ? [] : await transformKeycloakGroups(subGroups), members: await getGroupMembership(group), }); } @@ -219,7 +218,16 @@ export const Group = (clients) => { const loadGroupsByAttribute = async ( filterFn: AttributeFilterFn, ): Promise => { - const allGroups = await loadAllGroups(); + const keycloakGroups = await keycloakAdminClient.groups.find(); + + let fullGroups: Group[] | BillingGroup[] = []; + for (const group of keycloakGroups) { + const fullGroup = await keycloakAdminClient.groups.findOne({ + id: group.id, + }); + + fullGroups = [...fullGroups, fullGroup]; + } const filteredGroups = R.filter((group: Group) => R.pipe( @@ -231,16 +239,17 @@ export const Group = (clients) => { name: attribute[0], value: attribute[1], }, - group, ); } return isMatch; }, false), )(group.attributes), - )(allGroups); + )(fullGroups); + + const groups = await transformKeycloakGroups(filteredGroups); - return filteredGroups; + return groups; }; const loadGroupsByProjectId = async ( From 4ce7b0947b3e8ea6b3780ebd7d34d10429508096 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Mon, 6 Jul 2020 08:16:36 -0400 Subject: [PATCH 219/280] Use lagoon task system retrying instead of internal retrying this will not block other builds from being tested and make the retrying more aligned to other services --- services/kubernetesdeployqueue/src/index.ts | 65 +++++++++------------ 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/services/kubernetesdeployqueue/src/index.ts b/services/kubernetesdeployqueue/src/index.ts index 0570260470..e6a2c74f0f 100644 --- a/services/kubernetesdeployqueue/src/index.ts +++ b/services/kubernetesdeployqueue/src/index.ts @@ -20,14 +20,12 @@ import { initSendToLagoonLogs(); initSendToLagoonTasks(); -const pause = duration => new Promise(res => setTimeout(res, duration)); - -const retry = (retries, fn, delay = 1000) => - fn().catch(err => - retries > 1 - ? pause(delay).then(() => retry(retries - 1, fn, delay)) - : Promise.reject(err) - ); +class AnotherBuildAlreadyRunning extends Error { + constructor(message) { + super(message); + this.name = 'AnotherBuildAlreadyRunning'; + } +} const messageConsumer = async msg => { const { @@ -81,40 +79,31 @@ const messageConsumer = async msg => { } }); - const jobsGet = promisify( - kubernetesBatchApi.namespaces(openshiftProject).jobs.get - ); - - const hasNoActiveBuilds = () => - new Promise(async (resolve, reject) => { - const namespaceJobs = await jobsGet({ - qs: { - labelSelector: 'lagoon.sh/jobType=build' - } - }); - const activeBuilds: any = R.pipe( - R.propOr([], 'items'), - R.filter(R.pathSatisfies(R.lt(0), ['status', 'active'])) - )(namespaceJobs); - - if (R.isEmpty(activeBuilds)) { - resolve(); - } else { - logger.info( - `Delaying build of ${buildName} due to ${activeBuilds.length} pending builds` - ); - reject(); + // Check that there are no active builds in this namespace running + try { + const jobsGetAll = promisify( + kubernetesBatchApi.namespaces(openshiftProject).jobs.get + ); + const namespaceJobs = await jobsGetAll({ + qs: { + labelSelector: 'lagoon.sh/jobType=build' } }); - - // Wait until an there are no active builds in this namespace running - try { - // Check every minute for 30 minutes - await retry(30, hasNoActiveBuilds, 1 * 60 * 1000); + const activeBuilds: any = R.pipe( + R.propOr([], 'items'), + R.filter(R.pathSatisfies(R.lt(0), ['status', 'active'])) + )(namespaceJobs); + + if (!R.isEmpty(activeBuilds)) { + throw new AnotherBuildAlreadyRunning( + `${openshiftProject}: Reqeueing ${buildName} due to ${activeBuilds.length} pending builds` + ); + } } catch (err) { - throw new Error( - `${openshiftProject}: Requeue build due to error: ${err.message}` + logger.error( + `${openshiftProject}: Unexpected error loading current running Jobs, unable to build ${buildName}: ${err}` ); + return; } // Load job, if not exists create From efd118f2b8398a0e4a6070bbdbbbdb769c08a085 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 6 Jul 2020 09:20:42 -0500 Subject: [PATCH 220/280] Cache project groups in redis --- services/api/src/clients/redisClient.ts | 29 +++++--- services/api/src/models/group.ts | 88 ++++++++++++++++++++----- 2 files changed, 90 insertions(+), 27 deletions(-) diff --git a/services/api/src/clients/redisClient.ts b/services/api/src/clients/redisClient.ts index c5b55113ba..d15dcb02bb 100644 --- a/services/api/src/clients/redisClient.ts +++ b/services/api/src/clients/redisClient.ts @@ -1,3 +1,4 @@ +import * as R from 'ramda'; import redis, { ClientOpts } from 'redis'; import { promisify } from 'util'; @@ -19,8 +20,10 @@ redisClient.on('error', function(error) { console.error(error); }); -let redisHMGetAllAsync = promisify(redisClient.hgetall).bind(redisClient); -let redisDelAsync = promisify(redisClient.del).bind(redisClient); +const hgetall = promisify(redisClient.hgetall).bind(redisClient); +const smembers = promisify(redisClient.smembers).bind(redisClient); +const sadd = promisify(redisClient.sadd).bind(redisClient); +const del = promisify(redisClient.del).bind(redisClient); interface IUserResourceScope { resource: string; @@ -36,15 +39,13 @@ const hashKey = ({ resource, project, group, scope }: IUserResourceScope) => group ? `${group}:` : '' }${scope}`; -export const getRedisCache = async ( - resourceScope: IUserResourceScope -) => { - const redisHash = await redisHMGetAllAsync( +export const getRedisCache = async (resourceScope: IUserResourceScope) => { + const redisHash = await hgetall( `cache:authz:${resourceScope.currentUserId}` ); const key = hashKey(resourceScope); - return redisHash[key]; + return R.prop(key, redisHash); }; export const saveRedisCache = async ( @@ -59,11 +60,19 @@ export const saveRedisCache = async ( ); }; -export const deleteRedisUserCache = userId => - redisDelAsync(`cache:authz:${userId}`); +export const deleteRedisUserCache = userId => del(`cache:authz:${userId}`); + +export const getProjectGroupsCache = async projectId => + smembers(`project-groups:${projectId}`); +export const saveProjectGroupsCache = async (projectId, groupIds) => + sadd(`project-groups:${projectId}`, groupIds); +export const deleteProjectGroupsCache = async projectId => + del(`project-groups:${projectId}`); export default { getRedisCache, saveRedisCache, - deleteRedisUserCache + deleteRedisUserCache, + getProjectGroupsCache, + saveProjectGroupsCache }; diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index 3d4c7e633a..9f06926163 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -215,21 +215,11 @@ export const Group = (clients) => { R.cond([[R.isEmpty, R.always(null)], [R.T, loadGroupByName]]), )(groupInput); - const loadGroupsByAttribute = async ( + const filterGroupsByAttribute = ( + groups: Group[] | BillingGroup[], filterFn: AttributeFilterFn, - ): Promise => { - const keycloakGroups = await keycloakAdminClient.groups.find(); - - let fullGroups: Group[] | BillingGroup[] = []; - for (const group of keycloakGroups) { - const fullGroup = await keycloakAdminClient.groups.findOne({ - id: group.id, - }); - - fullGroups = [...fullGroups, fullGroup]; - } - - const filteredGroups = R.filter((group: Group) => + ): Group[] | BillingGroup[] => + R.filter((group: Group) => R.pipe( R.toPairs, R.reduce((isMatch: boolean, attribute: [string, string[]]): boolean => { @@ -245,7 +235,23 @@ export const Group = (clients) => { return isMatch; }, false), )(group.attributes), - )(fullGroups); + )(groups); + + const loadGroupsByAttribute = async ( + filterFn: AttributeFilterFn, + ): Promise => { + const keycloakGroups = await keycloakAdminClient.groups.find(); + + let fullGroups: Group[] | BillingGroup[] = []; + for (const group of keycloakGroups) { + const fullGroup = await keycloakAdminClient.groups.findOne({ + id: group.id, + }); + + fullGroups = [...fullGroups, fullGroup]; + } + + const filteredGroups = filterGroupsByAttribute(fullGroups, filterFn); const groups = await transformKeycloakGroups(filteredGroups); @@ -266,7 +272,43 @@ export const Group = (clients) => { return false; }; - return loadGroupsByAttribute(filterFn); + let groupIds = []; + + // This function is called often and is expensive to compute so prefer + // performance over DRY + try { + groupIds = await redisClient.getProjectGroupsCache(projectId); + } catch (err) { + logger.warn(`Error loading project groups from cache: ${err.message}`); + groupIds = []; + } + + if (R.isEmpty(groupIds)) { + const keycloakGroups = await keycloakAdminClient.groups.find(); + // @ts-ignore + groupIds = R.pluck('id', keycloakGroups); + } + + let fullGroups = []; + for (const id of groupIds) { + const fullGroup = await keycloakAdminClient.groups.findOne({ + id, + }); + + fullGroups = [...fullGroups, fullGroup]; + } + + const filteredGroups = filterGroupsByAttribute(fullGroups, filterFn); + try { + const filteredGroupIds = R.pluck('id', filteredGroups); + await redisClient.saveProjectGroupsCache(projectId, filteredGroupIds); + } catch (err) { + logger.warn(`Error saving project groups to cache: ${err.message}`); + } + + const groups = await transformKeycloakGroups(filteredGroups); + + return groups; }; // Recursive function to load projects "up" the group chain @@ -536,7 +578,7 @@ export const Group = (clients) => { try { await redisClient.deleteRedisUserCache(user.id) } catch(err) { - logger.error(`Error deleting user cache ${user.id}: ${err}`); + logger.warn(`Error deleting user cache ${user.id}: ${err}`); } } @@ -573,6 +615,12 @@ export const Group = (clients) => { throw new Error( `Error setting projects for group ${group.name}: ${err.message}`, ); + }; + + try { + await redisClient.deleteProjectGroupsCache(projectId); + } catch (err) { + logger.warn(`Error deleting project groups cache: ${err.message}`); } }; @@ -605,6 +653,12 @@ export const Group = (clients) => { throw new Error( `Error setting projects for group ${group.name}: ${err.message}`, ); + }; + + try { + await redisClient.deleteProjectGroupsCache(projectId); + } catch (err) { + logger.warn(`Error deleting project groups cache: ${err.message}`); } }; From d3dc5709c7a3f714ca12623fa6b6e90c92c3e492 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 6 Jul 2020 09:32:05 -0500 Subject: [PATCH 221/280] Disable apollo server tracing It was enabled to add data to new relic tracing but it doesn't provide anything extra and the output gets sent for every request for every user. --- services/api/src/apolloServer.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index 480ecf1845..8e1584bf8a 100644 --- a/services/api/src/apolloServer.js +++ b/services/api/src/apolloServer.js @@ -32,7 +32,6 @@ const apolloServer = new ApolloServer({ schema, debug: process.env.NODE_ENV === 'development', introspection: true, - tracing: true, subscriptions: { onConnect: async (connectionParams, webSocket) => { const token = R.prop('authToken', connectionParams); @@ -211,17 +210,6 @@ const apolloServer = new ApolloServer({ return { willSendResponse: data => { const { response } = data; - const traceDuration = R.pathSatisfies( - R.is(Number), - ['extensions', 'tracing', 'duration'], - response - ) - ? `Total Duration (ms): ${R.path( - ['extensions', 'tracing', 'duration'], - response - ) / 1000000}` - : 'No trace data'; - newrelic.addCustomAttribute('totalDuration', traceDuration); newrelic.addCustomAttribute( 'errorCount', R.pipe( From c8a1311ec309c2e523f56f64cc2ce3809aeae93f Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Mon, 6 Jul 2020 11:06:40 -0400 Subject: [PATCH 222/280] run 6 task monitors at one per pod --- node-packages/commons/src/tasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index e02d9e3668..96fad87e80 100644 --- a/node-packages/commons/src/tasks.ts +++ b/node-packages/commons/src/tasks.ts @@ -834,7 +834,7 @@ export const consumeTaskMonitor = async function( 'lagoon-tasks-monitor', taskMonitorQueueName ), - channel.prefetch(1), + channel.prefetch(6), channel.consume( `lagoon-tasks-monitor:${taskMonitorQueueName}`, onMessage, From 13bb23af339987c9773bf5b4bda477658ebce76b Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Mon, 6 Jul 2020 12:11:00 -0400 Subject: [PATCH 223/280] add ToDo --- services/kubernetesdeployqueue/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/kubernetesdeployqueue/src/index.ts b/services/kubernetesdeployqueue/src/index.ts index e6a2c74f0f..fa29516b56 100644 --- a/services/kubernetesdeployqueue/src/index.ts +++ b/services/kubernetesdeployqueue/src/index.ts @@ -79,6 +79,12 @@ const messageConsumer = async msg => { } }); + //@TODO + // 1. Load all current deployments from lagoon api that have "status: NEW" + // 2. Check if current buildName is the oldest of all found deployments + // IF yes: continue with checking if k8s has an active build + // IF no: republish (via throwing an exception) into the queue with a delay of 30secs + // Check that there are no active builds in this namespace running try { const jobsGetAll = promisify( From a0a9e6ba458da1af9ba9ceaf8b91b7f3175b49cb Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Mon, 6 Jul 2020 12:13:40 -0400 Subject: [PATCH 224/280] make prefetch configurable --- node-packages/commons/src/tasks.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index 96fad87e80..4716c77228 100644 --- a/node-packages/commons/src/tasks.ts +++ b/node-packages/commons/src/tasks.ts @@ -71,6 +71,9 @@ const rabbitmqHost = process.env.RABBITMQ_HOST || 'broker'; const rabbitmqUsername = process.env.RABBITMQ_USERNAME || 'guest'; const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || 'guest'; +const taskPrefetch = process.env.TASK_PREFETCH_COUNT || 2; +const taskMonitorPrefetch = process.env.TASKMONITOR_PREFETCH_COUNT || 1; + class UnknownActiveSystem extends Error { constructor(message) { super(message); @@ -761,7 +764,7 @@ export const consumeTasks = async function( 'lagoon-tasks', taskQueueName ), - channel.prefetch(2), + channel.prefetch(taskPrefetch), channel.consume(`lagoon-tasks:${taskQueueName}`, onMessage, { noAck: false }) @@ -834,7 +837,7 @@ export const consumeTaskMonitor = async function( 'lagoon-tasks-monitor', taskMonitorQueueName ), - channel.prefetch(6), + channel.prefetch(taskMonitorPrefetch), channel.consume( `lagoon-tasks-monitor:${taskMonitorQueueName}`, onMessage, From 03d15044ed647d5c4b1bbaad8002a2302a758546 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Tue, 7 Jul 2020 05:37:30 +1200 Subject: [PATCH 225/280] Removes debug logging from harborScanningCompleted --- .../src/handlers/problems/harborScanningCompleted.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts index c2da3ed533..5b1de5d0e7 100644 --- a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts +++ b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts @@ -59,13 +59,7 @@ const DEFAULT_REPO_DETAILS_MATCHER = { } let vulnerabilities = []; - try { - vulnerabilities = await getVulnerabilitiesFromHarbor(harborScanId); - } catch(error) { - console.log(error); - throw error; - } - + vulnerabilities = await getVulnerabilitiesFromHarbor(harborScanId); let { id: lagoonProjectId } = await getProjectByName(lagoonProjectName); From edcfbdf899fbc8d566d5f9cc9b32c5f05e971da8 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Tue, 7 Jul 2020 09:44:14 +1200 Subject: [PATCH 226/280] Adds Problems DB OpenShiftProjectName lookup functionality to use project pattern --- .../problems/harborScanningCompleted.ts | 17 +++++++++++++---- .../handlers/problems/processDrutinyResults.ts | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts index 5b1de5d0e7..cae6692777 100644 --- a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts +++ b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts @@ -9,11 +9,9 @@ import uuid4 from 'uuid4'; import { getProjectByName, - getEnvironmentByName, getProblemHarborScanMatches, getEnvironmentByOpenshiftProjectName, - sanitizeProjectName, - sanitizeGroupName, + getOpenShiftInfoForProject, } from '@lagoon/commons/dist/api'; const HARBOR_WEBHOOK_SUCCESSFUL_SCAN = "Success"; @@ -63,8 +61,19 @@ const DEFAULT_REPO_DETAILS_MATCHER = { let { id: lagoonProjectId } = await getProjectByName(lagoonProjectName); + const result = await getOpenShiftInfoForProject(lagoonProjectName); + const projectOpenShift = result.project; - let openshiftProjectName = sanitizeProjectName(`${lagoonProjectName}-${lagoonEnvironmentName}`); + const ocsafety = string => + string.toLocaleLowerCase().replace(/[^0-9a-z-]/g, '-'); + + let openshiftProjectName = projectOpenShift.openshiftProjectPattern + ? projectOpenShift.openshiftProjectPattern + .replace('${branch}', ocsafety(lagoonEnvironmentName)) + .replace('${project}', ocsafety(lagoonProjectName)) + : ocsafety(`${lagoonProjectName}-${lagoonEnvironmentName}`); + + // let openshiftProjectName = sanitizeProjectName(`${lagoonProjectName}-${lagoonEnvironmentName}`); const environmentResult = await getEnvironmentByOpenshiftProjectName(openshiftProjectName); const environmentDetails: any = R.prop('environmentByOpenshiftProjectName', environmentResult) diff --git a/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts b/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts index b99521011e..f0899a0c66 100644 --- a/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts +++ b/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts @@ -11,9 +11,8 @@ const DRUTINY_SERVICE_NAME = 'cli'; const DRUTINY_PACKAGE_NAME = '' import { getProjectByName, - getEnvironmentByName, getEnvironmentByOpenshiftProjectName, - sanitizeProjectName, + getOpenShiftInfoForProject, } from '@lagoon/commons/dist/api'; import { generateProblemsWebhookEventName } from "./webhookHelpers"; import * as R from 'ramda'; @@ -61,7 +60,18 @@ export async function processDrutinyResultset( lagoonProjectName ); - let openshiftProjectName = sanitizeProjectName(`${lagoonProjectName}-${lagoonEnvironmentName}`); + const result = await getOpenShiftInfoForProject(lagoonProjectName); + const projectOpenShift = result.project; + + const ocsafety = string => + string.toLocaleLowerCase().replace(/[^0-9a-z-]/g, '-'); + + let openshiftProjectName = projectOpenShift.openshiftProjectPattern + ? projectOpenShift.openshiftProjectPattern + .replace('${branch}', ocsafety(lagoonEnvironmentName)) + .replace('${project}', ocsafety(lagoonProjectName)) + : ocsafety(`${lagoonProjectName}-${lagoonEnvironmentName}`); + const environmentResult = await getEnvironmentByOpenshiftProjectName(openshiftProjectName); const environmentDetails: any = R.prop('environmentByOpenshiftProjectName', environmentResult) From af9c746281ec186184e083896e56374f79fb6e6e Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Tue, 7 Jul 2020 09:46:08 +1200 Subject: [PATCH 227/280] Remove extraneous comment from harborScanningCompleted.ts --- .../src/handlers/problems/harborScanningCompleted.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts index cae6692777..ef068f6913 100644 --- a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts +++ b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts @@ -73,7 +73,6 @@ const DEFAULT_REPO_DETAILS_MATCHER = { .replace('${project}', ocsafety(lagoonProjectName)) : ocsafety(`${lagoonProjectName}-${lagoonEnvironmentName}`); - // let openshiftProjectName = sanitizeProjectName(`${lagoonProjectName}-${lagoonEnvironmentName}`); const environmentResult = await getEnvironmentByOpenshiftProjectName(openshiftProjectName); const environmentDetails: any = R.prop('environmentByOpenshiftProjectName', environmentResult) From de9fd702d2f374b00cd7a6b8b6a1acb6adcf1bb0 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Mon, 6 Jul 2020 18:32:32 -0400 Subject: [PATCH 228/280] use proper Numbers --- node-packages/commons/src/tasks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index 4716c77228..a06adbd83a 100644 --- a/node-packages/commons/src/tasks.ts +++ b/node-packages/commons/src/tasks.ts @@ -71,8 +71,8 @@ const rabbitmqHost = process.env.RABBITMQ_HOST || 'broker'; const rabbitmqUsername = process.env.RABBITMQ_USERNAME || 'guest'; const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || 'guest'; -const taskPrefetch = process.env.TASK_PREFETCH_COUNT || 2; -const taskMonitorPrefetch = process.env.TASKMONITOR_PREFETCH_COUNT || 1; +const taskPrefetch = process.env.TASK_PREFETCH_COUNT ? Number(process.env.TASK_PREFETCH_COUNT) : 2; +const taskMonitorPrefetch = process.env.TASKMONITOR_PREFETCH_COUNT ? Number(process.env.TASKMONITOR_PREFETCH_COUNT) : 1; class UnknownActiveSystem extends Error { constructor(message) { From 58787cc7aee02fd9564233975dad00d17653220f Mon Sep 17 00:00:00 2001 From: Sean Hamlin Date: Tue, 7 Jul 2020 12:36:11 +1200 Subject: [PATCH 229/280] PATCH existing routes with disable_cookies upon deployment. --- .../oc-build-deploy-dind/scripts/exec-openshift-create-route.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/oc-build-deploy-dind/scripts/exec-openshift-create-route.sh b/images/oc-build-deploy-dind/scripts/exec-openshift-create-route.sh index a243302e2f..0d68a1cc44 100644 --- a/images/oc-build-deploy-dind/scripts/exec-openshift-create-route.sh +++ b/images/oc-build-deploy-dind/scripts/exec-openshift-create-route.sh @@ -2,7 +2,7 @@ # TODO: find out why we are using the if/else and if it's still needed for kubernetes if oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} get route "$ROUTE_DOMAIN" &> /dev/null; then - oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} patch route "$ROUTE_DOMAIN" -p "{\"metadata\":{\"labels\":{\"dioscuri.amazee.io/migrate\":\"${ROUTE_MIGRATE}\"},\"annotations\":{\"kubernetes.io/tls-acme\":\"${ROUTE_TLS_ACME}\",\"haproxy.router.openshift.io/hsts_header\":\"${ROUTE_HSTS}\",\"monitor.stakater.com/enabled\":\"${MONITORING_ENABLED}\",\"uptimerobot.monitor.stakater.com/interval\":\"${MONITORING_INTERVAL}\",\"uptimerobot.monitor.stakater.com/alert-contacts\":\"${MONITORING_ALERTCONTACT}\",\"monitor.stakater.com/overridePath\":\"${MONITORING_PATH}\",\"uptimerobot.monitor.stakater.com/status-pages\":\"${MONITORING_STATUSPAGEID}\"}},\"spec\":{\"to\":{\"name\":\"${ROUTE_SERVICE}\"},\"tls\":{\"insecureEdgeTerminationPolicy\":\"${ROUTE_INSECURE}\"}}}" + oc --insecure-skip-tls-verify -n ${OPENSHIFT_PROJECT} patch route "$ROUTE_DOMAIN" -p "{\"metadata\":{\"labels\":{\"dioscuri.amazee.io/migrate\":\"${ROUTE_MIGRATE}\"},\"annotations\":{\"haproxy.router.openshift.io/disable_cookies\":\"true\",\"kubernetes.io/tls-acme\":\"${ROUTE_TLS_ACME}\",\"haproxy.router.openshift.io/hsts_header\":\"${ROUTE_HSTS}\",\"monitor.stakater.com/enabled\":\"${MONITORING_ENABLED}\",\"uptimerobot.monitor.stakater.com/interval\":\"${MONITORING_INTERVAL}\",\"uptimerobot.monitor.stakater.com/alert-contacts\":\"${MONITORING_ALERTCONTACT}\",\"monitor.stakater.com/overridePath\":\"${MONITORING_PATH}\",\"uptimerobot.monitor.stakater.com/status-pages\":\"${MONITORING_STATUSPAGEID}\"}},\"spec\":{\"to\":{\"name\":\"${ROUTE_SERVICE}\"},\"tls\":{\"insecureEdgeTerminationPolicy\":\"${ROUTE_INSECURE}\"}}}" else oc process --local -o yaml --insecure-skip-tls-verify \ -n ${OPENSHIFT_PROJECT} \ From 268f52e6726eaf44d76f20d182a235c4e931c873 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 6 Jul 2020 22:10:10 -0500 Subject: [PATCH 230/280] Clear redis caches on relevant mutations --- services/api/src/models/group.ts | 43 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index 9f06926163..f474269780 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -89,6 +89,15 @@ const attrLagoonProjectsLens = R.compose( R.lensPath([0]), ); +const getProjectIdsFromGroup = R.pipe( + // @ts-ignore + R.view(attrLagoonProjectsLens), + R.defaultTo(''), + R.split(','), + R.reject(R.isEmpty), + R.map(id => parseInt(id, 10)), +); + export const isRoleSubgroup = R.pathEq( ['attributes', 'type', 0], 'role-subgroup', @@ -315,13 +324,7 @@ export const Group = (clients) => { const getProjectsFromGroupAndParents = async ( group: Group, ): Promise => { - const projectIds = R.pipe( - R.view(attrLagoonProjectsLens), - R.defaultTo(''), - R.split(','), - R.reject(R.isEmpty), - R.map(id => parseInt(id, 10)), - )(group); + const projectIds = getProjectIdsFromGroup(group); const parentGroup = await loadParentGroup(group); const parentProjectIds = parentGroup @@ -339,13 +342,7 @@ export const Group = (clients) => { const getProjectsFromGroupAndSubgroups = async ( group: Group, ): Promise => { - const groupProjectIds = R.pipe( - R.view(attrLagoonProjectsLens), - R.defaultTo(''), - R.split(','), - R.reject(R.isEmpty), - R.map(id => parseInt(id, 10)), - )(group); + const groupProjectIds = getProjectIdsFromGroup(group); let subGroupProjectIds = []; for (const subGroup of group.groups) { @@ -502,6 +499,10 @@ export const Group = (clients) => { }; const deleteGroup = async (id: string): Promise => { + const group = loadGroupById(id); + // @ts-ignore + const projectIds = getProjectIdsFromGroup(group); + try { await keycloakAdminClient.groups.del({ id }); } catch (err) { @@ -511,6 +512,14 @@ export const Group = (clients) => { throw new Error(`Error deleting group ${id}: ${err}`); } } + + for (const projectId of projectIds) { + try { + await redisClient.deleteProjectGroupsCache(projectId); + } catch (err) { + logger.warn(`Error deleting project groups cache: ${err.message}`); + } + } }; const addUserToGroup = async ( @@ -552,6 +561,12 @@ export const Group = (clients) => { throw new Error(`Could not add user to group: ${err.message}`); } + try { + await redisClient.deleteRedisUserCache(user.id) + } catch(err) { + logger.warn(`Error deleting user cache ${user.id}: ${err}`); + } + return await loadGroupById(group.id); }; From 06284c89ec5845dfe5eec43d857c623bcdf6ed4d Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 7 Jul 2020 03:00:14 -0500 Subject: [PATCH 231/280] Requeue instead of waiting for k8s deployments if they're not next in line --- node-packages/commons/src/api.ts | 2 +- node-packages/commons/src/tasks.ts | 8 +- services/kubernetesbuilddeploy/src/index.ts | 3 +- .../kubernetesbuilddeploymonitor/src/index.ts | 1 - services/kubernetesdeployqueue/src/index.ts | 73 +++++++++++++++---- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/node-packages/commons/src/api.ts b/node-packages/commons/src/api.ts index 293d9df28f..158634ad8f 100644 --- a/node-packages/commons/src/api.ts +++ b/node-packages/commons/src/api.ts @@ -105,7 +105,7 @@ const options = { const transport = new Transport(`${API_HOST}/graphql`, options); -const graphqlapi = new Lokka({ transport }); +export const graphqlapi = new Lokka({ transport }); class ProjectNotFound extends Error { constructor(message) { diff --git a/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index e02d9e3668..8d7ced8b7a 100644 --- a/node-packages/commons/src/tasks.ts +++ b/node-packages/commons/src/tasks.ts @@ -793,7 +793,13 @@ export const consumeTaskMonitor = async function( return; } - const retryDelayMilisecs = 5000; + let retryDelaySecs = 5; + + if (error.delayFn) { + retryDelaySecs = error.delayFn(retryCount); + } + + const retryDelayMilisecs = retryDelaySecs * 1000; // copying options from the original message const retryMsgOptions = { diff --git a/services/kubernetesbuilddeploy/src/index.ts b/services/kubernetesbuilddeploy/src/index.ts index d701d16802..ae98f632bc 100644 --- a/services/kubernetesbuilddeploy/src/index.ts +++ b/services/kubernetesbuilddeploy/src/index.ts @@ -367,12 +367,11 @@ const messageConsumer = async msg => { }) logger.info(`${openshiftProject}: Namespace ${openshiftProject} created`) } catch (err) { - console.log(err.code) // an already existing namespace throws an error, we check if it's a 409, means it does already exist, so we ignore that error. if (err.code == 409) { logger.info(`${openshiftProject}: Namespace ${openshiftProject} already exists`) } else { - logger.error(err) + logger.error(`Could not create namespace '${openshiftProject}': ${err.code} ${err.message}`); throw new Error } } diff --git a/services/kubernetesbuilddeploymonitor/src/index.ts b/services/kubernetesbuilddeploymonitor/src/index.ts index 54d95adfc2..90aa49a334 100644 --- a/services/kubernetesbuilddeploymonitor/src/index.ts +++ b/services/kubernetesbuilddeploymonitor/src/index.ts @@ -182,7 +182,6 @@ ${podLog}`; await updateDeployment(deployment.deploymentByRemoteId.id, { status: status.toUpperCase(), - created: convertDateFormat(jobInfo.metadata.creationTimestamp), started: dateOrNull(jobInfo.status.startTime), completed: dateOrNull(jobInfo.status.completionTime), }); diff --git a/services/kubernetesdeployqueue/src/index.ts b/services/kubernetesdeployqueue/src/index.ts index fa29516b56..40b02a967d 100644 --- a/services/kubernetesdeployqueue/src/index.ts +++ b/services/kubernetesdeployqueue/src/index.ts @@ -3,6 +3,7 @@ import KubernetesClient from 'kubernetes-client'; import R from 'ramda'; import { logger } from '@lagoon/commons/dist/local-logging'; import { + graphqlapi, getOpenShiftInfoForProject, updateDeployment } from '@lagoon/commons/dist/api'; @@ -27,6 +28,49 @@ class AnotherBuildAlreadyRunning extends Error { } } +class BuildOutOfOrder extends Error { + delayFn: (retryCount: number) => number; + + constructor(message) { + super(message); + this.name = 'BuildOutOfOrder'; + // Wait 30 seconds before checking again. + this.delayFn = () => 30; + } +} + +const getEnvironmentDeployments = async ( + openshiftProjectName: string +): Promise => { + const result = await graphqlapi.query( + ` + query getEnvironmentDeployments($openshiftProjectName: String!) { + environmentByOpenshiftProjectName(openshiftProjectName: $openshiftProjectName) { + deployments { + name + status + created + } + } + }`, + { openshiftProjectName } + ); + + return R.pathOr( + [], + ['environmentByOpenshiftProjectName', 'deployments'], + result + ); +}; + +const filterByNewStatus = R.filter(R.propEq('status', 'new')); +const sortByCreatedDate = R.sort(R.ascend(R.prop('created'))); +const oldestNewDeployment = R.pipe( + filterByNewStatus, + sortByCreatedDate, + R.head +); + const messageConsumer = async msg => { const { buildName, @@ -79,13 +123,17 @@ const messageConsumer = async msg => { } }); - //@TODO - // 1. Load all current deployments from lagoon api that have "status: NEW" - // 2. Check if current buildName is the oldest of all found deployments - // IF yes: continue with checking if k8s has an active build - // IF no: republish (via throwing an exception) into the queue with a delay of 30secs + const deployments = await getEnvironmentDeployments(openshiftProject); + const nextDeploymentToRun = oldestNewDeployment(deployments); + + if (R.prop('name', nextDeploymentToRun) !== buildName) { + const msg = `${openshiftProject}: Reqeueing ${buildName} since it's out of order`; + logger.debug(msg); + throw new BuildOutOfOrder(msg); + } // Check that there are no active builds in this namespace running + let activeBuilds; try { const jobsGetAll = promisify( kubernetesBatchApi.namespaces(openshiftProject).jobs.get @@ -95,16 +143,10 @@ const messageConsumer = async msg => { labelSelector: 'lagoon.sh/jobType=build' } }); - const activeBuilds: any = R.pipe( + activeBuilds = R.pipe( R.propOr([], 'items'), R.filter(R.pathSatisfies(R.lt(0), ['status', 'active'])) )(namespaceJobs); - - if (!R.isEmpty(activeBuilds)) { - throw new AnotherBuildAlreadyRunning( - `${openshiftProject}: Reqeueing ${buildName} due to ${activeBuilds.length} pending builds` - ); - } } catch (err) { logger.error( `${openshiftProject}: Unexpected error loading current running Jobs, unable to build ${buildName}: ${err}` @@ -112,6 +154,12 @@ const messageConsumer = async msg => { return; } + if (!R.isEmpty(activeBuilds)) { + const msg = `${openshiftProject}: Reqeueing ${buildName} due to ${activeBuilds.length} pending builds`; + logger.debug(msg); + throw new AnotherBuildAlreadyRunning(msg); + } + // Load job, if not exists create let jobInfo; try { @@ -126,7 +174,6 @@ const messageConsumer = async msg => { const jobPost = promisify( kubernetesApi.group(jobConfig).ns(openshiftProject).jobs.post ); - console.log(JSON.stringify(jobConfig, null, 4)); jobInfo = await jobPost({ body: jobConfig }); logger.info(`${openshiftProject}: Created build ${buildName}`); From 1bbb03bffa43a9ce07e833f5da2f0f770cb1cc64 Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Tue, 7 Jul 2020 12:28:55 +0200 Subject: [PATCH 232/280] Fixed the re-creation of .my.cnf symbolic link --- images/mariadb/entrypoints/9999-mariadb-init.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/mariadb/entrypoints/9999-mariadb-init.bash b/images/mariadb/entrypoints/9999-mariadb-init.bash index fecbd795a0..72dc40ad22 100755 --- a/images/mariadb/entrypoints/9999-mariadb-init.bash +++ b/images/mariadb/entrypoints/9999-mariadb-init.bash @@ -39,7 +39,7 @@ if [ -n "$MARIADB_COPY_DATA_DIR_SOURCE" ]; then fi fi -ln -s ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf /home/.my.cnf +ln -sf ${MARIADB_DATA_DIR:-/var/lib/mysql}/.my.cnf /home/.my.cnf if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then if [ ! -d "/run/mysqld" ]; then From f30b506baef8af51cc0af3fc3086cacfad30a2bf Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 7 Jul 2020 08:27:57 -0500 Subject: [PATCH 233/280] Avoid denying authorization if redis fails --- services/api/src/util/auth.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 49603e4230..6cacb3347e 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -120,13 +120,18 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) // Check a redis cache before doing a full keycloak lookup. const resourceScope = {resource, scope, currentUserId, ...attributes }; - const redisCache = await getRedisCache(resourceScope); - if (redisCache == 1) { - return true; - } else if (redisCache == 0) { - throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); + try { + const redisCache = await getRedisCache(resourceScope); + if (redisCache == 1) { + return true; + } else if (redisCache == 0) { + throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); + } + } catch (err) { + logger.warn(`Could not lookup authz cache: ${err.message}`); } + const currentUser = await UserModel.loadUserById(currentUserId); const serviceAccount = await keycloakGrantManager.obtainFromClientCredentials(); @@ -260,7 +265,12 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) if (newGrant.access_token.hasPermission(resource, scope)) { requestCache.set(cacheKey, true); - saveRedisCache(resourceScope, 1); + try { + saveRedisCache(resourceScope, 1); + } catch (err) { + logger.warn(`Could not save authz cache: ${err.message}`); + } + return; } } catch (err) { @@ -270,7 +280,11 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) } requestCache.set(cacheKey, false); - saveRedisCache(resourceScope, 0); + try { + saveRedisCache(resourceScope, 0); + } catch (err) { + logger.warn(`Could not save authz cache: ${err.message}`); + } throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); }; }; From 6f2c46ad34bddadf14128f23b133dcb9a01c4aeb Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 7 Jul 2020 11:01:12 -0500 Subject: [PATCH 234/280] Use a custom build for api-redis to get deploys working --- docker-compose.yaml | 6 +- services/api-redis/.lagoon.app.yml | 125 +++++++++++++++++++++++++++++ services/api-redis/Dockerfile | 2 + 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 services/api-redis/.lagoon.app.yml create mode 100644 services/api-redis/Dockerfile diff --git a/docker-compose.yaml b/docker-compose.yaml index e6f6178f58..5b568c5df7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -760,6 +760,8 @@ services: lagoon.name: harborregistry lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-1 api-redis: - image: ${IMAGE_REPO:-lagoon}/redis + image: ${IMAGE_REPO:-lagoon}/api-redis labels: - lagoon.type: redis + lagoon.type: custom + lagoon.template: services/api-redis/.lagoon.app.yml + lagoon.image: amazeeiolagoon/api-redis:v1-7-1 diff --git a/services/api-redis/.lagoon.app.yml b/services/api-redis/.lagoon.app.yml new file mode 100644 index 0000000000..48b7166b89 --- /dev/null +++ b/services/api-redis/.lagoon.app.yml @@ -0,0 +1,125 @@ +apiVersion: v1 +kind: Template +metadata: + creationTimestamp: null + name: lagoon-openshift-template-redis +parameters: + - name: SERVICE_NAME + description: Name of this service + required: true + - name: SAFE_BRANCH + description: Which branch this belongs to, special chars replaced with dashes + required: true + - name: SAFE_PROJECT + description: Which project this belongs to, special chars replaced with dashes + required: true + - name: BRANCH + description: Which branch this belongs to, original value + required: true + - name: PROJECT + description: Which project this belongs to, original value + required: true + - name: LAGOON_GIT_SHA + description: git hash sha of the current deployment + required: true + - name: SERVICE_ROUTER_URL + description: URL of the Router for this service + value: "" + - name: OPENSHIFT_PROJECT + description: Name of the Project that this service is in + required: true + - name: REGISTRY + description: Registry where Images are pushed to + required: true + - name: DEPLOYMENT_STRATEGY + description: Strategy of Deploymentconfig + value: "Rolling" + - name: SERVICE_IMAGE + description: Pullable image of service + required: true + - name: CRONJOBS + description: Oneliner of Cronjobs + value: "" + - name: ENVIRONMENT_TYPE + description: production level of this environment + value: 'production' + - name: CONFIG_MAP_SHA + description: SHA sum of the configmap + value: '' +objects: +- apiVersion: v1 + kind: DeploymentConfig + metadata: + creationTimestamp: null + labels: + service: ${SERVICE_NAME} + branch: ${SAFE_BRANCH} + project: ${SAFE_PROJECT} + name: ${SERVICE_NAME} + spec: + replicas: 1 + selector: + service: ${SERVICE_NAME} + strategy: + type: ${DEPLOYMENT_STRATEGY} + template: + metadata: + creationTimestamp: null + labels: + service: ${SERVICE_NAME} + branch: ${SAFE_BRANCH} + project: ${SAFE_PROJECT} + annotations: + lagoon.sh/configMapSha: ${CONFIG_MAP_SHA} + spec: + priorityClassName: lagoon-priority-${ENVIRONMENT_TYPE} + containers: + - image: ${SERVICE_IMAGE} + name: ${SERVICE_NAME} + ports: + - containerPort: 6379 + protocol: TCP + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 15 + timeoutSeconds: 1 + livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 120 + periodSeconds: 10 + envFrom: + - configMapRef: + name: lagoon-env + env: + - name: SERVICE_NAME + value: ${SERVICE_NAME} + - name: CRONJOBS + value: ${CRONJOBS} + resources: + requests: + cpu: 10m + memory: 10Mi + test: false + triggers: + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + creationTimestamp: null + labels: + service: ${SERVICE_NAME} + branch: ${SAFE_BRANCH} + project: ${SAFE_PROJECT} + name: ${SERVICE_NAME} + spec: + ports: + - name: 6379-tcp + port: 6379 + protocol: TCP + targetPort: 6379 + selector: + service: ${SERVICE_NAME} + status: + loadBalancer: {} diff --git a/services/api-redis/Dockerfile b/services/api-redis/Dockerfile new file mode 100644 index 0000000000..5f8d986391 --- /dev/null +++ b/services/api-redis/Dockerfile @@ -0,0 +1,2 @@ +ARG IMAGE_REPO +FROM ${IMAGE_REPO:-lagoon}/redis From 0fec059e39a7aabe74d662cbed6c5eea8959aff6 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 7 Jul 2020 11:08:15 -0500 Subject: [PATCH 235/280] Add api-redis as make build target --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2d206067d3..ef583b9e2a 100644 --- a/Makefile +++ b/Makefile @@ -449,7 +449,8 @@ services := api \ harbor-redis \ harborregistry \ harborregistryctl \ - harbor-trivy + harbor-trivy \ + api-redis service-images += $(services) From 085c49140c0db9fe9b4aa2bb9f03476ae1b3fbce Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 7 Jul 2020 11:14:24 -0500 Subject: [PATCH 236/280] Ensure redis is built before api-redis --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index ef583b9e2a..8d80ea40cf 100644 --- a/Makefile +++ b/Makefile @@ -483,6 +483,7 @@ build/harbor-nginx: build/harborregistryctl services/harbor-core/Dockerfile serv build/tests-kubernetes: build/tests build/tests-openshift: build/tests build/toolbox: build/mariadb +build/api-redis: build/redis # Auth SSH needs the context of the root folder, so we have it individually build/ssh: build/commons From 500041bb543cb3d3fba13aaba2e3be194245cbe7 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 8 Jul 2020 09:30:25 +0800 Subject: [PATCH 237/280] Label using the target env-type instead of the build env-type --- services/kubernetesbuilddeploy/src/index.ts | 2 +- services/openshiftbuilddeploy/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/kubernetesbuilddeploy/src/index.ts b/services/kubernetesbuilddeploy/src/index.ts index d701d16802..fe87af56b9 100644 --- a/services/kubernetesbuilddeploy/src/index.ts +++ b/services/kubernetesbuilddeploy/src/index.ts @@ -360,7 +360,7 @@ const messageConsumer = async msg => { "labels": { "lagoon.sh/project": projectName, "lagoon.sh/environment": environmentName, - "lagoon.sh/environmentType": lagoonEnvironmentType + "lagoon.sh/environmentType": environmentType } } } diff --git a/services/openshiftbuilddeploy/src/index.ts b/services/openshiftbuilddeploy/src/index.ts index b0f3b50db3..f7bfd715d9 100644 --- a/services/openshiftbuilddeploy/src/index.ts +++ b/services/openshiftbuilddeploy/src/index.ts @@ -358,7 +358,7 @@ const messageConsumer = async msg => { "labels": { "lagoon.sh/project": safeProjectName, "lagoon.sh/environment": safeBranchName, - "lagoon.sh/environmentType": lagoonEnvironmentType + "lagoon.sh/environmentType": environmentType } }, "displayName":`[${projectName}] ${branchName}` From 0742685b5e8c4c0ca1a49f5b891e771ddee8183f Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 8 Jul 2020 20:30:46 +1000 Subject: [PATCH 238/280] only run idling if production lagoon --- services/auto-idler/idle-clis.sh | 4 ++++ services/auto-idler/idle-services.sh | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/services/auto-idler/idle-clis.sh b/services/auto-idler/idle-clis.sh index 52f98f14e9..531285ac22 100755 --- a/services/auto-idler/idle-clis.sh +++ b/services/auto-idler/idle-clis.sh @@ -2,6 +2,8 @@ # set -e -o pipefail +if [ "${LAGOON_ENVIRONMENT_TYPE}" == "production" ]; then + prefixwith() { local prefix="$1" shift @@ -46,3 +48,5 @@ done sleep 5 # clean up the tmp file rm $TMP_DATA + +fi \ No newline at end of file diff --git a/services/auto-idler/idle-services.sh b/services/auto-idler/idle-services.sh index 8eb04d1b20..ec48759837 100755 --- a/services/auto-idler/idle-services.sh +++ b/services/auto-idler/idle-services.sh @@ -3,6 +3,8 @@ # make sure we stop if we fail set -eo pipefail +if [ "${LAGOON_ENVIRONMENT_TYPE}" == "production" ]; then + prefixwith() { local prefix="$1" shift @@ -52,3 +54,5 @@ done sleep 5 # clean up the tmp file rm $TMP_DATA + +fi \ No newline at end of file From 32889b337fd5796fe49ad8ce1bce314714392df6 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 8 Jul 2020 08:24:03 -0500 Subject: [PATCH 239/280] Also clear user caches with group<>project relationships change --- services/api/src/models/group.ts | 39 ++++++++++++++++++++++++++++++++ services/api/src/util/auth.ts | 22 ++++++++++-------- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index f474269780..19fbc5d6aa 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -320,6 +320,23 @@ export const Group = (clients) => { return groups; }; + // Recursive function to load membership "up" the group chain + const getMembersFromGroupAndParents = async ( + group: Group, + ): Promise => { + const members = R.prop('members', group); + + const parentGroup = await loadParentGroup(group); + const parentMembers = parentGroup + ? await getMembersFromGroupAndParents(parentGroup) + : []; + + return [ + ...members, + ...parentMembers, + ]; + }; + // Recursive function to load projects "up" the group chain const getProjectsFromGroupAndParents = async ( group: Group, @@ -632,6 +649,17 @@ export const Group = (clients) => { ); }; + // Clear the cache for users that gained access to the project + const groupAndParentsMembers = await getMembersFromGroupAndParents(group); + const userIds = R.map(R.path(['user', 'id']), groupAndParentsMembers); + for (const userId of userIds) { + try { + await redisClient.deleteRedisUserCache(userId) + } catch(err) { + logger.warn(`Error deleting user cache ${userId}: ${err}`); + } + } + try { await redisClient.deleteProjectGroupsCache(projectId); } catch (err) { @@ -670,6 +698,17 @@ export const Group = (clients) => { ); }; + // Clear the cache for users that lost access to the project + const groupAndParentsMembers = await getMembersFromGroupAndParents(group); + const userIds = R.map(R.path(['user', 'id']), groupAndParentsMembers); + for (const userId of userIds) { + try { + await redisClient.deleteRedisUserCache(userId) + } catch(err) { + logger.warn(`Error deleting user cache ${userId}: ${err}`); + } + } + try { await redisClient.deleteProjectGroupsCache(projectId); } catch (err) { diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 6cacb3347e..ce6482abaf 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -118,19 +118,23 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); } - // Check a redis cache before doing a full keycloak lookup. + // Check the redis cache before doing a full keycloak lookup. const resourceScope = {resource, scope, currentUserId, ...attributes }; + let redisCacheResult: number; try { - const redisCache = await getRedisCache(resourceScope); - if (redisCache == 1) { - return true; - } else if (redisCache == 0) { - throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); - } + const data = await getRedisCache(resourceScope); + redisCacheResult = parseInt(data, 10); } catch (err) { logger.warn(`Could not lookup authz cache: ${err.message}`); } + if (redisCacheResult === 1) { + return true; + } else if (redisCacheResult === 0) { + logger.debug(`Redis authz cache returned denied for ${JSON.stringify(resourceScope)}`); + throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); + } + const currentUser = await UserModel.loadUserById(currentUserId); const serviceAccount = await keycloakGrantManager.obtainFromClientCredentials(); @@ -266,7 +270,7 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) if (newGrant.access_token.hasPermission(resource, scope)) { requestCache.set(cacheKey, true); try { - saveRedisCache(resourceScope, 1); + await saveRedisCache(resourceScope, 1); } catch (err) { logger.warn(`Could not save authz cache: ${err.message}`); } @@ -281,7 +285,7 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) requestCache.set(cacheKey, false); try { - saveRedisCache(resourceScope, 0); + await saveRedisCache(resourceScope, 0); } catch (err) { logger.warn(`Could not save authz cache: ${err.message}`); } From 726ba32f2632bebf3c2f5361439381b98ae07e60 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 8 Jul 2020 08:25:56 -0500 Subject: [PATCH 240/280] Disable newrelic logging to disk in api --- services/api/src/newrelic.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/api/src/newrelic.js b/services/api/src/newrelic.js index f88079b178..cfaf8705a5 100644 --- a/services/api/src/newrelic.js +++ b/services/api/src/newrelic.js @@ -16,7 +16,8 @@ exports.config = { * issues with the agent, 'info' and higher will impose the least overhead on * production applications. */ - level: 'info' + level: 'info', + enabled: false, }, /** * When true, all request headers except for those listed in attributes.exclude From c2408e82e792cfdc1e6d487889cde401328e6cc3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 9 Jul 2020 04:18:13 -0500 Subject: [PATCH 241/280] If k8s monitoring services fail for some reason, error out the deployments in lagoon --- .../kubernetesbuilddeploymonitor/src/index.ts | 22 ++++++++++++--- services/kubernetesdeployqueue/src/index.ts | 28 +++++++++++++++---- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/services/kubernetesbuilddeploymonitor/src/index.ts b/services/kubernetesbuilddeploymonitor/src/index.ts index 90aa49a334..2bc0de8e65 100644 --- a/services/kubernetesbuilddeploymonitor/src/index.ts +++ b/services/kubernetesbuilddeploymonitor/src/index.ts @@ -1,6 +1,7 @@ import { promisify } from 'util'; import kubernetesClient from 'kubernetes-client'; import R from 'ramda'; +import moment from 'moment'; import { logger } from '@lagoon/commons/dist/local-logging'; import { @@ -341,12 +342,25 @@ const saveBuildLog = async(jobName, projectName, branchName, buildLog, status, r const deathHandler = async (msg, lastError) => { const { - jobName, + buildName: jobName, projectName, - openshiftProject, branchName, - sha - } = JSON.parse(msg.content.toString()) + sha, + deployment + } = JSON.parse(msg.content.toString()); + + // Don't leave the deployment in an active state + try { + const now = moment.utc(); + await updateDeployment(deployment.id, { + status: 'ERROR', + completed: now.format('YYYY-MM-DDTHH:mm:ss'), + }); + } catch (error) { + logger.error( + `Could not update deployment ${projectName} ${jobName}. Message: ${error}` + ); + } let logMessage = '' if (sha) { diff --git a/services/kubernetesdeployqueue/src/index.ts b/services/kubernetesdeployqueue/src/index.ts index 40b02a967d..24505d3e7b 100644 --- a/services/kubernetesdeployqueue/src/index.ts +++ b/services/kubernetesdeployqueue/src/index.ts @@ -1,6 +1,7 @@ const promisify = require('util').promisify; import KubernetesClient from 'kubernetes-client'; import R from 'ramda'; +import moment from 'moment'; import { logger } from '@lagoon/commons/dist/local-logging'; import { graphqlapi, @@ -127,7 +128,7 @@ const messageConsumer = async msg => { const nextDeploymentToRun = oldestNewDeployment(deployments); if (R.prop('name', nextDeploymentToRun) !== buildName) { - const msg = `${openshiftProject}: Reqeueing ${buildName} since it's out of order`; + const msg = `The build "${buildName}" is not next in line for project "${openshiftProject}"`; logger.debug(msg); throw new BuildOutOfOrder(msg); } @@ -155,7 +156,7 @@ const messageConsumer = async msg => { } if (!R.isEmpty(activeBuilds)) { - const msg = `${openshiftProject}: Reqeueing ${buildName} due to ${activeBuilds.length} pending builds`; + const msg = `${openshiftProject}: ${buildName} is waiting on ${activeBuilds.length} active builds`; logger.debug(msg); throw new AnotherBuildAlreadyRunning(msg); } @@ -210,7 +211,12 @@ const messageConsumer = async msg => { projectName, openshiftProject, branchName, - sha + sha, + deployment: { + ...deployment, + status: 'PENDING', + remoteId: jobInfo.metadata.uid, + } }; const taskMonitorLogs = await createTaskMonitor( @@ -239,12 +245,24 @@ const deathHandler = async (msg, lastError) => { const { buildName, projectName, - openshiftProject, branchName, sha, - jobConfig + deployment } = JSON.parse(msg.content.toString()); + // Don't leave the deployment in an active state + try { + const now = moment.utc(); + await updateDeployment(deployment.id, { + status: 'ERROR', + completed: now.format('YYYY-MM-DDTHH:mm:ss'), + }); + } catch (error) { + logger.error( + `Could not update deployment ${projectName} ${buildName}. Message: ${error}` + ); + } + let logMessage = ''; if (sha) { logMessage = `\`${branchName}\` (${sha.substring(0, 7)})`; From 2596947226dd579482a17cdb3f6ecef3745071c2 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 9 Jul 2020 06:22:31 -0500 Subject: [PATCH 242/280] Fix getEnvironmentByOpenshiftProjectName returns deleted environment --- services/api/src/resources/environment/resolvers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/api/src/resources/environment/resolvers.ts b/services/api/src/resources/environment/resolvers.ts index f00417f8f0..c275c285a6 100644 --- a/services/api/src/resources/environment/resolvers.ts +++ b/services/api/src/resources/environment/resolvers.ts @@ -260,6 +260,7 @@ export const getEnvironmentByOpenshiftProjectName: ResolverFn = async ( environment e JOIN project p ON e.project = p.id WHERE e.openshift_project_name = :openshift_project_name + AND e.deleted = "0000-00-00 00:00:00" `; const prep = prepare(sqlClient, str); From c3b7fe5c23256d2d299f2e354e6bc44a5d9ae444 Mon Sep 17 00:00:00 2001 From: Sean Hamlin Date: Fri, 10 Jul 2020 11:00:31 +1200 Subject: [PATCH 243/280] Add PECL Yaml 2.1.0 to the base PHP images --- images/php/fpm/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/images/php/fpm/Dockerfile b/images/php/fpm/Dockerfile index 3e0d6e0d67..fd44162d7e 100644 --- a/images/php/fpm/Dockerfile +++ b/images/php/fpm/Dockerfile @@ -61,6 +61,8 @@ RUN apk add --no-cache fcgi \ # for webp libwebp-dev \ postgresql-dev \ + # for yaml + yaml-dev \ # for imagemagick imagemagick \ imagemagick-libs \ @@ -68,6 +70,7 @@ RUN apk add --no-cache fcgi \ && apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS \ && yes '' | pecl install -f apcu \ && yes '' | pecl install -f xdebug \ + && yes '' | pecl install -f yaml \ && yes '' | pecl install -f redis-4.3.0 \ && yes '' | pecl install -f imagick \ && docker-php-ext-enable apcu redis xdebug imagick \ @@ -83,6 +86,7 @@ RUN apk add --no-cache fcgi \ && sed -i '1s/^/;Intentionally disabled. Enable via setting env variable XDEBUG_ENABLE to true\n;/' /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ && rm -rf /var/cache/apk/* /tmp/pear/ \ && apk del .phpize-deps \ + && echo "extension=yaml.so" > /usr/local/etc/php/conf.d/yaml.ini \ && mkdir -p /tmp/newrelic && cd /tmp/newrelic \ && wget https://download.newrelic.com/php_agent/archive/${NEWRELIC_VERSION}/newrelic-php5-${NEWRELIC_VERSION}-linux-musl.tar.gz \ && gzip -dc newrelic-php5-${NEWRELIC_VERSION}-linux-musl.tar.gz | tar --strip-components=1 -xf - \ From d4d5ef7e647b823dfb6a34d3d27a893c7a6afcaa Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Fri, 10 Jul 2020 11:20:25 +1200 Subject: [PATCH 244/280] Changes FAST_HEALTH_CHECK flag from 'on' to 'true' --- docs/using_lagoon/docker_images/nginx.md | 2 +- images/nginx/docker-entrypoint | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using_lagoon/docker_images/nginx.md b/docs/using_lagoon/docker_images/nginx.md index 60850e0233..3f107510b9 100644 --- a/docs/using_lagoon/docker_images/nginx.md +++ b/docs/using_lagoon/docker_images/nginx.md @@ -60,4 +60,4 @@ Environment variables are meant to contain common information for the `Nginx` co | `BASIC_AUTH` | `restricted` | By not setting `BASIC_AUTH` this will instruct Lagoon to automatically enable basic authentication if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are set. To disable basic authentication even if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are set, set `BASIC_AUTH` to `off`. | | `BASIC_AUTH_USERNAME` | \(not set\) | Username for basic authentication | | `BASIC_AUTH_PASSWORD` | \(not set\) | Password for basic authentication \(unencrypted\) | -| `FAST_HEALTH_CHECK` | \(not set\) | If set to `on` this will redirect GET requests from certain user agents (StatusCake, Pingdom, Site25x7, Uptime, nagios) to the lightweight Lagoon service healthcheck. | +| `FAST_HEALTH_CHECK` | \(not set\) | If set to `true` this will redirect GET requests from certain user agents (StatusCake, Pingdom, Site25x7, Uptime, nagios) to the lightweight Lagoon service healthcheck. | diff --git a/images/nginx/docker-entrypoint b/images/nginx/docker-entrypoint index c7bdac8ae1..ebef6d365d 100755 --- a/images/nginx/docker-entrypoint +++ b/images/nginx/docker-entrypoint @@ -31,7 +31,7 @@ else cp /etc/nginx/conf.d/healthz.locations.lua.disable /etc/nginx/conf.d/healthz.locations fi -if [ "$FAST_HEALTH_CHECK" == "on" ]; then +if [ "$FAST_HEALTH_CHECK" == "true" ]; then echo "FAST HEALTH CHECK ENABLED" cp /etc/nginx/helpers/90_healthz_fast_check.conf.disabled /etc/nginx/helpers/90_health_fast_check.conf fi \ No newline at end of file From 47943a1821d7d97b6d67e64dc30bf193aff70388 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Fri, 10 Jul 2020 13:55:54 +1200 Subject: [PATCH 245/280] Removes the redundant step of overwriting the lua healthcheck default with a duplicate lua file --- images/nginx/docker-entrypoint | 5 +---- images/nginx/healthcheck/healthz.locations.lua.disable | 8 -------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 images/nginx/healthcheck/healthz.locations.lua.disable diff --git a/images/nginx/docker-entrypoint b/images/nginx/docker-entrypoint index ebef6d365d..2f8458ec4a 100755 --- a/images/nginx/docker-entrypoint +++ b/images/nginx/docker-entrypoint @@ -24,11 +24,8 @@ ep /etc/nginx/helpers/* # If PHP is enabled, we override the Luascript /healthz check echo "Setting up Healthz routing" if [ ! -z "$NGINX_FASTCGI_PASS" ]; then - echo "Setting up Healthz routing - using PHP" + echo "Healthz routing - using PHP" cp /etc/nginx/conf.d/healthz.locations.php.disable /etc/nginx/conf.d/healthz.locations -else - echo "Setting up Healthz routing - using Lua as fallback" - cp /etc/nginx/conf.d/healthz.locations.lua.disable /etc/nginx/conf.d/healthz.locations fi if [ "$FAST_HEALTH_CHECK" == "true" ]; then diff --git a/images/nginx/healthcheck/healthz.locations.lua.disable b/images/nginx/healthcheck/healthz.locations.lua.disable deleted file mode 100644 index 95cf2ed753..0000000000 --- a/images/nginx/healthcheck/healthz.locations.lua.disable +++ /dev/null @@ -1,8 +0,0 @@ -location /.lagoonhealthz { - content_by_lua_block { - ngx.status = ngx.HTTP_OK; - ngx.header.content_type = 'application/json'; - ngx.say('{"check_nginx":"pass"}'); - ngx.exit(ngx.OK); - } -} From 47083ce1aff8e1b4806993b9e03096fdf61b8ba4 Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Fri, 10 Jul 2020 14:02:56 +1200 Subject: [PATCH 246/280] Removed reference to healthcheck lua file in dockerfile --- images/nginx/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/nginx/Dockerfile b/images/nginx/Dockerfile index beed56fdba..311bf2625f 100644 --- a/images/nginx/Dockerfile +++ b/images/nginx/Dockerfile @@ -36,7 +36,7 @@ COPY fastcgi.conf /etc/nginx/fastcgi_params COPY helpers/ /etc/nginx/helpers/ COPY static-files.conf /etc/nginx/conf.d/app.conf COPY redirects-map.conf /etc/nginx/redirects-map.conf -COPY healthcheck/healthz.locations healthcheck/healthz.locations.php.disable healthcheck/healthz.locations.lua.disable /etc/nginx/conf.d/ +COPY healthcheck/healthz.locations healthcheck/healthz.locations.php.disable /etc/nginx/conf.d/ RUN mkdir -p /app \ && rm -f /etc/nginx/conf.d/default.conf \ From b0612ed278a09d18d2235c3c005cebd8863155fc Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Fri, 10 Jul 2020 14:40:29 -0400 Subject: [PATCH 247/280] add redis-password --- .lagoon.secrets.yaml | 10 ++++++++++ services/api-redis/.lagoon.app.yml | 5 +++++ services/api-redis/Dockerfile | 2 ++ services/api/.lagoon.app.yml | 5 +++++ services/api/Dockerfile | 3 ++- 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.lagoon.secrets.yaml b/.lagoon.secrets.yaml index 41928b1a09..6a411006cb 100644 --- a/.lagoon.secrets.yaml +++ b/.lagoon.secrets.yaml @@ -32,6 +32,10 @@ parameters: description: Password used for connecting to the keycloak-db generate: expression from: "[a-zA-Z0-9]{32}" + - name: API_REDIS_PASSWORD + description: Password used for connecting to the api-redis + generate: expression + from: "[a-zA-Z0-9]{32}" - name: SAFE_BRANCH description: Which branch this belongs to, special chars replaced with dashes required: true @@ -99,3 +103,9 @@ objects: name: opendistro-security-cookie-password stringData: OPENDISTRO_SECURITY_COOKIE_PASSWORD: ${OPENDISTRO_SECURITY_COOKIE_PASSWORD} +- kind: Secret + apiVersion: v1 + metadata: + name: api-redis-password + stringData: + API_REDIS_PASSWORD: ${API_REDIS_PASSWORD} diff --git a/services/api-redis/.lagoon.app.yml b/services/api-redis/.lagoon.app.yml index 48b7166b89..e4ec8adba4 100644 --- a/services/api-redis/.lagoon.app.yml +++ b/services/api-redis/.lagoon.app.yml @@ -97,6 +97,11 @@ objects: value: ${SERVICE_NAME} - name: CRONJOBS value: ${CRONJOBS} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: api-redis-password + key: API_REDIS_PASSWORD resources: requests: cpu: 10m diff --git a/services/api-redis/Dockerfile b/services/api-redis/Dockerfile index 5f8d986391..3392398948 100644 --- a/services/api-redis/Dockerfile +++ b/services/api-redis/Dockerfile @@ -1,2 +1,4 @@ ARG IMAGE_REPO FROM ${IMAGE_REPO:-lagoon}/redis + +ENV REDIS_PASSWORD=admin \ No newline at end of file diff --git a/services/api/.lagoon.app.yml b/services/api/.lagoon.app.yml index 67765edb33..005463723f 100644 --- a/services/api/.lagoon.app.yml +++ b/services/api/.lagoon.app.yml @@ -134,6 +134,11 @@ objects: secretKeyRef: name: api-db-password key: API_DB_PASSWORD + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: api-redis-password + key: API_REDIS_PASSWORD - name: SERVICE_NAME value: ${SERVICE_NAME} - name: CRONJOBS diff --git a/services/api/Dockerfile b/services/api/Dockerfile index 7e220f2abb..ba213b25ca 100644 --- a/services/api/Dockerfile +++ b/services/api/Dockerfile @@ -28,7 +28,8 @@ ENV NODE_ENV=production \ ELASTICSEARCH_HOST=logs-db-service:9200 \ ELASTICSEARCH_URL=http://logs-db-service:9200 \ KEYCLOAK_API_CLIENT_SECRET=39d5282d-3684-4026-b4ed-04bbc034b61a \ - HARBOR_ADMIN_PASSWORD=admin + HARBOR_ADMIN_PASSWORD=admin \ + REDIS_PASSWORD=admin # The API is not very resilient to sudden mariadb restarts which can happen when the api and mariadb are starting # at the same time. So we have a small entrypoint which waits for mariadb to be fully ready. From b010c4af2c7be33b1bc25dd707eb266a48c39ffb Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Fri, 10 Jul 2020 14:56:30 -0400 Subject: [PATCH 248/280] v1.8.0 --- docker-compose.yaml | 92 +++++++++++++++---------------- lagoon-remote/docker-compose.yaml | 20 +++---- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 5b568c5df7..4446be4cd3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,7 @@ services: labels: lagoon.type: custom lagoon.template: services/api-db/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api-db:v1-7-1 + lagoon.image: amazeeiolagoon/api-db:v1-8-0 webhook-handler: image: ${IMAGE_REPO:-lagoon}/webhook-handler command: yarn run dev @@ -22,7 +22,7 @@ services: labels: lagoon.type: custom lagoon.template: services/webhook-handler/.lagoon.app.yml - lagoon.image: amazeeiolagoon/webhook-handler:v1-7-1 + lagoon.image: amazeeiolagoon/webhook-handler:v1-8-0 backup-handler: image: ${IMAGE_REPO:-lagoon}/backup-handler restart: on-failure @@ -31,7 +31,7 @@ services: labels: lagoon.type: custom lagoon.template: services/backup-handler/.lagoon.app.yml - lagoon.image: amazeeiolagoon/backup-handler:v1-7-1 + lagoon.image: amazeeiolagoon/backup-handler:v1-8-0 depends_on: - broker broker: @@ -42,7 +42,7 @@ services: labels: lagoon.type: rabbitmq-cluster lagoon.template: services/broker/.lagoon.app.yml - lagoon.image: amazeeiolagoon/broker:v1-7-1 + lagoon.image: amazeeiolagoon/broker:v1-8-0 openshiftremove: image: ${IMAGE_REPO:-lagoon}/openshiftremove command: yarn run dev @@ -52,7 +52,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftremove/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftremove:v1-7-1 + lagoon.image: amazeeiolagoon/openshiftremove:v1-8-0 openshiftbuilddeploy: image: ${IMAGE_REPO:-lagoon}/openshiftbuilddeploy command: yarn run dev @@ -64,7 +64,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftbuilddeploy/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftbuilddeploy:v1-7-1 + lagoon.image: amazeeiolagoon/openshiftbuilddeploy:v1-8-0 openshiftbuilddeploymonitor: image: ${IMAGE_REPO:-lagoon}/openshiftbuilddeploymonitor command: yarn run dev @@ -78,7 +78,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftbuilddeploymonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftbuilddeploymonitor:v1-7-1 + lagoon.image: amazeeiolagoon/openshiftbuilddeploymonitor:v1-8-0 openshiftjobs: image: ${IMAGE_REPO:-lagoon}/openshiftjobs command: yarn run dev @@ -92,7 +92,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftjobs/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftjobs:v1-7-1 + lagoon.image: amazeeiolagoon/openshiftjobs:v1-8-0 openshiftjobsmonitor: image: ${IMAGE_REPO:-lagoon}/openshiftjobsmonitor command: yarn run dev @@ -102,7 +102,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftjobsmonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftjobsmonitor:v1-7-1 + lagoon.image: amazeeiolagoon/openshiftjobsmonitor:v1-8-0 openshiftmisc: image: ${IMAGE_REPO:-lagoon}/openshiftmisc command: yarn run dev @@ -112,7 +112,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftmisc/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftmisc:v1-7-1 + lagoon.image: amazeeiolagoon/openshiftmisc:v1-8-0 kubernetesmisc: image: ${IMAGE_REPO:-lagoon}/kubernetesmisc command: yarn run dev @@ -122,7 +122,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesmisc/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesmisc:v1-7-1 + lagoon.image: amazeeiolagoon/kubernetesmisc:v1-8-0 kubernetesbuilddeploy: image: ${IMAGE_REPO:-lagoon}/kubernetesbuilddeploy command: yarn run dev @@ -135,7 +135,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesbuilddeploy/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesbuilddeploy:v1-7-1 + lagoon.image: amazeeiolagoon/kubernetesbuilddeploy:v1-8-0 kubernetesdeployqueue: image: ${IMAGE_REPO:-lagoon}/kubernetesdeployqueue command: yarn run dev @@ -145,7 +145,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesdeployqueue/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesdeployqueue:v1-7-1 + lagoon.image: amazeeiolagoon/kubernetesdeployqueue:v1-8-0 kubernetesbuilddeploymonitor: image: ${IMAGE_REPO:-lagoon}/kubernetesbuilddeploymonitor command: yarn run dev @@ -159,7 +159,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesbuilddeploymonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesbuilddeploymonitor:v1-7-1 + lagoon.image: amazeeiolagoon/kubernetesbuilddeploymonitor:v1-8-0 kubernetesjobs: image: ${IMAGE_REPO:-lagoon}/kubernetesjobs command: yarn run dev @@ -173,7 +173,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesjobs/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesjobs:v1-7-1 + lagoon.image: amazeeiolagoon/kubernetesjobs:v1-8-0 kubernetesjobsmonitor: image: ${IMAGE_REPO:-lagoon}/kubernetesjobsmonitor command: yarn run dev @@ -187,7 +187,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesjobsmonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesjobsmonitor:v1-7-1 + lagoon.image: amazeeiolagoon/kubernetesjobsmonitor:v1-8-0 kubernetesremove: image: ${IMAGE_REPO:-lagoon}/kubernetesremove command: yarn run dev @@ -197,7 +197,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesremove/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesremove:v1-7-1 + lagoon.image: amazeeiolagoon/kubernetesremove:v1-8-0 logs2rocketchat: image: ${IMAGE_REPO:-lagoon}/logs2rocketchat command: yarn run dev @@ -207,7 +207,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2rocketchat/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2rocketchat:v1-7-1 + lagoon.image: amazeeiolagoon/logs2rocketchat:v1-8-0 logs2slack: image: ${IMAGE_REPO:-lagoon}/logs2slack command: yarn run dev @@ -217,7 +217,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2slack/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2slack:v1-7-1 + lagoon.image: amazeeiolagoon/logs2slack:v1-8-0 logs2microsoftteams: image: ${IMAGE_REPO:-lagoon}/logs2microsoftteams command: yarn run dev @@ -227,7 +227,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2microsoftteams/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2microsoftteams:v1-7-1 + lagoon.image: amazeeiolagoon/logs2microsoftteams:v1-8-0 logs2email: image: ${IMAGE_REPO:-lagoon}/logs2email command: yarn run dev @@ -237,7 +237,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2slack/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2email:v1-7-1 + lagoon.image: amazeeiolagoon/logs2email:v1-8-0 depends_on: - mailhog mailhog: @@ -255,7 +255,7 @@ services: labels: lagoon.type: custom lagoon.template: services/webhooks2tasks/.lagoon.app.yml - lagoon.image: amazeeiolagoon/webhooks2tasks:v1-7-1 + lagoon.image: amazeeiolagoon/webhooks2tasks:v1-8-0 api: image: ${IMAGE_REPO:-lagoon}/api command: yarn run dev @@ -274,7 +274,7 @@ services: labels: lagoon.type: custom lagoon.template: services/api/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api:v1-7-1 + lagoon.image: amazeeiolagoon/api:v1-8-0 ui: image: ${IMAGE_REPO:-lagoon}/ui command: yarn run dev @@ -288,7 +288,7 @@ services: labels: lagoon.type: custom lagoon.template: services/ui/.lagoon.app.yml - lagoon.image: amazeeiolagoon/ui:v1-7-1 + lagoon.image: amazeeiolagoon/ui:v1-8-0 ssh: image: ${IMAGE_REPO:-lagoon}/ssh depends_on: @@ -309,7 +309,7 @@ services: labels: lagoon.type: custom lagoon.template: services/ssh/.lagoon.app.yml - lagoon.image: amazeeiolagoon/ssh:v1-7-1 + lagoon.image: amazeeiolagoon/ssh:v1-8-0 auth-server: image: ${IMAGE_REPO:-lagoon}/auth-server command: yarn run dev @@ -323,7 +323,7 @@ services: labels: lagoon.type: custom lagoon.template: services/auth-server/.lagoon.app.yml - lagoon.image: amazeeiolagoon/auth-server:v1-7-1 + lagoon.image: amazeeiolagoon/auth-server:v1-8-0 keycloak: image: ${IMAGE_REPO:-lagoon}/keycloak user: '111111111' @@ -334,7 +334,7 @@ services: labels: lagoon.type: custom lagoon.template: services/keycloak/.lagoon.app.yml - lagoon.image: amazeeiolagoon/keycloak:v1-7-1 + lagoon.image: amazeeiolagoon/keycloak:v1-8-0 keycloak-db: image: ${IMAGE_REPO:-lagoon}/keycloak-db ports: @@ -342,7 +342,7 @@ services: labels: lagoon.type: custom lagoon.template: services/keycloak-db/.lagoon.app.yml - lagoon.image: amazeeiolagoon/keycloak-db:v1-7-1 + lagoon.image: amazeeiolagoon/keycloak-db:v1-8-0 tests-kubernetes: image: ${IMAGE_REPO:-lagoon}/tests environment: @@ -458,7 +458,7 @@ services: labels: lagoon.type: custom lagoon.template: services/drush-alias/.lagoon.app.yml - lagoon.image: amazeeiolagoon/drush-alias:v1-7-1 + lagoon.image: amazeeiolagoon/drush-alias:v1-8-0 version: '2' logs-db: image: ${IMAGE_REPO:-lagoon}/logs-db @@ -474,14 +474,14 @@ services: labels: lagoon.type: elasticsearch lagoon.template: services/logs-db/.lagoon.single.yml - lagoon.image: amazeeiolagoon/logs-db:v1-7-1 + lagoon.image: amazeeiolagoon/logs-db:v1-8-0 logs-forwarder: image: ${IMAGE_REPO:-lagoon}/logs-forwarder user: '111111111' labels: lagoon.type: custom lagoon.template: services/logs-forwarder/.lagoon.single.yml - lagoon.image: amazeeiolagoon/logs-forwarder:v1-7-1 + lagoon.image: amazeeiolagoon/logs-forwarder:v1-8-0 logs-db-ui: image: ${IMAGE_REPO:-lagoon}/logs-db-ui user: '111111111' @@ -493,14 +493,14 @@ services: labels: lagoon.type: kibana lagoon.template: services/logs-db-ui/.lagoon.yml - lagoon.image: amazeeiolagoon/logs-db-ui:v1-7-1 + lagoon.image: amazeeiolagoon/logs-db-ui:v1-8-0 logs-db-curator: image: ${IMAGE_REPO:-lagoon}/logs-db-curator user: '111111111' labels: lagoon.type: cli lagoon.template: services/logs-db-curator/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs-db-curator:v1-7-1 + lagoon.image: amazeeiolagoon/logs-db-curator:v1-8-0 logs2logs-db: image: ${IMAGE_REPO:-lagoon}/logs2logs-db user: '111111111' @@ -516,7 +516,7 @@ services: labels: lagoon.type: logstash lagoon.template: services/logs2logs-db/.lagoon.yml - lagoon.image: amazeeiolagoon/logs2logs-db:v1-7-1 + lagoon.image: amazeeiolagoon/logs2logs-db:v1-8-0 auto-idler: image: ${IMAGE_REPO:-lagoon}/auto-idler user: '111111111' @@ -529,7 +529,7 @@ services: labels: lagoon.type: custom lagoon.template: services/auto-idler/.lagoon.yml - lagoon.image: amazeeiolagoon/auto-idler:v1-7-1 + lagoon.image: amazeeiolagoon/auto-idler:v1-8-0 storage-calculator: image: ${IMAGE_REPO:-lagoon}/storage-calculator user: '111111111' @@ -538,7 +538,7 @@ services: labels: lagoon.type: custom lagoon.template: services/storage-calculator/.lagoon.yml - lagoon.image: amazeeiolagoon/storage-calculator:v1-7-1 + lagoon.image: amazeeiolagoon/storage-calculator:v1-8-0 logs-collector: image: openshift/origin-logging-fluentd:v3.6.1 labels: @@ -610,7 +610,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-core/harbor-core.yml - lagoon.image: amazeeiolagoon/harbor-core:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-core:v1-8-0 harbor-database: image: ${IMAGE_REPO:-lagoon}/harbor-database hostname: harbor-database @@ -624,7 +624,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-database/harbor-database.yml - lagoon.image: amazeeiolagoon/harbor-database:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-database:v1-8-0 harbor-jobservice: image: ${IMAGE_REPO:-lagoon}/harbor-jobservice hostname: harbor-jobservice @@ -653,7 +653,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-jobservice/harbor-jobservice.yml - lagoon.image: amazeeiolagoon/harbor-jobservice:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-jobservice:v1-8-0 harbor-nginx: image: ${IMAGE_REPO:-lagoon}/harbor-nginx hostname: harbor-nginx @@ -669,7 +669,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-nginx/harbor-nginx.yml - lagoon.image: amazeeiolagoon/harbor-nginx:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-nginx:v1-8-0 harbor-portal: image: ${IMAGE_REPO:-lagoon}/harbor-portal hostname: harbor-portal @@ -679,7 +679,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-portal/harbor-portal.yml - lagoon.image: amazeeiolagoon/harbor-portal:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-portal:v1-8-0 harbor-redis: image: ${IMAGE_REPO:-lagoon}/harbor-redis hostname: harbor-redis @@ -689,7 +689,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-redis/harbor-redis.yml - lagoon.image: amazeeiolagoon/harbor-redis:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-redis:v1-8-0 harbor-trivy: image: ${IMAGE_REPO:-lagoon}/harbor-trivy hostname: harbor-trivy @@ -721,7 +721,7 @@ services: lagoon.type: custom lagoon.template: services/harbor-trivy/harbor-trivy.yml lagoon.name: harbor-trivy - lagoon.image: amazeeiolagoon/harbor-trivy:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-trivy:v1-8-0 harborregistry: image: ${IMAGE_REPO:-lagoon}/harborregistry hostname: harborregistry @@ -743,7 +743,7 @@ services: lagoon.type: custom lagoon.template: services/harborregistry/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistry:v1-7-1 + lagoon.image: amazeeiolagoon/harborregistry:v1-8-0 harborregistryctl: image: ${IMAGE_REPO:-lagoon}/harborregistryctl hostname: harborregistryctl @@ -758,10 +758,10 @@ services: lagoon.type: custom lagoon.template: services/harborregistryctl/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-1 + lagoon.image: amazeeiolagoon/harborregistryctl:v1-8-0 api-redis: image: ${IMAGE_REPO:-lagoon}/api-redis labels: lagoon.type: custom lagoon.template: services/api-redis/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api-redis:v1-7-1 + lagoon.image: amazeeiolagoon/api-redis:v1-8-0 diff --git a/lagoon-remote/docker-compose.yaml b/lagoon-remote/docker-compose.yaml index 77ce2433fe..1b35b89282 100644 --- a/lagoon-remote/docker-compose.yaml +++ b/lagoon-remote/docker-compose.yaml @@ -35,61 +35,61 @@ services: lagoon.type: custom lagoon.template: harborclair/harborclair.yml lagoon.name: harborclair - lagoon.image: amazeeiolagoon/harborclair:v1-7-1 + lagoon.image: amazeeiolagoon/harborclair:v1-8-0 harborclairadapter: image: ${IMAGE_REPO:-lagoon}/harborclairadapter labels: lagoon.type: custom lagoon.template: harborclairadapter/harborclair.yml lagoon.name: harborclair - lagoon.image: amazeeiolagoon/harborclairadapter:v1-7-1 + lagoon.image: amazeeiolagoon/harborclairadapter:v1-8-0 harbor-core: image: ${IMAGE_REPO:-lagoon}/harbor-core labels: lagoon.type: custom lagoon.template: harbor-core/harbor-core.yml - lagoon.image: amazeeiolagoon/harbor-core:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-core:v1-8-0 harbor-database: image: ${IMAGE_REPO:-lagoon}/harbor-database labels: lagoon.type: custom lagoon.template: harbor-database/harbor-database.yml - lagoon.image: amazeeiolagoon/harbor-database:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-database:v1-8-0 harbor-jobservice: image: ${IMAGE_REPO:-lagoon}/harbor-jobservice labels: lagoon.type: custom lagoon.template: harbor-jobservice/harbor-jobservice.yml - lagoon.image: amazeeiolagoon/harbor-jobservice:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-jobservice:v1-8-0 harbor-nginx: image: ${IMAGE_REPO:-lagoon}/harbor-nginx labels: lagoon.type: custom lagoon.template: harbor-nginx/harbor-nginx.yml - lagoon.image: amazeeiolagoon/harbor-nginx:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-nginx:v1-8-0 harbor-portal: image: ${IMAGE_REPO:-lagoon}/harbor-portal labels: lagoon.type: custom lagoon.template: harbor-portal/harbor-portal.yml - lagoon.image: amazeeiolagoon/harbor-portal:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-portal:v1-8-0 harbor-redis: image: ${IMAGE_REPO:-lagoon}/harbor-redis labels: lagoon.type: custom lagoon.template: harbor-redis/harbor-redis.yml - lagoon.image: amazeeiolagoon/harbor-redis:v1-7-1 + lagoon.image: amazeeiolagoon/harbor-redis:v1-8-0 harborregistry: image: ${IMAGE_REPO:-lagoon}/harborregistry labels: lagoon.type: custom lagoon.template: harborregistry/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistry:v1-7-1 + lagoon.image: amazeeiolagoon/harborregistry:v1-8-0 harborregistryctl: image: ${IMAGE_REPO:-lagoon}/harborregistryctl labels: lagoon.type: custom lagoon.template: harborregistryctl/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistryctl:v1-7-1 + lagoon.image: amazeeiolagoon/harborregistryctl:v1-8-0 From 04941d91ed50b6f2a1b03f1aa4b322b6bf7b7b7f Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Sat, 11 Jul 2020 16:27:25 -0400 Subject: [PATCH 249/280] 1.8.0 - bug fix for BillingCost query, missing sqlClient for Project Model --- services/api/src/apolloServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index 8e1584bf8a..b0ab7b474c 100644 --- a/services/api/src/apolloServer.js +++ b/services/api/src/apolloServer.js @@ -84,7 +84,7 @@ const apolloServer = new ApolloServer({ requestCache, models: { UserModel: User.User({ keycloakAdminClient, redisClient }), - GroupModel: Group.Group({ keycloakAdminClient, redisClient }), + GroupModel: Group.Group({ keycloakAdminClient, sqlClient, redisClient }), BillingModel: BillingModel.BillingModel({ keycloakAdminClient, sqlClient @@ -139,7 +139,7 @@ const apolloServer = new ApolloServer({ requestCache, models: { UserModel: User.User({ keycloakAdminClient, redisClient }), - GroupModel: Group.Group({ keycloakAdminClient, redisClient }), + GroupModel: Group.Group({ keycloakAdminClient, sqlClient, redisClient }), BillingModel: BillingModel.BillingModel({ keycloakAdminClient, sqlClient From 68a2d95e7ac19ccc11e627453ce10bc96af14aa5 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Sun, 12 Jul 2020 15:56:44 -0400 Subject: [PATCH 250/280] v1.8.1 --- docker-compose.yaml | 92 +++++++++++++++---------------- lagoon-remote/docker-compose.yaml | 20 +++---- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 4446be4cd3..d46cfb244d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,7 @@ services: labels: lagoon.type: custom lagoon.template: services/api-db/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api-db:v1-8-0 + lagoon.image: amazeeiolagoon/api-db:v1-8-1 webhook-handler: image: ${IMAGE_REPO:-lagoon}/webhook-handler command: yarn run dev @@ -22,7 +22,7 @@ services: labels: lagoon.type: custom lagoon.template: services/webhook-handler/.lagoon.app.yml - lagoon.image: amazeeiolagoon/webhook-handler:v1-8-0 + lagoon.image: amazeeiolagoon/webhook-handler:v1-8-1 backup-handler: image: ${IMAGE_REPO:-lagoon}/backup-handler restart: on-failure @@ -31,7 +31,7 @@ services: labels: lagoon.type: custom lagoon.template: services/backup-handler/.lagoon.app.yml - lagoon.image: amazeeiolagoon/backup-handler:v1-8-0 + lagoon.image: amazeeiolagoon/backup-handler:v1-8-1 depends_on: - broker broker: @@ -42,7 +42,7 @@ services: labels: lagoon.type: rabbitmq-cluster lagoon.template: services/broker/.lagoon.app.yml - lagoon.image: amazeeiolagoon/broker:v1-8-0 + lagoon.image: amazeeiolagoon/broker:v1-8-1 openshiftremove: image: ${IMAGE_REPO:-lagoon}/openshiftremove command: yarn run dev @@ -52,7 +52,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftremove/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftremove:v1-8-0 + lagoon.image: amazeeiolagoon/openshiftremove:v1-8-1 openshiftbuilddeploy: image: ${IMAGE_REPO:-lagoon}/openshiftbuilddeploy command: yarn run dev @@ -64,7 +64,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftbuilddeploy/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftbuilddeploy:v1-8-0 + lagoon.image: amazeeiolagoon/openshiftbuilddeploy:v1-8-1 openshiftbuilddeploymonitor: image: ${IMAGE_REPO:-lagoon}/openshiftbuilddeploymonitor command: yarn run dev @@ -78,7 +78,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftbuilddeploymonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftbuilddeploymonitor:v1-8-0 + lagoon.image: amazeeiolagoon/openshiftbuilddeploymonitor:v1-8-1 openshiftjobs: image: ${IMAGE_REPO:-lagoon}/openshiftjobs command: yarn run dev @@ -92,7 +92,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftjobs/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftjobs:v1-8-0 + lagoon.image: amazeeiolagoon/openshiftjobs:v1-8-1 openshiftjobsmonitor: image: ${IMAGE_REPO:-lagoon}/openshiftjobsmonitor command: yarn run dev @@ -102,7 +102,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftjobsmonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftjobsmonitor:v1-8-0 + lagoon.image: amazeeiolagoon/openshiftjobsmonitor:v1-8-1 openshiftmisc: image: ${IMAGE_REPO:-lagoon}/openshiftmisc command: yarn run dev @@ -112,7 +112,7 @@ services: labels: lagoon.type: custom lagoon.template: services/openshiftmisc/.lagoon.app.yml - lagoon.image: amazeeiolagoon/openshiftmisc:v1-8-0 + lagoon.image: amazeeiolagoon/openshiftmisc:v1-8-1 kubernetesmisc: image: ${IMAGE_REPO:-lagoon}/kubernetesmisc command: yarn run dev @@ -122,7 +122,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesmisc/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesmisc:v1-8-0 + lagoon.image: amazeeiolagoon/kubernetesmisc:v1-8-1 kubernetesbuilddeploy: image: ${IMAGE_REPO:-lagoon}/kubernetesbuilddeploy command: yarn run dev @@ -135,7 +135,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesbuilddeploy/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesbuilddeploy:v1-8-0 + lagoon.image: amazeeiolagoon/kubernetesbuilddeploy:v1-8-1 kubernetesdeployqueue: image: ${IMAGE_REPO:-lagoon}/kubernetesdeployqueue command: yarn run dev @@ -145,7 +145,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesdeployqueue/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesdeployqueue:v1-8-0 + lagoon.image: amazeeiolagoon/kubernetesdeployqueue:v1-8-1 kubernetesbuilddeploymonitor: image: ${IMAGE_REPO:-lagoon}/kubernetesbuilddeploymonitor command: yarn run dev @@ -159,7 +159,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesbuilddeploymonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesbuilddeploymonitor:v1-8-0 + lagoon.image: amazeeiolagoon/kubernetesbuilddeploymonitor:v1-8-1 kubernetesjobs: image: ${IMAGE_REPO:-lagoon}/kubernetesjobs command: yarn run dev @@ -173,7 +173,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesjobs/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesjobs:v1-8-0 + lagoon.image: amazeeiolagoon/kubernetesjobs:v1-8-1 kubernetesjobsmonitor: image: ${IMAGE_REPO:-lagoon}/kubernetesjobsmonitor command: yarn run dev @@ -187,7 +187,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesjobsmonitor/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesjobsmonitor:v1-8-0 + lagoon.image: amazeeiolagoon/kubernetesjobsmonitor:v1-8-1 kubernetesremove: image: ${IMAGE_REPO:-lagoon}/kubernetesremove command: yarn run dev @@ -197,7 +197,7 @@ services: labels: lagoon.type: custom lagoon.template: services/kubernetesremove/.lagoon.app.yml - lagoon.image: amazeeiolagoon/kubernetesremove:v1-8-0 + lagoon.image: amazeeiolagoon/kubernetesremove:v1-8-1 logs2rocketchat: image: ${IMAGE_REPO:-lagoon}/logs2rocketchat command: yarn run dev @@ -207,7 +207,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2rocketchat/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2rocketchat:v1-8-0 + lagoon.image: amazeeiolagoon/logs2rocketchat:v1-8-1 logs2slack: image: ${IMAGE_REPO:-lagoon}/logs2slack command: yarn run dev @@ -217,7 +217,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2slack/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2slack:v1-8-0 + lagoon.image: amazeeiolagoon/logs2slack:v1-8-1 logs2microsoftteams: image: ${IMAGE_REPO:-lagoon}/logs2microsoftteams command: yarn run dev @@ -227,7 +227,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2microsoftteams/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2microsoftteams:v1-8-0 + lagoon.image: amazeeiolagoon/logs2microsoftteams:v1-8-1 logs2email: image: ${IMAGE_REPO:-lagoon}/logs2email command: yarn run dev @@ -237,7 +237,7 @@ services: labels: lagoon.type: custom lagoon.template: services/logs2slack/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs2email:v1-8-0 + lagoon.image: amazeeiolagoon/logs2email:v1-8-1 depends_on: - mailhog mailhog: @@ -255,7 +255,7 @@ services: labels: lagoon.type: custom lagoon.template: services/webhooks2tasks/.lagoon.app.yml - lagoon.image: amazeeiolagoon/webhooks2tasks:v1-8-0 + lagoon.image: amazeeiolagoon/webhooks2tasks:v1-8-1 api: image: ${IMAGE_REPO:-lagoon}/api command: yarn run dev @@ -274,7 +274,7 @@ services: labels: lagoon.type: custom lagoon.template: services/api/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api:v1-8-0 + lagoon.image: amazeeiolagoon/api:v1-8-1 ui: image: ${IMAGE_REPO:-lagoon}/ui command: yarn run dev @@ -288,7 +288,7 @@ services: labels: lagoon.type: custom lagoon.template: services/ui/.lagoon.app.yml - lagoon.image: amazeeiolagoon/ui:v1-8-0 + lagoon.image: amazeeiolagoon/ui:v1-8-1 ssh: image: ${IMAGE_REPO:-lagoon}/ssh depends_on: @@ -309,7 +309,7 @@ services: labels: lagoon.type: custom lagoon.template: services/ssh/.lagoon.app.yml - lagoon.image: amazeeiolagoon/ssh:v1-8-0 + lagoon.image: amazeeiolagoon/ssh:v1-8-1 auth-server: image: ${IMAGE_REPO:-lagoon}/auth-server command: yarn run dev @@ -323,7 +323,7 @@ services: labels: lagoon.type: custom lagoon.template: services/auth-server/.lagoon.app.yml - lagoon.image: amazeeiolagoon/auth-server:v1-8-0 + lagoon.image: amazeeiolagoon/auth-server:v1-8-1 keycloak: image: ${IMAGE_REPO:-lagoon}/keycloak user: '111111111' @@ -334,7 +334,7 @@ services: labels: lagoon.type: custom lagoon.template: services/keycloak/.lagoon.app.yml - lagoon.image: amazeeiolagoon/keycloak:v1-8-0 + lagoon.image: amazeeiolagoon/keycloak:v1-8-1 keycloak-db: image: ${IMAGE_REPO:-lagoon}/keycloak-db ports: @@ -342,7 +342,7 @@ services: labels: lagoon.type: custom lagoon.template: services/keycloak-db/.lagoon.app.yml - lagoon.image: amazeeiolagoon/keycloak-db:v1-8-0 + lagoon.image: amazeeiolagoon/keycloak-db:v1-8-1 tests-kubernetes: image: ${IMAGE_REPO:-lagoon}/tests environment: @@ -458,7 +458,7 @@ services: labels: lagoon.type: custom lagoon.template: services/drush-alias/.lagoon.app.yml - lagoon.image: amazeeiolagoon/drush-alias:v1-8-0 + lagoon.image: amazeeiolagoon/drush-alias:v1-8-1 version: '2' logs-db: image: ${IMAGE_REPO:-lagoon}/logs-db @@ -474,14 +474,14 @@ services: labels: lagoon.type: elasticsearch lagoon.template: services/logs-db/.lagoon.single.yml - lagoon.image: amazeeiolagoon/logs-db:v1-8-0 + lagoon.image: amazeeiolagoon/logs-db:v1-8-1 logs-forwarder: image: ${IMAGE_REPO:-lagoon}/logs-forwarder user: '111111111' labels: lagoon.type: custom lagoon.template: services/logs-forwarder/.lagoon.single.yml - lagoon.image: amazeeiolagoon/logs-forwarder:v1-8-0 + lagoon.image: amazeeiolagoon/logs-forwarder:v1-8-1 logs-db-ui: image: ${IMAGE_REPO:-lagoon}/logs-db-ui user: '111111111' @@ -493,14 +493,14 @@ services: labels: lagoon.type: kibana lagoon.template: services/logs-db-ui/.lagoon.yml - lagoon.image: amazeeiolagoon/logs-db-ui:v1-8-0 + lagoon.image: amazeeiolagoon/logs-db-ui:v1-8-1 logs-db-curator: image: ${IMAGE_REPO:-lagoon}/logs-db-curator user: '111111111' labels: lagoon.type: cli lagoon.template: services/logs-db-curator/.lagoon.app.yml - lagoon.image: amazeeiolagoon/logs-db-curator:v1-8-0 + lagoon.image: amazeeiolagoon/logs-db-curator:v1-8-1 logs2logs-db: image: ${IMAGE_REPO:-lagoon}/logs2logs-db user: '111111111' @@ -516,7 +516,7 @@ services: labels: lagoon.type: logstash lagoon.template: services/logs2logs-db/.lagoon.yml - lagoon.image: amazeeiolagoon/logs2logs-db:v1-8-0 + lagoon.image: amazeeiolagoon/logs2logs-db:v1-8-1 auto-idler: image: ${IMAGE_REPO:-lagoon}/auto-idler user: '111111111' @@ -529,7 +529,7 @@ services: labels: lagoon.type: custom lagoon.template: services/auto-idler/.lagoon.yml - lagoon.image: amazeeiolagoon/auto-idler:v1-8-0 + lagoon.image: amazeeiolagoon/auto-idler:v1-8-1 storage-calculator: image: ${IMAGE_REPO:-lagoon}/storage-calculator user: '111111111' @@ -538,7 +538,7 @@ services: labels: lagoon.type: custom lagoon.template: services/storage-calculator/.lagoon.yml - lagoon.image: amazeeiolagoon/storage-calculator:v1-8-0 + lagoon.image: amazeeiolagoon/storage-calculator:v1-8-1 logs-collector: image: openshift/origin-logging-fluentd:v3.6.1 labels: @@ -610,7 +610,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-core/harbor-core.yml - lagoon.image: amazeeiolagoon/harbor-core:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-core:v1-8-1 harbor-database: image: ${IMAGE_REPO:-lagoon}/harbor-database hostname: harbor-database @@ -624,7 +624,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-database/harbor-database.yml - lagoon.image: amazeeiolagoon/harbor-database:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-database:v1-8-1 harbor-jobservice: image: ${IMAGE_REPO:-lagoon}/harbor-jobservice hostname: harbor-jobservice @@ -653,7 +653,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-jobservice/harbor-jobservice.yml - lagoon.image: amazeeiolagoon/harbor-jobservice:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-jobservice:v1-8-1 harbor-nginx: image: ${IMAGE_REPO:-lagoon}/harbor-nginx hostname: harbor-nginx @@ -669,7 +669,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-nginx/harbor-nginx.yml - lagoon.image: amazeeiolagoon/harbor-nginx:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-nginx:v1-8-1 harbor-portal: image: ${IMAGE_REPO:-lagoon}/harbor-portal hostname: harbor-portal @@ -679,7 +679,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-portal/harbor-portal.yml - lagoon.image: amazeeiolagoon/harbor-portal:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-portal:v1-8-1 harbor-redis: image: ${IMAGE_REPO:-lagoon}/harbor-redis hostname: harbor-redis @@ -689,7 +689,7 @@ services: labels: lagoon.type: custom lagoon.template: services/harbor-redis/harbor-redis.yml - lagoon.image: amazeeiolagoon/harbor-redis:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-redis:v1-8-1 harbor-trivy: image: ${IMAGE_REPO:-lagoon}/harbor-trivy hostname: harbor-trivy @@ -721,7 +721,7 @@ services: lagoon.type: custom lagoon.template: services/harbor-trivy/harbor-trivy.yml lagoon.name: harbor-trivy - lagoon.image: amazeeiolagoon/harbor-trivy:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-trivy:v1-8-1 harborregistry: image: ${IMAGE_REPO:-lagoon}/harborregistry hostname: harborregistry @@ -743,7 +743,7 @@ services: lagoon.type: custom lagoon.template: services/harborregistry/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistry:v1-8-0 + lagoon.image: amazeeiolagoon/harborregistry:v1-8-1 harborregistryctl: image: ${IMAGE_REPO:-lagoon}/harborregistryctl hostname: harborregistryctl @@ -758,10 +758,10 @@ services: lagoon.type: custom lagoon.template: services/harborregistryctl/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistryctl:v1-8-0 + lagoon.image: amazeeiolagoon/harborregistryctl:v1-8-1 api-redis: image: ${IMAGE_REPO:-lagoon}/api-redis labels: lagoon.type: custom lagoon.template: services/api-redis/.lagoon.app.yml - lagoon.image: amazeeiolagoon/api-redis:v1-8-0 + lagoon.image: amazeeiolagoon/api-redis:v1-8-1 diff --git a/lagoon-remote/docker-compose.yaml b/lagoon-remote/docker-compose.yaml index 1b35b89282..c833ea9320 100644 --- a/lagoon-remote/docker-compose.yaml +++ b/lagoon-remote/docker-compose.yaml @@ -35,61 +35,61 @@ services: lagoon.type: custom lagoon.template: harborclair/harborclair.yml lagoon.name: harborclair - lagoon.image: amazeeiolagoon/harborclair:v1-8-0 + lagoon.image: amazeeiolagoon/harborclair:v1-8-1 harborclairadapter: image: ${IMAGE_REPO:-lagoon}/harborclairadapter labels: lagoon.type: custom lagoon.template: harborclairadapter/harborclair.yml lagoon.name: harborclair - lagoon.image: amazeeiolagoon/harborclairadapter:v1-8-0 + lagoon.image: amazeeiolagoon/harborclairadapter:v1-8-1 harbor-core: image: ${IMAGE_REPO:-lagoon}/harbor-core labels: lagoon.type: custom lagoon.template: harbor-core/harbor-core.yml - lagoon.image: amazeeiolagoon/harbor-core:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-core:v1-8-1 harbor-database: image: ${IMAGE_REPO:-lagoon}/harbor-database labels: lagoon.type: custom lagoon.template: harbor-database/harbor-database.yml - lagoon.image: amazeeiolagoon/harbor-database:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-database:v1-8-1 harbor-jobservice: image: ${IMAGE_REPO:-lagoon}/harbor-jobservice labels: lagoon.type: custom lagoon.template: harbor-jobservice/harbor-jobservice.yml - lagoon.image: amazeeiolagoon/harbor-jobservice:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-jobservice:v1-8-1 harbor-nginx: image: ${IMAGE_REPO:-lagoon}/harbor-nginx labels: lagoon.type: custom lagoon.template: harbor-nginx/harbor-nginx.yml - lagoon.image: amazeeiolagoon/harbor-nginx:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-nginx:v1-8-1 harbor-portal: image: ${IMAGE_REPO:-lagoon}/harbor-portal labels: lagoon.type: custom lagoon.template: harbor-portal/harbor-portal.yml - lagoon.image: amazeeiolagoon/harbor-portal:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-portal:v1-8-1 harbor-redis: image: ${IMAGE_REPO:-lagoon}/harbor-redis labels: lagoon.type: custom lagoon.template: harbor-redis/harbor-redis.yml - lagoon.image: amazeeiolagoon/harbor-redis:v1-8-0 + lagoon.image: amazeeiolagoon/harbor-redis:v1-8-1 harborregistry: image: ${IMAGE_REPO:-lagoon}/harborregistry labels: lagoon.type: custom lagoon.template: harborregistry/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistry:v1-8-0 + lagoon.image: amazeeiolagoon/harborregistry:v1-8-1 harborregistryctl: image: ${IMAGE_REPO:-lagoon}/harborregistryctl labels: lagoon.type: custom lagoon.template: harborregistryctl/harborregistry.yml lagoon.name: harborregistry - lagoon.image: amazeeiolagoon/harborregistryctl:v1-8-0 + lagoon.image: amazeeiolagoon/harborregistryctl:v1-8-1 From 57e9403a9a8f2b57cbd08c3b274f10f58d31ba8d Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Mon, 29 Jun 2020 17:05:37 +0800 Subject: [PATCH 251/280] Always exclude the lagoon-logging namespace from clusterflow This avoids log routing loops when debugging log flow via stdout. --- charts/lagoon-logging/templates/clusterflow.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/charts/lagoon-logging/templates/clusterflow.yaml b/charts/lagoon-logging/templates/clusterflow.yaml index 77e2520e28..4b19d295cb 100644 --- a/charts/lagoon-logging/templates/clusterflow.yaml +++ b/charts/lagoon-logging/templates/clusterflow.yaml @@ -8,11 +8,12 @@ spec: # match entries are considered in order # the empty "select: {}" indicates all namespaces/labels match: - {{- with .Values.excludeNamespaces }} - exclude: namespaces: + - {{ .Release.Namespace }} + {{- with .Values.excludeNamespaces }} {{- toYaml . | nindent 6 }} - {{- end }} + {{- end }} {{- with .Values.selectNamespaces }} - select: namespaces: From 9b06e037092d932b9f11349074952a76975cefca Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Fri, 3 Jul 2020 15:26:56 +0800 Subject: [PATCH 252/280] Improvements to logs-dispatcher fluentd configuration * separate index name components with underscores * fix capitalisation of environmentType tag * add ability to disable hostname verification for upstream logs-concentrator * implement lagoon-logs-* flow --- ...logs-dispatcher.fluent-conf.configmap.yaml | 23 +++++++++++++------ .../logs-dispatcher.statefulset.yaml | 6 +++++ charts/lagoon-logging/values.yaml | 10 +++++--- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml b/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml index bdb32fffc5..2648fc3b96 100644 --- a/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml +++ b/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml @@ -147,7 +147,7 @@ data: @type record_modifier - index_name container-logs-${record.dig('kubernetes','namespace_labels','lagoon_sh/project') || "#{record.dig('kubernetes','namespace_name') || 'unknown_project'}_#{ENV['CLUSTER_NAME']}"}-${record.dig('kubernetes','namespace_labels','lagoon_sh/environmenttype') || "unknown_environmenttype"}-${Time.at(time).strftime("%Y.%m")} + index_name container-logs-${record.dig('kubernetes','namespace_labels','lagoon_sh/project') || "#{record.dig('kubernetes','namespace_name') || 'unknown_project'}_#{ENV['CLUSTER_NAME']}"}-_-${record.dig('kubernetes','namespace_labels','lagoon_sh/environmentType') || "unknown_environmenttype"}-_-${Time.at(time).strftime("%Y.%m")} # post-process to try to eke some more structure out of the logs. @@ -201,7 +201,7 @@ data: @type record_modifier - index_name application-logs-${record.dig('kubernetes','namespace_labels','lagoon_sh/project') || "#{record.dig('kubernetes','namespace_name') || 'unknown_project'}_#{ENV['CLUSTER_NAME']}"}-${record.dig('kubernetes','namespace_labels','lagoon_sh/environmenttype') || "unknown_environmenttype"}-${Time.at(time).strftime("%Y.%m")} + index_name application-logs-${record.dig('kubernetes','namespace_labels','lagoon_sh/project') || "#{record.dig('kubernetes','namespace_name') || 'unknown_project'}_#{ENV['CLUSTER_NAME']}"}-_-${record.dig('kubernetes','namespace_labels','lagoon_sh/environmentType') || "unknown_environmenttype"}-_-${Time.at(time).strftime("%Y.%m")} # strip the kubernetes data as it's duplicated in container/router logs and @@ -266,14 +266,20 @@ data: @type record_modifier - index_name router-logs-${record.dig('kubernetes','namespace_labels','lagoon_sh/project') || "#{record.dig('kubernetes','namespace_name') || 'unknown_project'}_#{ENV['CLUSTER_NAME']}"}-${record.dig('kubernetes','namespace_labels','lagoon_sh/environmenttype') || "unknown_environmenttype"}-${Time.at(time).strftime("%Y.%m")} + index_name router-logs-${record.dig('kubernetes','namespace_labels','lagoon_sh/project') || "#{record.dig('kubernetes','namespace_name') || 'unknown_project'}_#{ENV['CLUSTER_NAME']}"}-_-${record.dig('kubernetes','namespace_labels','lagoon_sh/environmentType') || "unknown_environmenttype"}-_-${Time.at(time).strftime("%Y.%m")} - # DEBUG DELETE ME - - @type stdout - + # + # add the lagoon index_name + # the source for this tag is included when lagoonLogs.enabled is true + # + + @type record_modifier + + index_name lagoon-logs-${record['project']}-_-all_environments-_-${Time.at(time).strftime("%Y.%m")} + + # # forward all to logs-concentrator @@ -290,6 +296,9 @@ data: tls_cert_path /fluentd/tls/ca.crt tls_client_cert_path /fluentd/tls/client.crt tls_client_private_key_path /fluentd/tls/client.key + {{- with .Values.forward.tlsVerifyHostname }} + tls_verify_hostname {{ . }} + {{- end }} # endpoint port "#{ENV['LOGS_FORWARD_HOST_PORT']}" diff --git a/charts/lagoon-logging/templates/logs-dispatcher.statefulset.yaml b/charts/lagoon-logging/templates/logs-dispatcher.statefulset.yaml index 3e79046084..8b0fc15763 100644 --- a/charts/lagoon-logging/templates/logs-dispatcher.statefulset.yaml +++ b/charts/lagoon-logging/templates/logs-dispatcher.statefulset.yaml @@ -109,6 +109,12 @@ spec: name: {{ include "lagoon-logging.logsDispatcher.fullname" . }}-store name: {{ include "lagoon-logging.logsDispatcher.fullname" . }}-store {{- end }} + {{- if .Values.lagoonLogs.enabled }} + - configMap: + defaultMode: 420 + name: {{ include "lagoon-logging.logsDispatcher.fullname" . }}-source-lagoon + name: {{ include "lagoon-logging.logsDispatcher.fullname" . }}-source-lagoon + {{- end }} - secret: defaultMode: 420 secretName: {{ include "lagoon-logging.logsDispatcher.fullname" . }}-tls diff --git a/charts/lagoon-logging/values.yaml b/charts/lagoon-logging/values.yaml index 9151b4eccd..cf69c0d18e 100644 --- a/charts/lagoon-logging/values.yaml +++ b/charts/lagoon-logging/values.yaml @@ -257,10 +257,14 @@ lagoonLogs: # username: "example1" # password: "securepass" # host: "203.0.113.9" -# # hostName is optional, with a fallback to host -# # this is useful for when host is an IP address, or when the server -# # certificate doesn't match the hostName. +# # hostName is optional - it is used for TLS verification for when host is an +# # IP address. +# # NOTE: if host is _not_ an IP address and it is presents a certificate +# # without that hostname, you'll also need to set tlsVerifyHostname to +# # false. The hostName field does _not_ override the host field for TLS +# # verification when host is not an IP address. # hostName: "logs.server.example.com" +# # tlsVerifyHostname: false # # hostPort is optional, default 24224 # hostPort: "24224" # selfHostname: "logs-dispatcher.example1.lagoon.example.com" From e6f33705ae00b86be0ee60f6e8aab64f5ddb2559 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Mon, 29 Jun 2020 22:02:41 +0800 Subject: [PATCH 253/280] Bump the logging-operator chart dependency --- charts/lagoon-logging/Chart.lock | 6 +++--- charts/lagoon-logging/Chart.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/charts/lagoon-logging/Chart.lock b/charts/lagoon-logging/Chart.lock index 95b9fc1235..2edb5a171d 100644 --- a/charts/lagoon-logging/Chart.lock +++ b/charts/lagoon-logging/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com - version: 3.2.2 -digest: sha256:674fd6770bf6e093b0f731967bda8cb68b2b59c376e572361aee06c5467523f1 -generated: "2020-06-08T21:53:43.188700242+08:00" + version: 3.3.0 +digest: sha256:c87737442c1727efe4bcf2e03a191c0e373f89ba3ac19869b83683e3df871fbc +generated: "2020-06-29T22:02:20.819623111+08:00" diff --git a/charts/lagoon-logging/Chart.yaml b/charts/lagoon-logging/Chart.yaml index 6fbebddf22..f60e6d7d8d 100644 --- a/charts/lagoon-logging/Chart.yaml +++ b/charts/lagoon-logging/Chart.yaml @@ -23,4 +23,4 @@ appVersion: 0.1.0 dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com - version: ~3.2.2 + version: ~3.3.0 From eb0c23eab9028f7309d1953beb67a12e04026c02 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 30 Jun 2020 08:27:52 +0800 Subject: [PATCH 254/280] Make all components of lagoon-logging chart HA --- charts/lagoon-logging/templates/logging.yaml | 2 ++ charts/lagoon-logging/values.yaml | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/charts/lagoon-logging/templates/logging.yaml b/charts/lagoon-logging/templates/logging.yaml index c570a32603..9a08dd28f1 100644 --- a/charts/lagoon-logging/templates/logging.yaml +++ b/charts/lagoon-logging/templates/logging.yaml @@ -9,6 +9,8 @@ spec: security: podSecurityContext: runAsUser: 100 + scaling: + replicas: 3 {{- with .Values.fluentbitPrivileged }} fluentbit: security: diff --git a/charts/lagoon-logging/values.yaml b/charts/lagoon-logging/values.yaml index cf69c0d18e..5bd204fb7a 100644 --- a/charts/lagoon-logging/values.yaml +++ b/charts/lagoon-logging/values.yaml @@ -9,7 +9,7 @@ logsDispatcher: name: logs-dispatcher - replicaCount: 2 + replicaCount: 3 image: repository: amazeeiolagoon/logs-dispatcher @@ -64,7 +64,7 @@ logsTeeRouter: name: logs-tee-router - replicaCount: 2 + replicaCount: 3 image: repository: amazeeiolagoon/logs-tee @@ -124,7 +124,7 @@ logsTeeApplication: name: logs-tee-application - replicaCount: 2 + replicaCount: 3 image: repository: amazeeiolagoon/logs-tee From dfbde29bdfc915ff631a98aaad8d893a554ed790 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 30 Jun 2020 09:54:30 +0800 Subject: [PATCH 255/280] Add fsGroup required by openshift --- charts/lagoon-logging/templates/logging.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/lagoon-logging/templates/logging.yaml b/charts/lagoon-logging/templates/logging.yaml index 9a08dd28f1..5edd1bbb23 100644 --- a/charts/lagoon-logging/templates/logging.yaml +++ b/charts/lagoon-logging/templates/logging.yaml @@ -9,6 +9,7 @@ spec: security: podSecurityContext: runAsUser: 100 + fsGroup: 0 scaling: replicas: 3 {{- with .Values.fluentbitPrivileged }} From 6263c9a2352832cf75c060c1fd8f5b69d644ad0d Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Fri, 3 Jul 2020 09:45:52 +0800 Subject: [PATCH 256/280] Exclude more system namespaces by default --- charts/lagoon-logging/values.yaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/charts/lagoon-logging/values.yaml b/charts/lagoon-logging/values.yaml index 5bd204fb7a..6a647aced5 100644 --- a/charts/lagoon-logging/values.yaml +++ b/charts/lagoon-logging/values.yaml @@ -183,11 +183,21 @@ logsTeeApplication: excludeNamespaces: # k8s - cattle-prometheus +- cattle-system +- dbaas-operator +- default +- kube-cleanup-operator +- kube-node-lease +- kube-public - kube-system +- metrics-server - syn +- syn-backup - syn-cert-manager -- syn-synsights - syn-cluster-autoscaler +- syn-efs-provisioner +- syn-resource-locker +- syn-synsights # openshift - acme-controller - appuio-baas-operator @@ -197,9 +207,7 @@ excludeNamespaces: - appuio-monitoring - appuio-pruner - appuio-tiller -- default - dioscuri-controller -- kube-public - kube-service-catalog - management-infra - monitoring-infra @@ -213,7 +221,6 @@ excludeNamespaces: - openshift-node - openshift-sdn - openshift-web-console -- syn-resource-locker - tiller # Configure the cluster output buffer. From 1f55e8acf774c2bad83dc1476661e0bbf80bcd61 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Fri, 3 Jul 2020 11:27:57 +0800 Subject: [PATCH 257/280] Disable logs-tee services by default --- charts/lagoon-logging/values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/lagoon-logging/values.yaml b/charts/lagoon-logging/values.yaml index 6a647aced5..26da090f6f 100644 --- a/charts/lagoon-logging/values.yaml +++ b/charts/lagoon-logging/values.yaml @@ -60,7 +60,7 @@ logsDispatcher: logsTeeRouter: - enabled: true + enabled: false name: logs-tee-router @@ -120,7 +120,7 @@ logsTeeRouter: logsTeeApplication: - enabled: true + enabled: false name: logs-tee-application From ed5fe14f1fa733dd294082c72ef37b3bf6695fc2 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Mon, 6 Jul 2020 16:46:44 +0800 Subject: [PATCH 258/280] Fix lagoon-logging chart on k8s Regression caused by adding OpenShift compatible configuration. --- charts/lagoon-logging/templates/logging.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/charts/lagoon-logging/templates/logging.yaml b/charts/lagoon-logging/templates/logging.yaml index 5edd1bbb23..0150941c18 100644 --- a/charts/lagoon-logging/templates/logging.yaml +++ b/charts/lagoon-logging/templates/logging.yaml @@ -17,5 +17,7 @@ spec: security: securityContext: privileged: {{ . }} + {{- else }} + fluentbit: {} {{- end }} controlNamespace: {{ .Release.Namespace | quote }} From 30f3e0253205322b0d8f552d1363728dc89b08a4 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Mon, 13 Jul 2020 20:49:56 +0800 Subject: [PATCH 259/280] Various improvements to logs-tee service * make socat unidirectional * add debug environment variable * increase transfer block size to maximum for IPv4 UDP * use broadcasts instead of piping to tee --- services/logs-tee/entrypoint.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/logs-tee/entrypoint.sh b/services/logs-tee/entrypoint.sh index 8916af8339..4448632d2c 100755 --- a/services/logs-tee/entrypoint.sh +++ b/services/logs-tee/entrypoint.sh @@ -5,11 +5,12 @@ # - assumes the remaining arguments are UDP endpoints # - duplicates received traffic to each UDP endpoints # - ensures that each defined endpoint resolves before starting +# - echoes to STDOUT if $DEBUG is set to "true" set -euo pipefail set -x -cmd="socat - udp-recvfrom:$1,fork | tee" +socat -b 65507 -u "udp-recvfrom:$1,fork" udp-sendto:127.255.255.255:9999,broadcast & shift @@ -18,7 +19,11 @@ for endpoint in "$@"; do echo "${endpoint/:[0-9]*/} doesn't resolve. retrying in 2 seconds.." sleep 2 done - cmd="$cmd >(socat - udp-sendto:$endpoint)" + socat -b 65507 -u udp-recvfrom:9999,reuseaddr,fork udp-sendto:$endpoint & done -eval "$cmd" +if [[ ${DEBUG:-} = true ]]; then + socat -b 65507 -u udp-recvfrom:9999,reuseaddr,fork - & +fi + +wait -n From eb9340a7df2982d08d143aa05218416b3da98388 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Mon, 13 Jul 2020 20:50:21 +0800 Subject: [PATCH 260/280] Bump logs-dispatcher UDP input buffer Set to max possible size for IPv4 UDP. --- .../templates/logs-dispatcher.fluent-conf.configmap.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml b/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml index 2648fc3b96..c286e27ae5 100644 --- a/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml +++ b/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml @@ -35,6 +35,8 @@ data: @id in_application tag "lagoon.#{ENV['CLUSTER_NAME']}.application" port 5140 + # max IPv4 UDP payload size + message_length_limit 65507 @type json @@ -49,6 +51,8 @@ data: # syslog parameters port 5141 severity_key severity + # max IPv4 UDP payload size + message_length_limit 65507 @type regexp # parse HTTP logs based on the haproxy documentation From 3da36f5bbe6b8a71535ee1f8ccf1043686f8d0b4 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Mon, 29 Jun 2020 22:39:02 +0800 Subject: [PATCH 261/280] Add openshift project network global setting to lagoon-logging README --- charts/lagoon-logging/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/charts/lagoon-logging/README.md b/charts/lagoon-logging/README.md index df51a86b84..aa223c8f02 100644 --- a/charts/lagoon-logging/README.md +++ b/charts/lagoon-logging/README.md @@ -58,14 +58,20 @@ oc adm policy add-scc-to-user nonroot -z lagoon-logging-fluentd # fluentbit daemonset serviceaccount (logging-operator chart) oc adm policy add-scc-to-user privileged -z lagoon-logging-fluentbit - # logs-dispatcher statefulset serviceaccount (lagoon-logging chart) oc adm policy add-scc-to-user anyuid -z lagoon-logging-logs-dispatcher ``` -4. Update application logs service +And make the project network global: +``` +oc adm pod-network make-projects-global lagoon-logging +``` + +4. Update application-logs and router-logs services + +The `application-logs` and `router-logs` services in the `lagoon` namespace needs to be updated to point their `externalName` to the `lagoon-logging-logs-dispatcher` service in the `lagoon-logging` namespace (or wherever you've installed it). -The `application-logs` service in the `lagoon` namespace needs to be updated to point its `externalName` to the `lagoon-logging-logs-dispatcher` service in the `lagoon-logging` namespace (or wherever you've installed it). +If you are migrating from the old lagoon logging infrastructure and want to keep logs flowing to both old and new infrastructure, point these services at the relevant `logs-tee` service in the `lagoon-logging` namespace. See the comments in the chart `values.yaml` for `logs-tee` configuration. ## View logs From 9f72833ad21c57fc27c5ed74b6a54fb1644c9491 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 7 Jul 2020 13:55:56 +0800 Subject: [PATCH 262/280] Use image builds from the logging-updates branch in lagoon-logging --- charts/lagoon-logging/values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/lagoon-logging/values.yaml b/charts/lagoon-logging/values.yaml index 26da090f6f..1d6fc3855a 100644 --- a/charts/lagoon-logging/values.yaml +++ b/charts/lagoon-logging/values.yaml @@ -15,7 +15,7 @@ logsDispatcher: repository: amazeeiolagoon/logs-dispatcher pullPolicy: Always # Overrides the image tag whose default is the chart version. - tag: logs-concentrator + tag: logging-updates serviceAccount: # Specifies whether a service account should be created @@ -70,7 +70,7 @@ logsTeeRouter: repository: amazeeiolagoon/logs-tee pullPolicy: Always # Overrides the image tag whose default is the chart version. - tag: logs-concentrator + tag: logging-updates serviceAccount: # Specifies whether a service account should be created @@ -130,7 +130,7 @@ logsTeeApplication: repository: amazeeiolagoon/logs-tee pullPolicy: Always # Overrides the image tag whose default is the chart version. - tag: logs-concentrator + tag: logging-updates serviceAccount: # Specifies whether a service account should be created From f1f8674189c4880995ec133a4247ed3c6e073780 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 30 Jun 2020 14:23:49 +0800 Subject: [PATCH 263/280] Allow endpoints field to be empty for logs-tee services --- charts/lagoon-logging/README.md | 2 +- .../templates/logs-tee.deployment.yaml | 8 ++++++-- charts/lagoon-logging/values.yaml | 12 +++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/charts/lagoon-logging/README.md b/charts/lagoon-logging/README.md index aa223c8f02..b881442bee 100644 --- a/charts/lagoon-logging/README.md +++ b/charts/lagoon-logging/README.md @@ -71,7 +71,7 @@ oc adm pod-network make-projects-global lagoon-logging The `application-logs` and `router-logs` services in the `lagoon` namespace needs to be updated to point their `externalName` to the `lagoon-logging-logs-dispatcher` service in the `lagoon-logging` namespace (or wherever you've installed it). -If you are migrating from the old lagoon logging infrastructure and want to keep logs flowing to both old and new infrastructure, point these services at the relevant `logs-tee` service in the `lagoon-logging` namespace. See the comments in the chart `values.yaml` for `logs-tee` configuration. +If you are migrating from the old lagoon logging infrastructure and want to keep logs flowing to both old and new infrastructure, point these services at the relevant `logs-tee` service in the `lagoon-logging` namespace. The `logs-tee` services then need to have the legacy `endpoint` configured. See the comments in the chart `values.yaml` for an example. ## View logs diff --git a/charts/lagoon-logging/templates/logs-tee.deployment.yaml b/charts/lagoon-logging/templates/logs-tee.deployment.yaml index 94a74e5f87..bafcb90776 100644 --- a/charts/lagoon-logging/templates/logs-tee.deployment.yaml +++ b/charts/lagoon-logging/templates/logs-tee.deployment.yaml @@ -30,7 +30,9 @@ spec: - {{ .Values.logsTeeRouter.listenPort | quote }} # UDP endpoints out - {{ include "lagoon-logging.logsDispatcher.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:5141 - {{- toYaml .Values.logsTeeRouter.endpoints | nindent 10 }} + {{- with .Values.logsTeeRouter.endpoints }} + {{- toYaml . | nindent 10 }} + {{- end }} ports: - containerPort: {{ .Values.logsTeeRouter.listenPort }} protocol: UDP @@ -94,7 +96,9 @@ spec: - {{ .Values.logsTeeApplication.listenPort | quote }} # UDP endpoints out - {{ include "lagoon-logging.logsDispatcher.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:5140 - {{- toYaml .Values.logsTeeApplication.endpoints | nindent 10 }} + {{- with .Values.logsTeeApplication.endpoints }} + {{- toYaml . | nindent 10 }} + {{- end }} ports: - containerPort: {{ .Values.logsTeeApplication.listenPort }} protocol: UDP diff --git a/charts/lagoon-logging/values.yaml b/charts/lagoon-logging/values.yaml index 1d6fc3855a..a60283f1e6 100644 --- a/charts/lagoon-logging/values.yaml +++ b/charts/lagoon-logging/values.yaml @@ -84,10 +84,11 @@ logsTeeRouter: # view. name: "" - listenPort: 5141 - endpoints: + listenPort: 5140 # the logs-dispatcher endpoint is automatically added to this list - - logs-forwarder-logstash.lagoon.svc.cluster.local:5140 + # define other endpoints here + #endpoints: + #- logs2logs-db.lagoon.svc.cluster.local podAnnotations: {} @@ -145,9 +146,10 @@ logsTeeApplication: name: "" listenPort: 5140 - endpoints: # the logs-dispatcher endpoint is automatically added to this list - - logs-forwarder-logstash.lagoon.svc.cluster.local:5140 + # define other endpoints here + #endpoints: + #- logs2logs-db.lagoon.svc.cluster.local podAnnotations: {} From f4fbe8bf027ffce9a3cf1d17b9c805b34118a736 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Mon, 13 Jul 2020 19:46:53 +0800 Subject: [PATCH 264/280] Fix NOTES for lagoon-logging chart --- charts/lagoon-logging/templates/NOTES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/lagoon-logging/templates/NOTES.txt b/charts/lagoon-logging/templates/NOTES.txt index 7e94f8fde8..311f0a2bee 100644 --- a/charts/lagoon-logging/templates/NOTES.txt +++ b/charts/lagoon-logging/templates/NOTES.txt @@ -2,4 +2,4 @@ Thank you for installing {{ .Chart.Name }}. Your release is named {{ .Release.Name }}. -Your logs are now being sent to {{ coalesce .Values.forward.hostName .Values.forward.host }}:{{ .Values.forward.hostPort }}. +Your logs are now being sent to {{ coalesce .Values.forward.host }}:{{ .Values.forward.hostPort }}. From 73749699215a36daac2f0fcaae97356145e42141 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 1 Jul 2020 14:04:47 +0800 Subject: [PATCH 265/280] Bump lagoon-logging chart version --- charts/lagoon-logging/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/lagoon-logging/Chart.yaml b/charts/lagoon-logging/Chart.yaml index f60e6d7d8d..27055b9510 100644 --- a/charts/lagoon-logging/Chart.yaml +++ b/charts/lagoon-logging/Chart.yaml @@ -12,7 +12,7 @@ type: application # time you make changes to the chart and its templates, including the app # version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.0 +version: 0.6.1 # This is the version number of the application being deployed. This version # number should be incremented each time you make changes to the application. From ef7fcae3c0e7e3e8b43ca1fb750f9d9497e44279 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 2 Jul 2020 14:30:04 +0800 Subject: [PATCH 266/280] Update chart index --- charts/index.yaml | 37 +++++++++++++++++++--- charts/lagoon-logging-0.6.1.tgz | Bin 0 -> 112748 bytes charts/lagoon-logs-concentrator-0.2.0.tgz | Bin 0 -> 5879 bytes 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 charts/lagoon-logging-0.6.1.tgz create mode 100644 charts/lagoon-logs-concentrator-0.2.0.tgz diff --git a/charts/index.yaml b/charts/index.yaml index bf5badc833..5de985f508 100644 --- a/charts/index.yaml +++ b/charts/index.yaml @@ -3,7 +3,22 @@ entries: lagoon-logging: - apiVersion: v2 appVersion: 0.1.0 - created: "2020-05-27T21:44:37.427234974+08:00" + created: "2020-07-08T15:23:02.537279009+08:00" + dependencies: + - name: logging-operator + repository: https://kubernetes-charts.banzaicloud.com + version: ~3.3.0 + description: | + A Helm chart for Kubernetes which installs the lagoon container and router logs collection system. + digest: 516961903c4c2fc2d8b39b3a9c8f594bdb5db7a1e70c480b89554e64d7303902 + name: lagoon-logging + type: application + urls: + - lagoon-logging-0.6.1.tgz + version: 0.6.1 + - apiVersion: v2 + appVersion: 0.1.0 + created: "2020-07-08T15:23:02.527457528+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -18,7 +33,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2020-05-27T21:44:37.420526997+08:00" + created: "2020-07-08T15:23:02.522593529+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -31,10 +46,22 @@ entries: urls: - lagoon-logging-0.1.0.tgz version: 0.1.0 + lagoon-logs-concentrator: + - apiVersion: v2 + appVersion: 1.16.0 + created: "2020-07-08T15:23:02.538051671+08:00" + description: A Helm chart for Kubernetes which installs the Lagoon logs-concentrator + service. + digest: c66bc7450f61a74cb1e8742c4feb5146c7361e2c04e3171235c1e776ca958327 + name: lagoon-logs-concentrator + type: application + urls: + - lagoon-logs-concentrator-0.2.0.tgz + version: 0.2.0 lagoon-remote: - apiVersion: v2 appVersion: 1.4.0 - created: "2020-05-27T21:44:37.428425112+08:00" + created: "2020-07-08T15:23:02.539156031+08:00" description: A Helm chart to run a lagoon-remote digest: 96bc41bc9985cd6a7fbd85a32affea3bbbabdf4baa0cd829e7e3d33fb975ceeb name: lagoon-remote @@ -44,7 +71,7 @@ entries: version: 0.1.3 - apiVersion: v2 appVersion: 1.4.0 - created: "2020-05-27T21:44:37.427949322+08:00" + created: "2020-07-08T15:23:02.538674677+08:00" description: A Helm chart to run a lagoon-remote digest: 5756a3fbb46a11f2f43fdcadb41d709d90c70208b90fa0257d48dcacc4df3040 name: lagoon-remote @@ -52,4 +79,4 @@ entries: urls: - lagoon-remote-0.1.2.tgz version: 0.1.2 -generated: "2020-05-27T21:44:37.415688985+08:00" +generated: "2020-07-08T15:23:02.517886032+08:00" diff --git a/charts/lagoon-logging-0.6.1.tgz b/charts/lagoon-logging-0.6.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..74d0031b530834585b29659cc2e121576aa98dd9 GIT binary patch literal 112748 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 KxJ2u48g*gqwo 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

Ery+;P5`VA09A(d9#AA+)^yr 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 N&5Md2*d7Yl{{v2xN!Dc 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 N|Nq|N)m{LK000K{qV50y literal 0 HcmV?d00001 From d029196140d00d3067c0552d9b12b3509307890b Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Mon, 13 Jul 2020 22:09:13 +0800 Subject: [PATCH 267/280] Remove accidentally committed values file --- charts/lagoon-logging.values.yaml | 91 ------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 charts/lagoon-logging.values.yaml diff --git a/charts/lagoon-logging.values.yaml b/charts/lagoon-logging.values.yaml deleted file mode 100644 index 2abc7accf5..0000000000 --- a/charts/lagoon-logging.values.yaml +++ /dev/null @@ -1,91 +0,0 @@ -# Default values for lagoon-logging. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -nameOverride: "" -fullnameOverride: "" - -logsDispatcher: - - name: logs-dispatcher - - replicaCount: 2 - - image: - repository: amazeeiolagoon/logs-dispatcher - pullPolicy: Always - # Overrides the image tag whose default is the chart version. - tag: v1-5-0 - - serviceAccount: - # Specifies whether a service account should be created - create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname - # template - # If this value is set, the serviceAccount named must have clusterrole - # view. - name: "" - - podAnnotations: {} - - podSecurityContext: {} - # fsGroup: 2000 - - securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - - resources: {} - # If you want to specify resources, uncomment the following lines, adjust - # them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - - nodeSelector: {} - - tolerations: [] - - affinity: {} - -# Don't collect logs from these namespaces. -# Comment out this field to collect from all namespaces. -excludeNamespaces: -- cattle-prometheus -- kube-system -- syn -- syn-cert-manager -- syn-synsights -- syn-cluster-autoscaler - -# Configure the cluster output buffer. -# This may require tweaking to handle high volumes of logs. -clusterOutputBuffer: - flush_thread_count: 256 - timekey: 1m - timekey_wait: 10s - timekey_use_utc: true - -# Elasticsearch output config. -elasticsearchHostPort: "443" -elasticsearchScheme: https -# The values below must be supplied during installation as they have no sane -# defaults. -elasticsearchAdminPassword: SOp1qe31Bb6jqIjjpPaqNURtMbBIo7Ah -elasticsearchHost: logs-db.ch2.amazee.io -clusterName: amazeeio-de3 - -# chart dependency on logging-operator -logging-operator: - enabled: true - createCustomResource: false From 8fac893ee706fc3be641c97e9cbf81d8dfb844a5 Mon Sep 17 00:00:00 2001 From: Michael Schmid Date: Mon, 13 Jul 2020 14:30:15 -0400 Subject: [PATCH 268/280] update tags to master --- charts/lagoon-logging/values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/lagoon-logging/values.yaml b/charts/lagoon-logging/values.yaml index a60283f1e6..18d44d330f 100644 --- a/charts/lagoon-logging/values.yaml +++ b/charts/lagoon-logging/values.yaml @@ -15,7 +15,7 @@ logsDispatcher: repository: amazeeiolagoon/logs-dispatcher pullPolicy: Always # Overrides the image tag whose default is the chart version. - tag: logging-updates + tag: master serviceAccount: # Specifies whether a service account should be created @@ -70,7 +70,7 @@ logsTeeRouter: repository: amazeeiolagoon/logs-tee pullPolicy: Always # Overrides the image tag whose default is the chart version. - tag: logging-updates + tag: master serviceAccount: # Specifies whether a service account should be created @@ -131,7 +131,7 @@ logsTeeApplication: repository: amazeeiolagoon/logs-tee pullPolicy: Always # Overrides the image tag whose default is the chart version. - tag: logging-updates + tag: master serviceAccount: # Specifies whether a service account should be created From b8733f4d2a67275fc695704c85dc2368f69287b5 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Mon, 13 Jul 2020 17:59:06 -0400 Subject: [PATCH 269/280] Cluster name on Billing Group Admin pages --- services/ui/src/components/BillingGroups/index.js | 8 ++++++-- services/ui/src/lib/query/AllBillingGroups.js | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/services/ui/src/components/BillingGroups/index.js b/services/ui/src/components/BillingGroups/index.js index cf4eb31e4c..bd2e7e6807 100644 --- a/services/ui/src/components/BillingGroups/index.js +++ b/services/ui/src/components/BillingGroups/index.js @@ -13,10 +13,10 @@ const BillingGroups = ({ billingGroups }) => (

{!billingGroups.length &&
No BillingGroups
} { - billingGroups.map(({name, id, currency}) => ( + billingGroups.map(({name, id, currency, projects}) => (
-
{name}
+
{name}
{projects[0].openshift.name}
{currency}
@@ -61,6 +61,10 @@ const BillingGroups = ({ billingGroups }) => ( } } + .cluster { + color: gray; + } + .data-table { background-color: ${color.white}; border: 1px solid ${color.lightestGrey}; diff --git a/services/ui/src/lib/query/AllBillingGroups.js b/services/ui/src/lib/query/AllBillingGroups.js index 25a1513857..173deabb15 100644 --- a/services/ui/src/lib/query/AllBillingGroups.js +++ b/services/ui/src/lib/query/AllBillingGroups.js @@ -6,6 +6,11 @@ export default gql` id, name ... on BillingGroup { currency + projects{ + openshift { + name + } + } } } } From 9fb958bdc41461def35ea313fe0d19adfa248add Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 14 Jul 2020 10:01:44 +0800 Subject: [PATCH 270/280] Roll logs-concentrator statefulset on config change --- charts/lagoon-logs-concentrator/templates/statefulset.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/charts/lagoon-logs-concentrator/templates/statefulset.yaml b/charts/lagoon-logs-concentrator/templates/statefulset.yaml index 8f332f9d39..0f41ac058e 100644 --- a/charts/lagoon-logs-concentrator/templates/statefulset.yaml +++ b/charts/lagoon-logs-concentrator/templates/statefulset.yaml @@ -14,8 +14,11 @@ spec: {{- include "lagoon-logs-concentrator.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + checksum/env-configmap: {{ include (print $.Template.BasePath "/env.configmap.yaml") . | sha256sum }} + checksum/fluent-conf-configmap: {{ include (print $.Template.BasePath "/fluent-conf.configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: From 9b8c186a28d23d7a118d100f24bc3c2ad19c9cb3 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 14 Jul 2020 10:17:02 +0800 Subject: [PATCH 271/280] Roll logs-dispatcher statefulset on config change --- .../templates/logs-dispatcher.statefulset.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/charts/lagoon-logging/templates/logs-dispatcher.statefulset.yaml b/charts/lagoon-logging/templates/logs-dispatcher.statefulset.yaml index 8b0fc15763..cea31ef5ec 100644 --- a/charts/lagoon-logging/templates/logs-dispatcher.statefulset.yaml +++ b/charts/lagoon-logging/templates/logs-dispatcher.statefulset.yaml @@ -12,8 +12,17 @@ spec: {{- include "lagoon-logging.logsDispatcher.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.logsDispatcher.podAnnotations }} annotations: + checksum/secret: {{ include (print $.Template.BasePath "/logs-dispatcher.secret.yaml") . | sha256sum }} + checksum/env-configmap: {{ include (print $.Template.BasePath "/logs-dispatcher.env.configmap.yaml") . | sha256sum }} + checksum/fluent-conf-configmap: {{ include (print $.Template.BasePath "/logs-dispatcher.fluent-conf.configmap.yaml") . | sha256sum }} + {{- if .Values.exportLogs }} + checksum/store-configmap: {{ include (print $.Template.BasePath "/logs-dispatcher.store.configmap.yaml") . | sha256sum }} + {{- end }} + {{- if .Values.lagoonLogs.enabled }} + checksum/source-lagoon-configmap: {{ include (print $.Template.BasePath "/logs-dispatcher.source-lagoon.configmap.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.logsDispatcher.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: From 631dc29e12085ee55b2203f0c08758e6b3ffc79f Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Tue, 14 Jul 2020 10:36:11 +0800 Subject: [PATCH 272/280] Bump lagoon-logging and lagoon-logs-concentrator chart versions --- charts/index.yaml | 40 +++++++++++++++++---- charts/lagoon-logging-0.6.2.tgz | Bin 0 -> 112897 bytes charts/lagoon-logging/Chart.yaml | 2 +- charts/lagoon-logs-concentrator-0.2.1.tgz | Bin 0 -> 5958 bytes charts/lagoon-logs-concentrator/Chart.yaml | 2 +- 5 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 charts/lagoon-logging-0.6.2.tgz create mode 100644 charts/lagoon-logs-concentrator-0.2.1.tgz diff --git a/charts/index.yaml b/charts/index.yaml index 5de985f508..a282187c9d 100644 --- a/charts/index.yaml +++ b/charts/index.yaml @@ -3,7 +3,22 @@ entries: lagoon-logging: - apiVersion: v2 appVersion: 0.1.0 - created: "2020-07-08T15:23:02.537279009+08:00" + created: "2020-07-14T10:33:46.526893465+08:00" + dependencies: + - name: logging-operator + repository: https://kubernetes-charts.banzaicloud.com + version: ~3.3.0 + description: | + A Helm chart for Kubernetes which installs the lagoon container and router logs collection system. + digest: d944b8a7dd5ba927eab5be5df30ebbd0fafb5c45277a550c89c300853b80167a + name: lagoon-logging + type: application + urls: + - lagoon-logging-0.6.2.tgz + version: 0.6.2 + - apiVersion: v2 + appVersion: 0.1.0 + created: "2020-07-14T10:33:46.520370477+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -18,7 +33,7 @@ entries: version: 0.6.1 - apiVersion: v2 appVersion: 0.1.0 - created: "2020-07-08T15:23:02.527457528+08:00" + created: "2020-07-14T10:33:46.514231477+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -33,7 +48,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2020-07-08T15:23:02.522593529+08:00" + created: "2020-07-14T10:33:46.504520947+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -49,7 +64,18 @@ entries: lagoon-logs-concentrator: - apiVersion: v2 appVersion: 1.16.0 - created: "2020-07-08T15:23:02.538051671+08:00" + created: "2020-07-14T10:33:46.528022156+08:00" + description: A Helm chart for Kubernetes which installs the Lagoon logs-concentrator + service. + digest: a4373f224b6435b3c4b4556c99a081c9467edc7748991446a11b1735789bbdcb + name: lagoon-logs-concentrator + type: application + urls: + - lagoon-logs-concentrator-0.2.1.tgz + version: 0.2.1 + - apiVersion: v2 + appVersion: 1.16.0 + created: "2020-07-14T10:33:46.52745819+08:00" description: A Helm chart for Kubernetes which installs the Lagoon logs-concentrator service. digest: c66bc7450f61a74cb1e8742c4feb5146c7361e2c04e3171235c1e776ca958327 @@ -61,7 +87,7 @@ entries: lagoon-remote: - apiVersion: v2 appVersion: 1.4.0 - created: "2020-07-08T15:23:02.539156031+08:00" + created: "2020-07-14T10:33:46.528834847+08:00" description: A Helm chart to run a lagoon-remote digest: 96bc41bc9985cd6a7fbd85a32affea3bbbabdf4baa0cd829e7e3d33fb975ceeb name: lagoon-remote @@ -71,7 +97,7 @@ entries: version: 0.1.3 - apiVersion: v2 appVersion: 1.4.0 - created: "2020-07-08T15:23:02.538674677+08:00" + created: "2020-07-14T10:33:46.528450843+08:00" description: A Helm chart to run a lagoon-remote digest: 5756a3fbb46a11f2f43fdcadb41d709d90c70208b90fa0257d48dcacc4df3040 name: lagoon-remote @@ -79,4 +105,4 @@ entries: urls: - lagoon-remote-0.1.2.tgz version: 0.1.2 -generated: "2020-07-08T15:23:02.517886032+08:00" +generated: "2020-07-14T10:33:46.500236738+08:00" diff --git a/charts/lagoon-logging-0.6.2.tgz b/charts/lagoon-logging-0.6.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..1fdee215a28566a807f198377e144695c3860df3 GIT binary patch literal 112897 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⪚HF#xfY#S*iU=gx2| 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 literal 0 HcmV?d00001 diff --git a/charts/lagoon-logging/Chart.yaml b/charts/lagoon-logging/Chart.yaml index 27055b9510..fb0fe6b321 100644 --- a/charts/lagoon-logging/Chart.yaml +++ b/charts/lagoon-logging/Chart.yaml @@ -12,7 +12,7 @@ type: application # time you make changes to the chart and its templates, including the app # version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.6.1 +version: 0.6.2 # This is the version number of the application being deployed. This version # number should be incremented each time you make changes to the application. diff --git a/charts/lagoon-logs-concentrator-0.2.1.tgz b/charts/lagoon-logs-concentrator-0.2.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..9eba50868b86efb065476dc5620707a0bda167bf GIT binary patch literal 5958 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 Date: Wed, 15 Jul 2020 11:41:43 +0800 Subject: [PATCH 273/280] Keep raw nginx log message We parse the nginx log message into the relevant fields, but some users may want the original log message. --- .../templates/logs-dispatcher.fluent-conf.configmap.yaml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml b/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml index c286e27ae5..0489d27405 100644 --- a/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml +++ b/charts/lagoon-logging/templates/logs-dispatcher.fluent-conf.configmap.yaml @@ -171,16 +171,11 @@ data: - # remove the redundant "log" field if the record was successfully parsed # some container logs have a duplicate message field for some reason, so - # remove that too. + # remove that. @type record_modifier - remove_keys _dummy_,message - - # check for nginxy fields - _dummy_ ${if %w(http_x_forwarded_for referer agent).all?{|key| record[key]}; record.delete("log"); end; nil} - + remove_keys message # From a75bc6cc5cf5f0042ee22695c9add7f5a2f10e4d Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 15 Jul 2020 11:51:47 +0800 Subject: [PATCH 274/280] Bump logging-operator dependency version --- charts/lagoon-logging/Chart.lock | 6 +++--- charts/lagoon-logging/Chart.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/charts/lagoon-logging/Chart.lock b/charts/lagoon-logging/Chart.lock index 2edb5a171d..861f97b626 100644 --- a/charts/lagoon-logging/Chart.lock +++ b/charts/lagoon-logging/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com - version: 3.3.0 -digest: sha256:c87737442c1727efe4bcf2e03a191c0e373f89ba3ac19869b83683e3df871fbc -generated: "2020-06-29T22:02:20.819623111+08:00" + version: 3.4.0 +digest: sha256:d248221846af4df24cb1402d84c6bf8d8db6c26a6348d10345bd5572ed6d8ab7 +generated: "2020-07-15T11:52:39.381243481+08:00" diff --git a/charts/lagoon-logging/Chart.yaml b/charts/lagoon-logging/Chart.yaml index fb0fe6b321..e7cc16f771 100644 --- a/charts/lagoon-logging/Chart.yaml +++ b/charts/lagoon-logging/Chart.yaml @@ -23,4 +23,4 @@ appVersion: 0.1.0 dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com - version: ~3.3.0 + version: ~3.4.0 From 90d3b9a4429ecb98a066f1aa8b77480b23c542dc Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 15 Jul 2020 13:23:36 +0800 Subject: [PATCH 275/280] Bump lagoon-logging chart version --- charts/index.yaml | 33 ++++++++++++++++++++++--------- charts/lagoon-logging-0.6.3.tgz | Bin 0 -> 114078 bytes charts/lagoon-logging/Chart.yaml | 2 +- 3 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 charts/lagoon-logging-0.6.3.tgz diff --git a/charts/index.yaml b/charts/index.yaml index a282187c9d..f49ac5c23d 100644 --- a/charts/index.yaml +++ b/charts/index.yaml @@ -3,7 +3,22 @@ entries: lagoon-logging: - apiVersion: v2 appVersion: 0.1.0 - created: "2020-07-14T10:33:46.526893465+08:00" + created: "2020-07-15T13:20:44.801012001+08:00" + dependencies: + - name: logging-operator + repository: https://kubernetes-charts.banzaicloud.com + version: ~3.4.0 + description: | + A Helm chart for Kubernetes which installs the lagoon container and router logs collection system. + digest: e06440b9199bc69f46c2fb66a20d3ed153c2f0fe749c97a40b2bf137f97c7205 + name: lagoon-logging + type: application + urls: + - lagoon-logging-0.6.3.tgz + version: 0.6.3 + - apiVersion: v2 + appVersion: 0.1.0 + created: "2020-07-15T13:20:44.793677497+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -18,7 +33,7 @@ entries: version: 0.6.2 - apiVersion: v2 appVersion: 0.1.0 - created: "2020-07-14T10:33:46.520370477+08:00" + created: "2020-07-15T13:20:44.786136365+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -33,7 +48,7 @@ entries: version: 0.6.1 - apiVersion: v2 appVersion: 0.1.0 - created: "2020-07-14T10:33:46.514231477+08:00" + created: "2020-07-15T13:20:44.777543065+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -48,7 +63,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2020-07-14T10:33:46.504520947+08:00" + created: "2020-07-15T13:20:44.769678229+08:00" dependencies: - name: logging-operator repository: https://kubernetes-charts.banzaicloud.com @@ -64,7 +79,7 @@ entries: lagoon-logs-concentrator: - apiVersion: v2 appVersion: 1.16.0 - created: "2020-07-14T10:33:46.528022156+08:00" + created: "2020-07-15T13:20:44.802816825+08:00" description: A Helm chart for Kubernetes which installs the Lagoon logs-concentrator service. digest: a4373f224b6435b3c4b4556c99a081c9467edc7748991446a11b1735789bbdcb @@ -75,7 +90,7 @@ entries: version: 0.2.1 - apiVersion: v2 appVersion: 1.16.0 - created: "2020-07-14T10:33:46.52745819+08:00" + created: "2020-07-15T13:20:44.802021602+08:00" description: A Helm chart for Kubernetes which installs the Lagoon logs-concentrator service. digest: c66bc7450f61a74cb1e8742c4feb5146c7361e2c04e3171235c1e776ca958327 @@ -87,7 +102,7 @@ entries: lagoon-remote: - apiVersion: v2 appVersion: 1.4.0 - created: "2020-07-14T10:33:46.528834847+08:00" + created: "2020-07-15T13:20:44.803852312+08:00" description: A Helm chart to run a lagoon-remote digest: 96bc41bc9985cd6a7fbd85a32affea3bbbabdf4baa0cd829e7e3d33fb975ceeb name: lagoon-remote @@ -97,7 +112,7 @@ entries: version: 0.1.3 - apiVersion: v2 appVersion: 1.4.0 - created: "2020-07-14T10:33:46.528450843+08:00" + created: "2020-07-15T13:20:44.803424311+08:00" description: A Helm chart to run a lagoon-remote digest: 5756a3fbb46a11f2f43fdcadb41d709d90c70208b90fa0257d48dcacc4df3040 name: lagoon-remote @@ -105,4 +120,4 @@ entries: urls: - lagoon-remote-0.1.2.tgz version: 0.1.2 -generated: "2020-07-14T10:33:46.500236738+08:00" +generated: "2020-07-15T13:20:44.761029818+08:00" diff --git a/charts/lagoon-logging-0.6.3.tgz b/charts/lagoon-logging-0.6.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..b23e145f717ddf96b19f7e152480362380fb1faf GIT binary patch literal 114078 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 Vm!wa0AfV4r`$)S@ARt7b{}0w4e^CGc literal 0 HcmV?d00001 diff --git a/charts/lagoon-logging/Chart.yaml b/charts/lagoon-logging/Chart.yaml index e7cc16f771..7e2d3e6760 100644 --- a/charts/lagoon-logging/Chart.yaml +++ b/charts/lagoon-logging/Chart.yaml @@ -12,7 +12,7 @@ type: application # time you make changes to the chart and its templates, including the app # version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.6.2 +version: 0.6.3 # This is the version number of the application being deployed. This version # number should be incremented each time you make changes to the application. From 32b007239d7e5ede49aaf70605cdd4e95a818cfc Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Wed, 15 Jul 2020 13:52:35 +0800 Subject: [PATCH 276/280] Add label-namespaces.sh script --- helpers/label-namespaces.sh | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100755 helpers/label-namespaces.sh diff --git a/helpers/label-namespaces.sh b/helpers/label-namespaces.sh new file mode 100755 index 0000000000..e9f1f93c8e --- /dev/null +++ b/helpers/label-namespaces.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +## +# Label all namespaces with lagoon info +# +# Old environments weren't labelled the way that Lagoon expects. This script +# can be run against a cluster to add the missing labels. + +set -euo pipefail +#set -x + +# Loop through all oc projects. +while read -r project ; do + + # Check if lagoon-env configmap exists. + if oc get configmap -n "$project" lagoon-env >/dev/null 2>&1; then + + echo "################################################" + echo "Annotating project: $project..." + echo "################################################" + + LAGOON_PROJECT=$(oc get configmaps -n "$project" lagoon-env -o yaml | awk '/LAGOON_PROJECT:/ { print $2 }') + LAGOON_ENVIRONMENT_TYPE=$(oc get configmaps -n "$project" lagoon-env -o yaml | awk '/LAGOON_ENVIRONMENT_TYPE:/ { print $2 }') + LAGOON_GIT_SAFE_BRANCH=$(oc get configmaps -n "$project" lagoon-env -o yaml | awk '/LAGOON_GIT_SAFE_BRANCH:/ { print $2 }') + MARIADB_DATABASE=$(oc get configmaps -n "$project" lagoon-env -o yaml | awk '/MARIADB_DATABASE:/ { print $2 }') + MARIADB_USERNAME=$(oc get configmaps -n "$project" lagoon-env -o yaml | awk '/MARIADB_USERNAME:/ { print $2 }') + + oc label namespace "$project" "lagoon.sh/project=$LAGOON_PROJECT" --overwrite + oc label namespace "$project" "lagoon.sh/environmentType=$LAGOON_ENVIRONMENT_TYPE" --overwrite + oc label namespace "$project" "lagoon.sh/environment=$LAGOON_GIT_SAFE_BRANCH" --overwrite + oc label namespace "$project" "lagoon.sh/mariadb-schema=$MARIADB_DATABASE" --overwrite + oc label namespace "$project" "lagoon.sh/mariadb-username=$MARIADB_USERNAME" --overwrite + else + + echo "No lagoon-env configmap found for $project" + + fi + +done < <(oc get ns -l '!lagoon.sh/project' | sed '1d' | awk '{print $1}') From 88cd9b16f1330b8bb8f99941634f8a7484fe4eb8 Mon Sep 17 00:00:00 2001 From: Vincenzo De Naro Papa Date: Wed, 15 Jul 2020 18:01:35 +0200 Subject: [PATCH 277/280] Changed logic how gather projectname Set notification channels as an array --- helpers/check_acme_routes.sh | 40 ++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/helpers/check_acme_routes.sh b/helpers/check_acme_routes.sh index 788d5cb568..c4dc864235 100755 --- a/helpers/check_acme_routes.sh +++ b/helpers/check_acme_routes.sh @@ -119,7 +119,15 @@ function create_routes_array() { # Get the list of namespaces with broker routes, according to REGEX for namespace in $(oc get routes --all-namespaces|grep exposer|awk '{print $1}'|sort -u|grep -E "$REGEX") do - PROJECTNAME=$(oc get project "$namespace" -o json|grep display-name|awk -F'[][]' '{print $2}'|tr "_" "-") + # Raw JSON Openshift project output + PROJECTJSON="$(oc get project "$namespace" -o json)" + + # Gather project name based on a label or an annotation + if [ $(echo $PROJECTJSON |grep -q 'lagoon.sh/project'; echo $?) -eq 0 ]; then + PROJECTNAME=$(echo "${PROJECTJSON}" | grep 'lagoon.sh/project' | awk -F'"' '{print $4}') + else + PROJECTNAME=$(echo "${PROJECTJSON}" |grep display-name|awk -F'[][]' '{print $2}'|tr "_" "-") + fi # Get the list of broken unique routes for each namespace for routelist in $(oc get -n "$namespace" route|grep exposer|awk -vNAMESPACE="$namespace" -vPROJECTNAME="$PROJECTNAME" '{print $1";"$2";"NAMESPACE";"PROJECTNAME}'|sort -u -k2 -t ";") @@ -221,21 +229,27 @@ function notify_customer() { echo "No notification set" return 0 fi - NOTIFICATION_DATA=$(lagoon list $NOTIFICATION -p "$1" --no-header|head -n1|awk '{print $3";"$4}') - CHANNEL=$(echo "$NOTIFICATION_DATA"|cut -f1 -d ";") - WEBHOOK=$(echo "$NOTIFICATION_DATA"|cut -f2 -d ";") + MESSAGE="Your $ROUTE_HOSTNAME route is configured in the \`.lagoon.yml\` file to issue an TLS certificate from Lets Encrypt. Unfortunately Lagoon is unable to issue a certificate as $DNS_ERROR.\nTo be issued correctly, the DNS records for $ROUTE_HOSTNAME should point to $CLUSTER_HOSTNAME with an CNAME record (preferred) or to ${CLUSTER_IPS[*]} via an A record (also possible but not preferred).\nIf you don't need the SSL certificate or you are using a CDN that provides you with an TLS certificate, please update your .lagoon.yml file by setting the tls-acme parameter to false for $ROUTE_HOSTNAME, as described here: https://lagoon.readthedocs.io/en/latest/using_lagoon/lagoon_yml/#ssl-configuration-tls-acme.\nWe have now administratively disabled the issuing of Lets Encrypt certificate for $ROUTE_HOSTNAME in order to protect the cluster, this will be reset during the next deployment, therefore we suggest to resolve this issue as soon as possible. Feel free to reach out to us for further information.\nThanks you.\namazee.io team" - # json Payload - PAYLOAD="\"channel\": \"$CHANNEL\", \"text\": \"${MESSAGE}\"" + NOTIFICATION_DATA=($(lagoon list $NOTIFICATION -p "$1" --no-header|awk '{print $3";"$4}')) + for notification in ${NOTIFICATION_DATA[@]} + do + CHANNEL=$(echo "$notification"|cut -f1 -d ";") + WEBHOOK=$(echo "$notification"|cut -f2 -d ";") + + # json Payload + PAYLOAD="\"channel\": \"$CHANNEL\", \"text\": \"${MESSAGE}\"" - echo -e "Sending notification into ${CHANNEL}" - # Execute curl to send message into the channel - if [[ $DRYRUN = true ]]; then - echo "DRYRUN Sending notification on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data '{'"$PAYLOAD"'}' "$WEBHOOK"" - else - curl -X POST -H 'Content-type: application/json' --data '{'"${PAYLOAD}"'}' ${WEBHOOK} - fi + echo -e "Sending notification into ${CHANNEL}" + + # Execute curl to send message into the channel + if [[ $DRYRUN = true ]]; then + echo "DRYRUN Sending notification on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data '{'"$PAYLOAD"'}' "$WEBHOOK"" + else + curl -X POST -H 'Content-type: application/json' --data '{'"${PAYLOAD}"'}' ${WEBHOOK} + fi + done } # Main function From 0e3e058c0d928d7808b9a5ca814dccc5509e3c49 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 16 Jul 2020 23:20:14 -0500 Subject: [PATCH 278/280] Don't cache keycloak authz denials in redis It's possible the denial is due to a network error or some other temporary issue --- services/api/src/util/auth.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index ce6482abaf..c11b93a50e 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -284,11 +284,12 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) } requestCache.set(cacheKey, false); - try { - await saveRedisCache(resourceScope, 0); - } catch (err) { - logger.warn(`Could not save authz cache: ${err.message}`); - } + // TODO: Re-enable when we can distinguish between error and access denied + // try { + // await saveRedisCache(resourceScope, 0); + // } catch (err) { + // logger.warn(`Could not save authz cache: ${err.message}`); + // } throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); }; }; From aba3d26c13c8b7165d6efdd62f25139272894228 Mon Sep 17 00:00:00 2001 From: Justin Winter Date: Fri, 17 Jul 2020 16:38:01 -0400 Subject: [PATCH 279/280] Additional tests and formatting for POLYSITES greater than 10 --- .../billing/billingCalculations.test.ts | 219 +++++++++++++++++- .../components/BillingGroupInvoice/index.js | 3 +- 2 files changed, 219 insertions(+), 3 deletions(-) diff --git a/services/api/src/resources/billing/billingCalculations.test.ts b/services/api/src/resources/billing/billingCalculations.test.ts index eb8d1ef829..5d0b4c6c79 100644 --- a/services/api/src/resources/billing/billingCalculations.test.ts +++ b/services/api/src/resources/billing/billingCalculations.test.ts @@ -440,6 +440,223 @@ const mockData: IMockDataType = { } ] }, + { + name: 'RC', + expectations: { + hits: 87.78, + storage: 7.69, + prod: 30.02, + dev: 20.02 + }, + currency: CURRENCIES.CHF, + projects: [ + { + name: "srf_ch", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 59937, + storageDays: 454.666423, + prodHours: 720, + devHours: 720, + }, + { + name: "1cms-zh", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 3600, + storageDays: 56.651285, + prodHours: 720, + devHours: 1440, + }, + { + name: "1cms-bl", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 720, + storageDays: 0.005496, + prodHours: 720, + devHours: 720, + }, + { + name: "1cms-lu", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.0049640000000000005, + prodHours: 720, + devHours: 720, + }, + { + name: "1cms-zg", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.0016020000000000001, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-sg", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.0016020000000000001, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-tg", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.0049640000000000005, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-uw", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.001588, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-ag", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.003363, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-ge", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.004966, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-ju", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.004966, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-ti", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.0022919999999999998, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-gr", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.004966, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-hs", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.001588, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-lt", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.004966, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-so", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.004067, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-smsv", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 360921, + storageDays: 19.414938, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-sh", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.001588, + prodHours: 720, + devHours: 720, + + }, + { + name: "1cms-sz", + availability: "POLYSITE", + month: 6, + year: 2020, + hits: 0, + storageDays: 0.004951, + prodHours: 720, + devHours: 720, + + } + ] + } ], }; @@ -1232,7 +1449,7 @@ describe('Billing Calculations #only-billing-calculations', () => { const lastMonth = moment().subtract(1, 'M').format('YYYY-MM').toString(); const nextMonth = moment().add(1, 'M').format('YYYY-MM').toString(); - const nextYear = moment().add(1, "year").format('YYYY-MM').toString(); + const nextYear = moment().add(1, 'year').format('YYYY-MM').toString(); const currMonth = moment().format('YYYY-MM').toString(); // Act diff --git a/services/ui/src/components/BillingGroupInvoice/index.js b/services/ui/src/components/BillingGroupInvoice/index.js index e81de14156..fb756b9aea 100644 --- a/services/ui/src/components/BillingGroupInvoice/index.js +++ b/services/ui/src/components/BillingGroupInvoice/index.js @@ -34,7 +34,6 @@ const Invoice = ({ cost, language }) => { setLang(value); } - return (

}
-
{cost.environmentCostDescription.prod.quantity.toFixed(2).toLocaleString()}
+
{ cost.availability === 'POLYSITE' && cost.projects.length > 10 ? `${Math.max(Math.round(cost.projects.length / 10), 1)} x ` : '' }{cost.environmentCostDescription.prod.quantity.toFixed(2).toLocaleString()}
{cost.environmentCostDescription.prod.unitPrice}
{cost.environmentCost.prod.toFixed(2)}

From 92757713b872290bfc6527fc821aa893a0f7ee86 Mon Sep 17 00:00:00 2001 From: Tim Clifford Date: Wed, 22 Jul 2020 10:13:06 +0100 Subject: [PATCH 280/280] Merge conflict with yarn lock after ui changes --- yarn.lock | 82 ++++++++----------------------------------------------- 1 file changed, 12 insertions(+), 70 deletions(-) diff --git a/yarn.lock b/yarn.lock index c06a5c7105..087dce9484 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1645,6 +1645,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.8.7": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" + integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@7.0.0-beta.44": version "7.0.0-beta.44" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" @@ -1879,11 +1886,6 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" - integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ== - "@emotion/is-prop-valid@0.8.5": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.5.tgz#2dda0791f0eafa12b7a0a5b39858405cc7bde983" @@ -1908,11 +1910,6 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== -"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" - integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ== - "@emotion/serialize@^0.11.12", "@emotion/serialize@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.14.tgz#56a6d8d04d837cc5b0126788b2134c51353c6488" @@ -1935,17 +1932,6 @@ "@emotion/utils" "0.11.3" csstype "^2.5.7" -"@emotion/serialize@^0.9.1": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" - integrity sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ== - dependencies: - "@emotion/hash" "0.8.0" - "@emotion/memoize" "0.7.4" - "@emotion/unitless" "0.7.5" - "@emotion/utils" "0.11.3" - csstype "^2.5.7" - "@emotion/sheet@0.9.3": version "0.9.3" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a" @@ -2002,11 +1988,6 @@ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== -"@emotion/stylis@^0.7.0": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" - integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ== - "@emotion/unitless@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.4.tgz#a87b4b04e5ae14a88d48ebef15015f6b7d1f5677" @@ -2017,11 +1998,6 @@ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== -"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": - version "0.6.7" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" - integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg== - "@emotion/utils@0.11.2": version "0.11.2" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183" @@ -2032,11 +2008,6 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== -"@emotion/utils@^0.8.2": - version "0.8.2" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" - integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== - "@emotion/weak-memoize@0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" @@ -3407,6 +3378,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.12.tgz#9c72e865380a7dc99999ea0ef20fc9635b503d20" integrity sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw== +"@types/npmlog@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.2.tgz#d070fe6a6b78755d1092a3dc492d34c3d8f871c4" + integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA== + "@types/p-cancelable@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-1.0.1.tgz#4f0ce8aa3ee0007c2768b9b3e6e22af20a6eecbd" @@ -4998,22 +4974,6 @@ babel-plugin-emotion@^10.0.27: find-root "^1.1.0" source-map "^0.5.7" -babel-plugin-emotion@^9.2.11: - version "9.2.11" - resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" - integrity sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@emotion/hash" "0.8.0" - "@emotion/memoize" "0.7.4" - "@emotion/serialize" "^0.11.16" - babel-plugin-macros "^2.0.0" - babel-plugin-syntax-jsx "^6.18.0" - convert-source-map "^1.5.0" - escape-string-regexp "^1.0.5" - find-root "^1.1.0" - source-map "^0.5.7" - babel-plugin-extract-import-names@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.5.1.tgz#79fb8550e3e0a9e8654f9461ccade56c9a669a74" @@ -14316,13 +14276,6 @@ raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== -raf@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - ramda@^0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" @@ -14683,17 +14636,6 @@ react-redux@^7.1.1: prop-types "^15.7.2" react-is "^16.9.0" -react-select@^2.1.1: - version "2.4.4" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.4.4.tgz#ba72468ef1060c7d46fbb862b0748f96491f1f73" - integrity sha512-C4QPLgy9h42J/KkdrpVxNmkY6p4lb49fsrbDk/hRcZpX7JvZPNb6mGj+c5SzyEtBv1DmQ9oPH4NmhAFvCrg8Jw== - dependencies: - "@babel/runtime" "^7.5.5" - hoist-non-react-statics "^3.3.0" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.9.0" - react-select@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.0.tgz#ab098720b2e9fe275047c993f0d0caf5ded17c27"