diff --git a/Makefile b/Makefile index 2d206067d3..7da9c940f6 100644 --- a/Makefile +++ b/Makefile @@ -449,7 +449,8 @@ services := api \ harbor-redis \ harborregistry \ harborregistryctl \ - harbor-trivy + harbor-trivy \ + api-redis service-images += $(services) @@ -482,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 @@ -1028,31 +1030,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)')" --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 -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)')" --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 -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)')" --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 +1076,27 @@ 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)')" --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 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 -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 @@ -1140,3 +1142,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 diff --git a/docker-compose.yaml b/docker-compose.yaml index db90b7d98b..5b568c5df7 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-7-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-7-1 diff --git a/docs/using_lagoon/active_standby.md b/docs/using_lagoon/active_standby.md index ea479a0566..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 current environment that is 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 { @@ -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. @@ -52,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( @@ -94,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. @@ -112,4 +119,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`. For more information on the differences between environment types, read the [documentation for `environment types`](environment_types.md#environment-types) + +``` +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 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 . 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) | 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: diff --git a/docs/using_lagoon/lagoon_yml.md b/docs/using_lagoon/lagoon_yml.md index 92aee984f0..8c91ec1cc4 100644 --- a/docs/using_lagoon/lagoon_yml.md +++ b/docs/using_lagoon/lagoon_yml.md @@ -126,6 +126,17 @@ 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.allowPullrequests` + +This allows pull request to get autogenerated routes when route autogeneration is disabled. + +``` +routes: + autogenerate: + enabled: false + allowPullrequests: true +``` + ### `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: @@ -272,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/helpers/check_acme_routes.sh b/helpers/check_acme_routes.sh index c37bca77eb..788d5cb568 100755 --- a/helpers/check_acme_routes.sh +++ b/helpers/check_acme_routes.sh @@ -7,7 +7,14 @@ 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 + # Some variables + # Cluster full hostname and API hostname CLUSTER_HOSTNAME="${CLUSTER_HOSTNAME:-""}" CLUSTER_API_HOSTNAME="${CLUSTER_API_HOSTNAME:-"$CLUSTER_HOSTNAME"}" @@ -18,9 +25,16 @@ COMMAND=${1:-"help"} # Set DRYRUN variable to true to run in dry-run mode DRYRUN="${DRYRUN:-"false"}" + # Set a REGEX variable to filter the execution of the script REGEX=${REGEX:-".*"} +# 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 function usage() { echo -e "The available commands are: @@ -105,7 +119,8 @@ 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 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 @@ -135,7 +150,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 # Check if the route matches the Cluster's IP(s) if echo "$ROUTE_HOSTNAME_IP" | grep -E -q -v "${CLUSTER_IPS[*]}"; then @@ -147,38 +166,46 @@ 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" - # 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}') - #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]} - if [[ $DRYRUN = true ]]; then - echo -e "DRYRUN oc delete -n $ROUTE_NAMESPACE route $OCROUTE_NAME" - else - oc delete -n "$ROUTE_NAMESPACE" route "$OCROUTE_NAME" - fi - done + if [[ "$NOTIFYONLY" = "true" ]]; then + notify_customer "$ROUTE_PROJECTNAME" + else + # 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" + + done } # 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)" } @@ -197,21 +224,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\"}'" - echo "Sending message $JSON to $CHANNEL" + # 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 on \"$NOTIFICATION\" curl -X POST -H 'Content-type: application/json' --data "$JSON" "$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 "$JSON" "$WEBHOOK" + curl -X POST -H 'Content-type: application/json' --data '{'"${PAYLOAD}"'}' ${WEBHOOK} fi } - # Main function function main() { @@ -258,4 +284,4 @@ function main() { } initial_checks "$COMMAND" -main "$COMMAND" \ No newline at end of file +main "$COMMAND" 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/kubectl-build-deploy-dind/build-deploy-docker-compose.sh b/images/kubectl-build-deploy-dind/build-deploy-docker-compose.sh index 34732be852..c57454f145 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 @@ -369,6 +369,15 @@ 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.allowPullrequests $ROUTES_AUTOGENERATE_ENABLED) +if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then + ROUTES_AUTOGENERATE_ENABLED=true +fi +## 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 touch /kubectl-build-deploy/values.yaml @@ -916,10 +925,13 @@ 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}\"))") + 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 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 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..fadb00cda6 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 @@ -429,7 +429,15 @@ 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.allowPullrequests $ROUTES_AUTOGENERATE_ENABLED) +if [[ "$TYPE" == "pullrequest" && "$ROUTES_AUTOGENERATE_ALLOW_PRS" == "true" ]]; then + ROUTES_AUTOGENERATE_ENABLED=true +fi +## 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 for SERVICE_TYPES_ENTRY in "${SERVICE_TYPES[@]}" do 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} \ 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 28eec074af..6d8f6d4765 100644 --- a/images/php/fpm/Dockerfile +++ b/images/php/fpm/Dockerfile @@ -48,7 +48,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/ 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 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 "$@" 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/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index e02d9e3668..a06adbd83a 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 ? 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) { 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(1), + channel.prefetch(taskMonitorPrefetch), channel.consume( `lagoon-tasks-monitor:${taskMonitorQueueName}`, onMessage, 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 diff --git a/services/api/package.json b/services/api/package.json index 5743e8a86e..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", @@ -69,6 +70,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/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index bb266f7445..8e1584bf8a 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'); @@ -31,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); @@ -83,8 +83,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,8 +138,8 @@ const apolloServer = new ApolloServer({ keycloakGrant: req.kauth ? req.kauth.grant : null, requestCache, models: { - UserModel: User.User({ keycloakAdminClient }), - GroupModel: Group.Group({ keycloakAdminClient, sqlClient }), + UserModel: User.User({ keycloakAdminClient, redisClient }), + GroupModel: Group.Group({ keycloakAdminClient, redisClient }), BillingModel: BillingModel.BillingModel({ keycloakAdminClient, sqlClient @@ -210,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( diff --git a/services/api/src/clients/redisClient.ts b/services/api/src/clients/redisClient.ts new file mode 100644 index 0000000000..d15dcb02bb --- /dev/null +++ b/services/api/src/clients/redisClient.ts @@ -0,0 +1,78 @@ +import * as R from 'ramda'; +import redis, { ClientOpts } from 'redis'; +import { promisify } from 'util'; + +const { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } = process.env; + +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) { + console.error(error); +}); + +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; + 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 getRedisCache = async (resourceScope: IUserResourceScope) => { + const redisHash = await hgetall( + `cache:authz:${resourceScope.currentUserId}` + ); + const key = hashKey(resourceScope); + + return R.prop(key, redisHash); +}; + +export const saveRedisCache = async ( + resourceScope: IUserResourceScope, + value: number | string +) => { + const key = hashKey(resourceScope); + await redisClient.hmset( + `cache:authz:${resourceScope.currentUserId}`, + key, + value + ); +}; + +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, + getProjectGroupsCache, + saveProjectGroupsCache +}; 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 d184e63865..19fbc5d6aa 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 { @@ -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', @@ -98,7 +107,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[], @@ -121,11 +130,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), }); } @@ -216,12 +224,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 allGroups = await loadAllGroups(); - - const filteredGroups = R.filter((group: Group) => + ): Group[] | BillingGroup[] => + R.filter((group: Group) => R.pipe( R.toPairs, R.reduce((isMatch: boolean, attribute: [string, string[]]): boolean => { @@ -231,16 +238,33 @@ export const Group = (clients) => { name: attribute[0], value: attribute[1], }, - group, ); } return isMatch; }, false), )(group.attributes), - )(allGroups); + )(groups); + + const loadGroupsByAttribute = async ( + filterFn: AttributeFilterFn, + ): Promise => { + const keycloakGroups = await keycloakAdminClient.groups.find(); - return filteredGroups; + 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); + + return groups; }; const loadGroupsByProjectId = async ( @@ -257,20 +281,67 @@ 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 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, ): 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 @@ -288,13 +359,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) { @@ -451,6 +516,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) { @@ -460,6 +529,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 ( @@ -501,6 +578,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); }; @@ -523,6 +606,12 @@ 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) { + logger.warn(`Error deleting user cache ${user.id}: ${err}`); + } } return await loadGroupById(group.id); @@ -558,6 +647,23 @@ export const Group = (clients) => { throw new Error( `Error setting projects for group ${group.name}: ${err.message}`, ); + }; + + // 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) { + logger.warn(`Error deleting project groups cache: ${err.message}`); } }; @@ -590,6 +696,23 @@ export const Group = (clients) => { throw new Error( `Error setting projects for group ${group.name}: ${err.message}`, ); + }; + + // 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) { + logger.warn(`Error deleting project groups cache: ${err.message}`); } }; diff --git a/services/api/src/models/user.ts b/services/api/src/models/user.ts index 659fede608..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'; @@ -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({ @@ -351,6 +352,11 @@ export const User = (clients): UserModel => { throw new Error(`Error deleting user ${id}: ${err}`); } } + try { + await redisClient.deleteRedisUserCache(id) + } catch(err) { + logger.error(`Error deleting user cache ${id}: ${err}`); + } }; return { @@ -363,6 +369,6 @@ export const User = (clients): UserModel => { getUserRolesForProject, addUser, updateUser, - deleteUser, + deleteUser } }; 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 diff --git a/services/api/src/resources/user/resolvers.ts b/services/api/src/resources/user/resolvers.ts index dcc4619b58..bc40c3436b 100644 --- a/services/api/src/resources/user/resolvers.ts +++ b/services/api/src/resources/user/resolvers.ts @@ -97,7 +97,6 @@ export const deleteUser: ResolverFn = async ( }); await models.UserModel.deleteUser(user.id); - // TODO remove user ssh keys return 'success'; diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 0406935fa9..ce6482abaf 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -1,11 +1,11 @@ import * as R from 'ramda'; +import { getRedisCache, 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'; - const { JWTSECRET, JWTAUDIENCE } = process.env; interface ILegacyToken { @@ -105,7 +105,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. @@ -113,10 +112,31 @@ 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 the redis cache before doing a full keycloak lookup. + const resourceScope = {resource, scope, currentUserId, ...attributes }; + let redisCacheResult: number; + try { + 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(); let claims: { @@ -249,6 +269,12 @@ export const keycloakHasPermission = (grant, requestCache, keycloakAdminClient) if (newGrant.access_token.hasPermission(resource, scope)) { requestCache.set(cacheKey, true); + try { + await saveRedisCache(resourceScope, 1); + } catch (err) { + logger.warn(`Could not save authz cache: ${err.message}`); + } + return; } } catch (err) { @@ -258,6 +284,11 @@ 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}`); + } throw new KeycloakUnauthorizedError(`Unauthorized: You don't have permission to "${scope}" on "${resource}".`); }; }; 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 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" diff --git a/services/kubernetesbuilddeploy/src/index.ts b/services/kubernetesbuilddeploy/src/index.ts index 86bfa82c80..fe87af56b9 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}`) @@ -358,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 313938950f..f7bfd715d9 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}`) @@ -356,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}` 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; diff --git a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts index e1709c89cf..ef068f6913 100644 --- a/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts +++ b/services/webhooks2tasks/src/handlers/problems/harborScanningCompleted.ts @@ -9,8 +9,9 @@ import uuid4 from 'uuid4'; import { getProjectByName, - getEnvironmentByName, getProblemHarborScanMatches, + getEnvironmentByOpenshiftProjectName, + getOpenShiftInfoForProject, } from '@lagoon/commons/dist/api'; const HARBOR_WEBHOOK_SUCCESSFUL_SCAN = "Success"; @@ -55,14 +56,26 @@ const DEFAULT_REPO_DETAILS_MATCHER = { return; } - let vulnerabilities = await getVulnerabilitiesFromHarbor(harborScanId); + let vulnerabilities = []; + vulnerabilities = await getVulnerabilitiesFromHarbor(harborScanId); let { id: lagoonProjectId } = await getProjectByName(lagoonProjectName); - let { environmentByName: environmentDetails } = await getEnvironmentByName( - lagoonEnvironmentName, - lagoonProjectId - ); + 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) + let messageBody = { lagoonProjectId, diff --git a/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts b/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts index 61404cbb88..f0899a0c66 100644 --- a/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts +++ b/services/webhooks2tasks/src/handlers/problems/processDrutinyResults.ts @@ -11,9 +11,11 @@ const DRUTINY_SERVICE_NAME = 'cli'; const DRUTINY_PACKAGE_NAME = '' import { getProjectByName, - getEnvironmentByName, + getEnvironmentByOpenshiftProjectName, + getOpenShiftInfoForProject, } from '@lagoon/commons/dist/api'; import { generateProblemsWebhookEventName } from "./webhookHelpers"; +import * as R from 'ramda'; const ERROR_STATES = ["error", "failure"]; const SEVERITY_LEVELS = [ @@ -58,9 +60,20 @@ export async function processDrutinyResultset( lagoonProjectName ); - const { - environmentByName: environmentDetails, - } = await getEnvironmentByName(lagoonEnvironmentName, lagoonProjectId); + 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) const lagoonEnvironmentId = environmentDetails.id; const lagoonServiceName = DRUTINY_SERVICE_NAME; diff --git a/yarn.lock b/yarn.lock index 005d3a546b..d9e670c42e 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" @@ -3193,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" @@ -4676,6 +4802,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 +6504,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 +7109,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 +11574,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 +13811,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 +14106,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 +14194,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 +14467,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 +14770,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 +14805,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 +14854,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 +16583,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 +17303,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"