From 5a814d1d883900fdaa5187e5a55de0b75e2d692d Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 14 May 2021 13:19:14 -0700 Subject: [PATCH 001/236] bridge-net instead of bridge (#79) --- etc/env/example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/env/example.env b/etc/env/example.env index b1c923e51..65204ffe4 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -26,7 +26,7 @@ VENV_PIP=20.2.4 # docker # options are [bridge, bridge-net, ingress, traefik-net] -DOCKER_NET=bridge +DOCKER_NET=bridge-net DOCKER_BRIDGE_IP=10.10.1.0/24 DOCKER_GWBRIDGE_IP=10.10.2.0/24 # options are [compose, swarm, kubernetes] From 8753c896f38547966dc1d3fc4e10a7e3f1cc18e7 Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Tue, 18 May 2021 20:46:48 -0400 Subject: [PATCH 002/236] bump toil to 5.3.1, following 5.3.x release (#80) --- etc/env/example.env | 4 ++-- lib/toil/toil-docker | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 65204ffe4..e70a7f593 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -160,8 +160,8 @@ WES_OPT+=--opt=extra=--metrics #--- # toil executor -TOIL_VERSION=5.2.1a1 -TOIL_BUILD_HASH=b8b60d4c5931f9ed890ceac5080de466e841b76e-py3.7 +TOIL_VERSION=5.3.1a1 +TOIL_BUILD_HASH=dd4d4725f51c0e59a58a4ebffe143410dee4722e-py3.7 TOIL_MODULES=toil toil-grafana toil-mtail toil-prometheus TOIL_IP=0.0.0.0 TOIL_PORT=5050 diff --git a/lib/toil/toil-docker b/lib/toil/toil-docker index b8b60d4c5..dd4d4725f 160000 --- a/lib/toil/toil-docker +++ b/lib/toil/toil-docker @@ -1 +1 @@ -Subproject commit b8b60d4c5931f9ed890ceac5080de466e841b76e +Subproject commit dd4d4725f51c0e59a58a4ebffe143410dee4722e From f4cb67718695c377bf7a70cdc5e09c74fca6ae89 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 12 Jul 2021 11:08:44 -0700 Subject: [PATCH 003/236] Initial jenkins setup (#82) * set a default value for WORKING_DIR * activate conda on login * working dir * oops * Update example.env * initial commit for setup_jenkins * test * disable toil-docker for now * update setup_jenkins * try editing env vars * try editing env vars * try editing env vars * don't push toil modules either * working_dir is just the wd * change location for progress.txt * try using conda activate as the test * touch logfile * touch logfile * touch logfile * move log file --- Makefile | 53 +++++++++++++++++++++++---------------------- provision.sh | 27 +++++++++++++---------- setup_containers.sh | 22 ++++++++++--------- setup_jenkins.sh | 19 ++++++++++++++++ 4 files changed, 73 insertions(+), 48 deletions(-) create mode 100644 setup_jenkins.sh diff --git a/Makefile b/Makefile index 234d78d05..3d6cdbaf2 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ export $(shell sed 's/=.*//' $(env)) SHELL = bash DIR = $(PWD) CONDA = $(DIR)/bin/miniconda3/condabin/conda +LOGFILE=$(DIR)/tmp/progress.txt .PHONY: all @@ -48,7 +49,7 @@ bin-all: bin-conda bin-docker-machine bin-kompose bin-kubectl \ #<<< bin-conda: mkdir - echo " started bin-conda" >> ~/progress.txt + echo " started bin-conda" >> $(LOGFILE) ifeq ($(VENV_OS), linux) curl -Lo $(DIR)/bin/miniconda_install.sh \ https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh @@ -58,7 +59,7 @@ ifeq ($(VENV_OS), darwin) https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh endif bash $(DIR)/bin/miniconda_install.sh -f -b -u -p $(DIR)/bin/miniconda3 - echo " finished bin-conda" >> ~/progress.txt + echo " finished bin-conda" >> $(LOGFILE) #>>> # download docker-machine (for swarm deployment) @@ -66,11 +67,11 @@ endif #<<< bin-docker-machine: mkdir - echo " started bin-docker-machine" >> ~/progress.txt + echo " started bin-docker-machine" >> $(LOGFILE) curl -Lo $(DIR)/bin/docker-machine \ https://github.com/docker/machine/releases/download/v0.16.2/docker-machine-`uname -s`-`uname -m` chmod 755 $(DIR)/bin/docker-machine - echo " finished bin-docker-machine" >> ~/progress.txt + echo " finished bin-docker-machine" >> $(LOGFILE) #>>> # download kompose (for kubernetes deployment) @@ -78,11 +79,11 @@ bin-docker-machine: mkdir #<<< bin-kompose: mkdir - echo " started bin-kompose" >> ~/progress.txt + echo " started bin-kompose" >> $(LOGFILE) curl -Lo $(DIR)/bin/kompose \ https://github.com/kubernetes/kompose/releases/download/v1.21.0/kompose-$(VENV_OS)-amd64 chmod 755 $(DIR)/bin/kompose - echo " finished bin-kompose" >> ~/progress.txt + echo " finished bin-kompose" >> $(LOGFILE) #>>> # download latest kubectl (for kubernetes deployment) @@ -90,11 +91,11 @@ bin-kompose: mkdir #<<< bin-kubectl: mkdir - echo " started bin-kubectl" >> ~/progress.txt + echo " started bin-kubectl" >> $(LOGFILE) curl -Lo $(DIR)/bin/kubectl \ https://storage.googleapis.com/kubernetes-release/release/v1.18.6/bin/$(VENV_OS)/amd64/kubectl chmod 755 $(DIR)/bin/kubectl - echo " finished bin-kubectl" >> ~/progress.txt + echo " finished bin-kubectl" >> $(LOGFILE) #>>> # download latest minikube binary from Google repo @@ -102,11 +103,11 @@ bin-kubectl: mkdir #<<< bin-minikube: mkdir - echo " started bin-minikube" >> ~/progress.txt + echo " started bin-minikube" >> $(LOGFILE) curl -Lo $(DIR)/bin/minikube \ https://storage.googleapis.com/minikube/releases/latest/minikube-$(VENV_OS)-amd64 chmod 755 $(DIR)/bin/minikube - echo " finished bin-minikube" >> ~/progress.txt + echo " finished bin-minikube" >> $(LOGFILE) #>>> # download latest minio server/client from Minio repo @@ -114,14 +115,14 @@ bin-minikube: mkdir #<<< bin-minio: mkdir - echo " started bin-minio" >> ~/progress.txt + echo " started bin-minio" >> $(LOGFILE) curl -Lo $(DIR)/bin/minio \ https://dl.minio.io/server/minio/release/$(VENV_OS)-amd64/minio curl -Lo $(DIR)/bin/mc \ https://dl.minio.io/client/mc/release/$(VENV_OS)-amd64/mc chmod 755 $(DIR)/bin/minio chmod 755 $(DIR)/bin/mc - echo " finished bin-minio" >> ~/progress.txt + echo " finished bin-minio" >> $(LOGFILE) #>>> # download prometheus binaries from Github repo @@ -129,13 +130,13 @@ bin-minio: mkdir #<<< bin-prometheus: - echo " started bin-prometheus" >> ~/progress.txt + echo " started bin-prometheus" >> $(LOGFILE) mkdir -p $(DIR)/bin/prometheus curl -Lo $(DIR)/bin/prometheus/prometheus.tar.gz \ https://github.com/prometheus/prometheus/releases/download/v$(PROMETHEUS_VERSION)/prometheus-$(PROMETHEUS_VERSION).$(VENV_OS)-amd64.tar.gz tar --strip-components=1 -zxvf $(DIR)/bin/prometheus/prometheus.tar.gz -C $(DIR)/bin/prometheus chmod 755 $(DIR)/bin/prometheus/prometheus - echo " finished bin-prometheus" >> ~/progress.txt + echo " finished bin-prometheus" >> $(LOGFILE) #>>> # download latest traefik binary from Github repo @@ -143,12 +144,12 @@ bin-prometheus: #<<< bin-traefik: mkdir - echo " started bin-traefik" >> ~/progress.txt + echo " started bin-traefik" >> $(LOGFILE) curl -Lo $(DIR)/bin/traefik.tar.gz \ https://github.com/traefik/traefik/releases/download/v$(TRAEFIK_VERSION)/traefik_v$(TRAEFIK_VERSION)_$(VENV_OS)_amd64.tar.gz tar -xvzf $(DIR)/bin/traefik.tar.gz -C bin/ chmod 755 $(DIR)/bin/traefik - echo " finished bin-traefik" >> ~/progress.txt + echo " finished bin-traefik" >> $(LOGFILE) #>>> # (re)build service image and deploy/test using docker-compose @@ -159,11 +160,11 @@ bin-traefik: mkdir #<<< build-%: - echo " started build-$*" >> ~/progress.txt + echo " started build-$*" >> $(LOGFILE) DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 \ cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml $(DIR)/lib/$*/docker-compose.yml \ | docker-compose -f - build $(BUILD_OPTS) - echo " finished build-$*" >> ~/progress.txt + echo " finished build-$*" >> $(LOGFILE) #>>> # run all cleanup functions @@ -492,11 +493,11 @@ compose-authx-setup-candig-server: compose-authx-setup #<<< compose-%: - echo " started compose-$*" >> ~/progress.txt + echo " started compose-$*" >> $(LOGFILE) cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ $(DIR)/lib/$*/docker-compose.yml \ | docker-compose -f - up -d - echo " finished compose-$*" >> ~/progress.txt + echo " finished compose-$*" >> $(LOGFILE) #>>> # create docker bridge networks @@ -530,7 +531,6 @@ docker-pull: .PHONY: docker-push docker-push: $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) push-$(MODULE);) - $(foreach MODULE, $(TOIL_MODULES), docker push $(DOCKER_REGISTRY)/$(MODULE):latest;) #>>> # create secrets for CanDIG services @@ -580,7 +580,7 @@ docker-volumes: #<<< .PHONY: images -images: toil-docker +images: #toil-docker $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) build-$(MODULE);) #>>> @@ -590,12 +590,12 @@ images: toil-docker #<<< .PHONY: init-conda init-conda: - echo " started init-conda" >> ~/progress.txt + echo " started init-conda" >> $(LOGFILE) $(CONDA) create -y -n $(VENV_NAME) python=$(VENV_PYTHON) pip=$(VENV_PIP) @echo "Load local conda: source $(DIR)/bin/miniconda3/etc/profile.d/conda.sh" @echo "Activate conda env: conda activate $(VENV_NAME)" @echo "Install requirements: pip install -U -r $(DIR)/etc/venv/requirements.txt" - echo " finished init-conda" >> ~/progress.txt + echo " finished init-conda" >> $(LOGFILE) #>>> # initialize docker and create required docker networks, volumes, certs, secrets, and conda env @@ -822,7 +822,7 @@ swarm-secrets: #<<< .PHONY: toil-docker toil-docker: - echo " started toil-docker" >> ~/progress.txt + echo " started toil-docker" >> $(LOGFILE) VIRTUAL_ENV=1 DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 TOIL_DOCKER_REGISTRY=$(DOCKER_REGISTRY) $(MAKE) -C $(DIR)/lib/toil/toil-docker docker $(foreach MODULE,$(TOIL_MODULES), \ docker tag $(DOCKER_REGISTRY)/$(MODULE):$(TOIL_VERSION)-$(TOIL_BUILD_HASH) \ @@ -830,7 +830,8 @@ toil-docker: $(foreach MODULE,$(TOIL_MODULES), \ docker tag $(DOCKER_REGISTRY)/$(MODULE):$(TOIL_VERSION) \ $(DOCKER_REGISTRY)/$(MODULE):latest;) - echo " finished toil-docker" >> ~/progress.txt + $(foreach MODULE, $(TOIL_MODULES), docker push $(DOCKER_REGISTRY)/$(MODULE):latest;) + echo " finished toil-docker" >> $(LOGFILE) #>>> # deploys all modules using Tox diff --git a/provision.sh b/provision.sh index 3e436ea9f..28e8f7961 100644 --- a/provision.sh +++ b/provision.sh @@ -1,29 +1,32 @@ #!/usr/bin/env bash # Optional args: CanDIGv2 repo path, CanDIGv2 repo branch -echo "started provision.sh" | tee -a ~/progress.txt + +LOGFILE=$PWD/tmp/progress.txt + +echo "started provision.sh" | tee -a $LOGFILE # sudo apt-get update sudo apt-get upgrade -y -echo " finished apt-get upgrade" | tee -a ~/progress.txt +echo " finished apt-get upgrade" | tee -a $LOGFILE # remove grub file and recreate, in case of version conflicts sudo rm /boot/grub/menu.lst sudo update-grub-legacy-ec2 -y sudo apt-get dist-upgrade -y sudo apt-get update -echo " finished apt-get dist-upgrade" | tee -a ~/progress.txt +echo " finished apt-get dist-upgrade" | tee -a $LOGFILE -echo " installing necessary packages" | tee -a ~/progress.txt +echo " installing necessary packages" | tee -a $LOGFILE declare -a install_pkgs=("build-essential" "zlib1g-dev" "libncurses5-dev" "libgdbm-dev" "libnss3-dev" "libssl-dev" "libreadline-dev" "libffi-dev" "wget" "curl" "git" "make" "gcc" "libsqlite3-dev" "libbz2-dev" "liblzma-dev" "lzma" "sqlite3" "apt-transport-https" "ca-certificates" "gnupg2" "software-properties-common") for pkg in ${install_pkgs[@]}; do sudo apt-get install -y $pkg if [ $? -ne 0 ]; then - echo "ERROR! Failed to install $pkg" | tee -a ~/progress.txt + echo "ERROR! Failed to install $pkg" | tee -a $LOGFILE exit 1 fi done -echo " finished installs" | tee -a ~/progress.txt +echo " finished installs" | tee -a $LOGFILE if [ -n "$1" ] then @@ -53,7 +56,7 @@ fi sudo chown -R $(whoami):$(whoami) $path git submodule update --init --recursive cp -i etc/env/example.env .env -echo " finished setting up CanDIGv2 repo" | tee -a ~/progress.txt +echo " finished setting up CanDIGv2 repo" | tee -a $LOGFILE dist=$(lsb_release -is) codename=$(lsb_release -cs) @@ -63,7 +66,7 @@ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/${di sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io -echo " finished installing docker components" | tee -a ~/progress.txt +echo " finished installing docker components" | tee -a $LOGFILE sudo apt-get autoclean sudo apt-get autoremove -y @@ -73,19 +76,19 @@ sudo usermod -aG docker $(whoami) make bin-all if [ $? -ne 0 ]; then - echo "ERROR! Failed make bin-all" | tee -a ~/progress.txt + echo "ERROR! Failed make bin-all" | tee -a $LOGFILE exit 1 fi make init-conda if [ $? -ne 0 ]; then - echo "ERROR! Failed make init-conda" | tee -a ~/progress.txt + echo "ERROR! Failed make init-conda" | tee -a $LOGFILE exit 1 fi -echo " starting candig pip install" | tee -a ~/progress.txt +echo " starting candig pip install" | tee -a $LOGFILE source $PWD/bin/miniconda3/etc/profile.d/conda.sh conda activate candig pip install -U -r $PWD/etc/venv/requirements.txt #pip install -U -r $PWD/etc/venv/requirements-dev.txt -echo "finished provision.sh" | tee -a ~/progress.txt +echo "finished provision.sh" | tee -a $LOGFILE diff --git a/setup_containers.sh b/setup_containers.sh index 732247bfd..f3919fc3c 100644 --- a/setup_containers.sh +++ b/setup_containers.sh @@ -1,36 +1,38 @@ #!/usr/bin/env bash -grep -q "finished provision.sh" ~/progress.txt +LOGFILE=$PWD/tmp/progress.txt + +grep -q "finished provision.sh" $LOGFILE if [ $? -ne 0 ]; then - echo "ERROR! Provisioning failed, do not set up containers" | tee -a ~/progress.txt + echo "ERROR! Provisioning failed, do not set up containers" | tee -a $LOGFILE exit 1 fi -echo "started setup_containers.sh" | tee -a ~/progress.txt +echo "started setup_containers.sh" | tee -a $LOGFILE cd $1 source $PWD/bin/miniconda3/etc/profile.d/conda.sh conda activate candig export PATH="$PWD/bin:$PATH" eval $(docker-machine env manager) -echo " started make init-docker" | tee -a ~/progress.txt +echo " started make init-docker" | tee -a $LOGFILE make init-docker if [ $? -ne 0 ]; then - echo "ERROR! make init-docker failed" | tee -a ~/progress.txt + echo "ERROR! make init-docker failed" | tee -a $LOGFILE exit 1 fi -echo " started make docker-pull" | tee -a ~/progress.txt +echo " started make docker-pull" | tee -a $LOGFILE make docker-pull if [ $? -ne 0 ]; then - echo "ERROR! make docker-pull failed" | tee -a ~/progress.txt + echo "ERROR! make docker-pull failed" | tee -a $LOGFILE exit 1 fi -echo " started make compose" | tee -a ~/progress.txt +echo " started make compose" | tee -a $LOGFILE make compose if [ $? -ne 0 ]; then - echo "ERROR! make compose failed" | tee -a ~/progress.txt + echo "ERROR! make compose failed" | tee -a $LOGFILE exit 1 fi -echo "finished setup_containers.sh" | tee -a ~/progress.txt +echo "finished setup_containers.sh" | tee -a $LOGFILE diff --git a/setup_jenkins.sh b/setup_jenkins.sh new file mode 100644 index 000000000..a18cc920e --- /dev/null +++ b/setup_jenkins.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +LOGFILE=$PWD/tmp/progress.txt + +sed 's/DOCKER_REGISTRY=.*/DOCKER_REGISTRY=ghcr.io\/candig/' etc/env/example.env > .env + +. $PWD/bin/miniconda3/etc/profile.d/conda.sh +conda activate candig +if [ $? -ne 0 ]; then + echo "need to re-run setup" + touch $LOGFILE + make bin-all + make init-conda + . $PWD/bin/miniconda3/etc/profile.d/conda.sh + conda activate candig + pip install -U -r $PWD/etc/venv/requirements.txt +else + echo "already set up" +fi From a78177ea6a910dbfa2a451f489e07d9386bb780f Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 12 Jul 2021 11:10:11 -0700 Subject: [PATCH 004/236] update cancogen-dashboard (#81) --- lib/cancogen-dashboard/cancogen_dashboard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cancogen-dashboard/cancogen_dashboard b/lib/cancogen-dashboard/cancogen_dashboard index 5d23947cb..09b32935a 160000 --- a/lib/cancogen-dashboard/cancogen_dashboard +++ b/lib/cancogen-dashboard/cancogen_dashboard @@ -1 +1 @@ -Subproject commit 5d23947cb065088fd14c6ff90bf4206b884d51a7 +Subproject commit 09b32935a1bc0c933390f7a522cd0c56e691bf30 From daf96bb7e0432faebd33fd64a1e76c771b66c2ea Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 12 Jul 2021 13:13:28 -0700 Subject: [PATCH 005/236] create Jenkinsfile (#83) --- Jenkinsfile | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..c1bea3b11 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,25 @@ +pipeline { + agent any + + stages { + stage('Setup') { + steps { + checkout([$class: 'GitSCM', branches: [[name: 'develop']], extensions: [[$class: 'SubmoduleOption', disableSubmodules: false, parentCredentials: false, recursiveSubmodules: true, reference: '', trackingSubmodules: false]], userRemoteConfigs: [[url: 'https://github.com/CanDIG/CanDIGv2']]]) + sh '''bash setup_jenkins.sh''' + } + } + stage('Make') { + steps { + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + sh '''. $PWD/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; make images''' + } + } + } + stage('Publish') { + steps { + sh '''. $PWD/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; make docker-push''' + } + } + } +} + From 6151ab8ab570fffe07eeee4c4a5ee4bc98026dde Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 13 Jul 2021 10:42:53 -0700 Subject: [PATCH 006/236] Pin version of alpine to 3.13 (#84) * add pinned version for ALPINE_VERSION * pin alpine_version * test * test * test * test * test --- etc/env/example.env | 2 ++ lib/chord-metadata/Dockerfile | 4 ++-- lib/chord-metadata/docker-compose.yml | 1 + lib/drs-server/Dockerfile | 4 ++-- lib/drs-server/docker-compose.yml | 1 + lib/federation-service/Dockerfile | 4 ++-- lib/federation-service/docker-compose.yml | 1 + lib/htsget-server/docker-compose.yml | 1 + lib/templates/Dockerfile | 4 ++-- lib/templates/docker-compose.yml | 1 + 10 files changed, 15 insertions(+), 8 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index e70a7f593..48cf3b1d4 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -36,6 +36,8 @@ DOCKER_REGISTRY=candig # options are [json, fluentd] DOCKER_LOG_DRIVER=json +ALPINE_VERSION=3.13 + # docker swarm # options are [manager, worker] SWARM_MODE=manager diff --git a/lib/chord-metadata/Dockerfile b/lib/chord-metadata/Dockerfile index 1c7f59b8f..8ea7586fc 100644 --- a/lib/chord-metadata/Dockerfile +++ b/lib/chord-metadata/Dockerfile @@ -1,6 +1,6 @@ ARG venv_python - -FROM python:${venv_python}-alpine +ARG alpine_version +FROM python:${venv_python}-alpine${alpine_version} LABEL Maintainer="CanDIG Project" diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 3b49ec5de..223d33c30 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -6,6 +6,7 @@ services: context: $PWD/lib/chord-metadata args: venv_python: "${VENV_PYTHON}" + alpine_version: "${ALPINE_VERSION}" image: ${DOCKER_REGISTRY}/chord-metadata:${CHORD_METADATA_VERSION:-latest} networks: - ${DOCKER_NET} diff --git a/lib/drs-server/Dockerfile b/lib/drs-server/Dockerfile index 08ed75db2..b8f8b7118 100644 --- a/lib/drs-server/Dockerfile +++ b/lib/drs-server/Dockerfile @@ -1,6 +1,6 @@ ARG venv_python - -FROM python:${venv_python}-alpine +ARG alpine_version +FROM python:${venv_python}-alpine${alpine_version} LABEL Maintainer="CanDIG Project" diff --git a/lib/drs-server/docker-compose.yml b/lib/drs-server/docker-compose.yml index dad701bb1..da250e8fd 100644 --- a/lib/drs-server/docker-compose.yml +++ b/lib/drs-server/docker-compose.yml @@ -6,6 +6,7 @@ services: context: $PWD/lib/drs-server args: venv_python: "${VENV_PYTHON}" + alpine_version: "${ALPINE_VERSION}" image: ${DOCKER_REGISTRY}/chord-drs:${CHORD_DRS_VERSION:-latest} networks: - ${DOCKER_NET} diff --git a/lib/federation-service/Dockerfile b/lib/federation-service/Dockerfile index 0baf1a6f8..a29c6bb26 100644 --- a/lib/federation-service/Dockerfile +++ b/lib/federation-service/Dockerfile @@ -1,6 +1,6 @@ ARG venv_python - -FROM python:${venv_python}-alpine +ARG alpine_version +FROM python:${venv_python}-alpine${alpine_version} LABEL Maintainer="CanDIG Project" diff --git a/lib/federation-service/docker-compose.yml b/lib/federation-service/docker-compose.yml index 3a1fab85e..e775056c5 100644 --- a/lib/federation-service/docker-compose.yml +++ b/lib/federation-service/docker-compose.yml @@ -6,6 +6,7 @@ services: context: $PWD/lib/federation-service args: venv_python: "${VENV_PYTHON}" + alpine_version: "${ALPINE_VERSION}" image: ${DOCKER_REGISTRY}/federation-service:${FEDERATION_VERSION:-latest} networks: - ${DOCKER_NET} diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index a2b28a560..360b44078 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -6,6 +6,7 @@ services: context: $PWD/lib/htsget-server/htsget_app args: venv_python: "${VENV_PYTHON}" + alpine_version: "${ALPINE_VERSION}" image: ${DOCKER_REGISTRY}/htsget-app:${HTSGET_APP_VERSION:-latest} networks: - ${DOCKER_NET} diff --git a/lib/templates/Dockerfile b/lib/templates/Dockerfile index c0cf3be18..b7ef297d2 100644 --- a/lib/templates/Dockerfile +++ b/lib/templates/Dockerfile @@ -1,6 +1,6 @@ ARG venv_python - -FROM python:${venv_python}-alpine +ARG alpine_version +FROM python:${venv_python}-alpine${alpine_version} LABEL Maintainer="CanDIG Project" diff --git a/lib/templates/docker-compose.yml b/lib/templates/docker-compose.yml index df1efe5f1..1b64a48f9 100644 --- a/lib/templates/docker-compose.yml +++ b/lib/templates/docker-compose.yml @@ -7,6 +7,7 @@ services: #context: $PWD/lib/{{service_name}}/{{submodule_name}} args: venv_python: "${VENV_PYTHON}" + alpine_version: "${ALPINE_VERSION}" image: ${DOCKER_REGISTRY}/{{service_name}}:${{{service_version}}:-latest} #volumes: #- {{service_name}}-data:/data From 866625597f23394613e3657fabb038108a775a29 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 15 Jul 2021 09:06:54 -0700 Subject: [PATCH 007/236] update htsget submodule (#85) * update htsget submodule * update again to stable --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 1a47f80a7..b024c264d 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 1a47f80a7ccc8e84af0481a2f521938caed092c6 +Subproject commit b024c264d32b10a2eb7f1c0120f91eb8d76dc326 From f0b8505f42ddf33cf1ac41672f9c3ccf38dfc0dd Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 16 Jul 2021 13:35:01 -0700 Subject: [PATCH 008/236] add GitHub credentials (#86) --- Jenkinsfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index c1bea3b11..b4a6e33c6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,7 +17,9 @@ pipeline { } stage('Publish') { steps { - sh '''. $PWD/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; make docker-push''' + withCredentials([usernamePassword(credentialsId: '76fcbce2-568d-4fe9-b56e-b269473e7b7f', passwordVariable: 'GITHUB_TOKEN', usernameVariable: 'GITHUB_USER')]) { + sh '''. $PWD/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_USER --password-stdin; make docker-push''' + } } } } From 445d508cf5223410e4be1ee8d0f5d3f6f4ef0dfd Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 21 Jul 2021 17:14:30 -0700 Subject: [PATCH 009/236] Switch back to Dockerhub registry (#87) * instead of overriding DOCKER_REGISTRY in the env var directly, override as a make argument * check out the code branch that matches the Jenkins-UI one * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry * add options for which registry --- Jenkinsfile | 9 ++++----- Makefile | 6 ++++++ setup_jenkins.sh | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index b4a6e33c6..7b1b753c7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,24 +1,23 @@ pipeline { agent any - stages { stage('Setup') { steps { - checkout([$class: 'GitSCM', branches: [[name: 'develop']], extensions: [[$class: 'SubmoduleOption', disableSubmodules: false, parentCredentials: false, recursiveSubmodules: true, reference: '', trackingSubmodules: false]], userRemoteConfigs: [[url: 'https://github.com/CanDIG/CanDIGv2']]]) + checkout([$class: 'GitSCM', branches: [[name: "$GIT_BRANCH"]], extensions: [[$class: 'SubmoduleOption', disableSubmodules: false, parentCredentials: false, recursiveSubmodules: true, reference: '', trackingSubmodules: false]], userRemoteConfigs: [[url: 'https://github.com/CanDIG/CanDIGv2']]]) sh '''bash setup_jenkins.sh''' } } stage('Make') { steps { catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { - sh '''. $PWD/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; make images''' + sh('. $WORKSPACE/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; make images') } } } stage('Publish') { steps { - withCredentials([usernamePassword(credentialsId: '76fcbce2-568d-4fe9-b56e-b269473e7b7f', passwordVariable: 'GITHUB_TOKEN', usernameVariable: 'GITHUB_USER')]) { - sh '''. $PWD/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_USER --password-stdin; make docker-push''' + withCredentials([usernamePassword(credentialsId: 'registry-1.docker.io', passwordVariable: 'TOKEN', usernameVariable: 'USERNAME')]) { + sh('. $WORKSPACE/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; echo $TOKEN | docker login registry-1.docker.io -u $USERNAME --password-stdin; make docker-push') } } } diff --git a/Makefile b/Makefile index 3d6cdbaf2..23c71f3d7 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,12 @@ DIR = $(PWD) CONDA = $(DIR)/bin/miniconda3/condabin/conda LOGFILE=$(DIR)/tmp/progress.txt +ifeq ($(OVERRIDE_REGISTRY), registry-1.docker.io) + export DOCKER_REGISTRY = candig +endif +ifeq ($(OVERRIDE_REGISTRY), ghcr.io) + export DOCKER_REGISTRY = ghcr.io/candig +endif .PHONY: all all: diff --git a/setup_jenkins.sh b/setup_jenkins.sh index a18cc920e..384496178 100644 --- a/setup_jenkins.sh +++ b/setup_jenkins.sh @@ -2,7 +2,7 @@ LOGFILE=$PWD/tmp/progress.txt -sed 's/DOCKER_REGISTRY=.*/DOCKER_REGISTRY=ghcr.io\/candig/' etc/env/example.env > .env +cp etc/env/example.env .env . $PWD/bin/miniconda3/etc/profile.d/conda.sh conda activate candig From f62ca320a3b696a5b064c87d0e2c13ce37dd292e Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 3 Aug 2021 18:28:16 -0700 Subject: [PATCH 010/236] update to new stable commit for htsget (#89) --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index b024c264d..4430b4d58 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit b024c264d32b10a2eb7f1c0120f91eb8d76dc326 +Subproject commit 4430b4d584c0966ce0cc262e556da9fd3f51409a From 119b382280450ea3472e8131fa1b9bf449139fd4 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 17 Aug 2021 14:13:07 -0700 Subject: [PATCH 011/236] update datasets submodule to point to develop branch (#90) * update datasets submodule to point to develop branch * update datasets submodule --- lib/datasets/datasets_service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/datasets/datasets_service b/lib/datasets/datasets_service index fe050986d..4f47525db 160000 --- a/lib/datasets/datasets_service +++ b/lib/datasets/datasets_service @@ -1 +1 @@ -Subproject commit fe050986d7f56a9e35696d636a5f38810c220a91 +Subproject commit 4f47525dbb15be49bcd4a793d8fd5699970f3383 From 71856ea652dcb06a62dfc302b54cc7dd7f263215 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 17 Aug 2021 14:13:22 -0700 Subject: [PATCH 012/236] Switch for registries in Jenkins (#88) * how it would ideally work one parameter to pass in to the pipeline * Fix syntax * add defaultValue * first choice is default choice * actually last choice is the default choice * log value of REGISTRY_URL at top of console log * log value of REGISTRY_URL at top of console log --- Jenkinsfile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7b1b753c7..635a02929 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,8 +1,13 @@ pipeline { agent any + parameters { + // the choice that the automated system uses is whichever value was last chosen in "Build with Parameters" + choice choices: ['registry-1.docker.io', 'ghcr.io'], description: 'URL of container registry', name: 'REGISTRY_URL' + } stages { stage('Setup') { steps { + sh('echo "REGISTRY_URL is set to ${REGISTRY_URL}"') checkout([$class: 'GitSCM', branches: [[name: "$GIT_BRANCH"]], extensions: [[$class: 'SubmoduleOption', disableSubmodules: false, parentCredentials: false, recursiveSubmodules: true, reference: '', trackingSubmodules: false]], userRemoteConfigs: [[url: 'https://github.com/CanDIG/CanDIGv2']]]) sh '''bash setup_jenkins.sh''' } @@ -10,14 +15,14 @@ pipeline { stage('Make') { steps { catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { - sh('. $WORKSPACE/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; make images') + sh('. $WORKSPACE/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; make images OVERRIDE_REGISTRY=${REGISTRY_URL}') } } } stage('Publish') { steps { - withCredentials([usernamePassword(credentialsId: 'registry-1.docker.io', passwordVariable: 'TOKEN', usernameVariable: 'USERNAME')]) { - sh('. $WORKSPACE/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; echo $TOKEN | docker login registry-1.docker.io -u $USERNAME --password-stdin; make docker-push') + withCredentials([usernamePassword(credentialsId: params.REGISTRY_URL, passwordVariable: 'TOKEN', usernameVariable: 'USERNAME')]) { + sh('. $WORKSPACE/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; echo ${TOKEN} | docker login ${REGISTRY_URL} -u ${USERNAME} --password-stdin; make docker-push OVERRIDE_REGISTRY=${REGISTRY_URL}') } } } From c97b3f49ad91b5832cce9d56d34d519ecd70acad Mon Sep 17 00:00:00 2001 From: Shaikh Farhan Rashid Date: Wed, 18 Aug 2021 14:25:24 -0400 Subject: [PATCH 013/236] bump datasets version --- lib/datasets/datasets_service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/datasets/datasets_service b/lib/datasets/datasets_service index 4f47525db..8f0403fff 160000 --- a/lib/datasets/datasets_service +++ b/lib/datasets/datasets_service @@ -1 +1 @@ -Subproject commit 4f47525dbb15be49bcd4a793d8fd5699970f3383 +Subproject commit 8f0403fffeb4974a90e00eb6add7c20bbe301f8c From c64e7a7764936bb7eaa1e58f4e669f43b2bbd1b7 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 19 Aug 2021 12:06:08 -0700 Subject: [PATCH 014/236] Reorganize submodules (#92) * move Dockerfile to within repo * move Dockerfile to within repo * move dockerfile for datasets submodule * remove Dockerfile from cnv-service * remove Dockerfile from cnv-service * moving Dockerfile into submodule * updating templates * update context in template docker-compose --- lib/cancogen-dashboard/Dockerfile | 14 ------ lib/cancogen-dashboard/cancogen_dashboard | 2 +- lib/cancogen-dashboard/docker-compose.yml | 2 +- lib/cnv-service/Dockerfile | 16 ------- lib/cnv-service/candig_cnv_service | 2 +- lib/cnv-service/docker-compose.yml | 2 +- lib/datasets/Dockerfile | 15 ------- lib/datasets/docker-compose.yml | 2 +- lib/federation-service/Dockerfile | 36 --------------- lib/federation-service/docker-compose.yml | 2 +- lib/federation-service/federation_service | 2 +- lib/templates/Dockerfile | 53 ++++++----------------- lib/templates/README.md | 9 ++++ lib/templates/docker-compose.yml | 3 +- lib/templates/run.sh | 28 ------------ 15 files changed, 31 insertions(+), 157 deletions(-) delete mode 100644 lib/cancogen-dashboard/Dockerfile delete mode 100644 lib/cnv-service/Dockerfile delete mode 100644 lib/datasets/Dockerfile delete mode 100644 lib/federation-service/Dockerfile create mode 100644 lib/templates/README.md delete mode 100755 lib/templates/run.sh diff --git a/lib/cancogen-dashboard/Dockerfile b/lib/cancogen-dashboard/Dockerfile deleted file mode 100644 index 477470f13..000000000 --- a/lib/cancogen-dashboard/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM node:14.16.0-alpine - -LABEL Maintainer="CanDIG Project" - -RUN apk add --no-cache git - -COPY cancogen_dashboard /app/cancogen_dashboard - -WORKDIR /app/cancogen_dashboard - -RUN rm -rf package-lock.json .git \ - && yarn install && yarn run build - -ENTRYPOINT ["yarn", "start"] diff --git a/lib/cancogen-dashboard/cancogen_dashboard b/lib/cancogen-dashboard/cancogen_dashboard index 09b32935a..946b72fa9 160000 --- a/lib/cancogen-dashboard/cancogen_dashboard +++ b/lib/cancogen-dashboard/cancogen_dashboard @@ -1 +1 @@ -Subproject commit 09b32935a1bc0c933390f7a522cd0c56e691bf30 +Subproject commit 946b72fa941e1b00184191f95f83f73b7dec91e6 diff --git a/lib/cancogen-dashboard/docker-compose.yml b/lib/cancogen-dashboard/docker-compose.yml index 632e0b08a..fd5cbacb9 100644 --- a/lib/cancogen-dashboard/docker-compose.yml +++ b/lib/cancogen-dashboard/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.7' services: cancogen-dashboard: build: - context: $PWD/lib/cancogen-dashboard + context: $PWD/lib/cancogen-dashboard/cancogen_dashboard image: ${DOCKER_REGISTRY}/cancogen-dashboard:${CANCOGEN_DASHBOARD_VERSION:-latest} networks: - ${DOCKER_NET} diff --git a/lib/cnv-service/Dockerfile b/lib/cnv-service/Dockerfile deleted file mode 100644 index 076cd3d82..000000000 --- a/lib/cnv-service/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -ARG venv_python - -FROM python:${venv_python}-slim - -LABEL Maintainer="CanDIG Project" - -RUN apt-get update && apt-get install -y build-essential - -COPY candig_cnv_service /app/candig_cnv_service - -WORKDIR /app/candig_cnv_service - -#RUN python setup.py install -RUN pip install --no-cache-dir -e . - -ENTRYPOINT ["candig_cnv_service"] diff --git a/lib/cnv-service/candig_cnv_service b/lib/cnv-service/candig_cnv_service index 1e225e272..29cff73a9 160000 --- a/lib/cnv-service/candig_cnv_service +++ b/lib/cnv-service/candig_cnv_service @@ -1 +1 @@ -Subproject commit 1e225e272b11950ccb83684c561696a163fa62b9 +Subproject commit 29cff73a941d6d4f439e4b27076f9922f5881d36 diff --git a/lib/cnv-service/docker-compose.yml b/lib/cnv-service/docker-compose.yml index 275867a5f..4cb2a6ce4 100644 --- a/lib/cnv-service/docker-compose.yml +++ b/lib/cnv-service/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.7' services: cnv-service: build: - context: $PWD/lib/cnv-service + context: $PWD/lib/cnv-service/candig_cnv_service args: venv_python: "${VENV_PYTHON}" image: ${DOCKER_REGISTRY}/cnv-service:${CNV_SERVICE_VERSION:-latest} diff --git a/lib/datasets/Dockerfile b/lib/datasets/Dockerfile deleted file mode 100644 index 704bc8f11..000000000 --- a/lib/datasets/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -ARG venv_python - -FROM python:${venv_python}-slim - -LABEL Maintainer="CanDIG Project" - -COPY datasets_service /app/datasets_service - -WORKDIR /app/datasets_service - -RUN apt-get update -RUN apt-get -y install gcc libc-dev -RUN pip install -r requirements.txt - -ENTRYPOINT [ "python", "-m", "candig_dataset_service" ] diff --git a/lib/datasets/docker-compose.yml b/lib/datasets/docker-compose.yml index e0f51f337..dcb57180b 100644 --- a/lib/datasets/docker-compose.yml +++ b/lib/datasets/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.7' services: datasets: build: - context: $PWD/lib/datasets + context: $PWD/lib/datasets/datasets_service args: venv_python: "${VENV_PYTHON}" image: ${DOCKER_REGISTRY}/datasets:${DATASETS_VERSION:-latest} diff --git a/lib/federation-service/Dockerfile b/lib/federation-service/Dockerfile deleted file mode 100644 index a29c6bb26..000000000 --- a/lib/federation-service/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -ARG venv_python -ARG alpine_version -FROM python:${venv_python}-alpine${alpine_version} - -LABEL Maintainer="CanDIG Project" - -USER root - -RUN apk update - -RUN apk add --no-cache \ - autoconf \ - automake \ - make \ - gcc \ - linux-headers \ - perl \ - bash \ - build-base \ - musl-dev \ - zlib-dev \ - bzip2-dev \ - xz-dev \ - libcurl \ - curl \ - curl-dev \ - yaml-dev \ - libressl-dev \ - git - -COPY federation_service /app/federation_service -WORKDIR /app/federation_service - -RUN pip install --no-cache-dir -r requirements.txt - -ENTRYPOINT ["python", "-m", "candig_federation"] diff --git a/lib/federation-service/docker-compose.yml b/lib/federation-service/docker-compose.yml index e775056c5..ff4cfa94a 100644 --- a/lib/federation-service/docker-compose.yml +++ b/lib/federation-service/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.7' services: federation-service: build: - context: $PWD/lib/federation-service + context: $PWD/lib/federation-service/federation_service args: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" diff --git a/lib/federation-service/federation_service b/lib/federation-service/federation_service index 47e7a87c9..20b9805a7 160000 --- a/lib/federation-service/federation_service +++ b/lib/federation-service/federation_service @@ -1 +1 @@ -Subproject commit 47e7a87c96f26c5403eb29d7f8dad5e472c476a0 +Subproject commit 20b9805a7835ee25c7f176e6760be19fc7da233c diff --git a/lib/templates/Dockerfile b/lib/templates/Dockerfile index b7ef297d2..0be165a96 100644 --- a/lib/templates/Dockerfile +++ b/lib/templates/Dockerfile @@ -1,52 +1,27 @@ +# This Dockerfile should be located inside the root level of the submodule + +# These arguments are picked up from the environment and passed in from the docker-compose file ARG venv_python ARG alpine_version FROM python:${venv_python}-alpine${alpine_version} LABEL Maintainer="CanDIG Project" -ENV PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PYTHONHASHSEED=random \ - PIP_NO_CACHE_DIR=off \ - PIP_DISABLE_PIP_VERSION_CHECK=on - -RUN adduser -h /app -D user - -RUN apk add --no-cache \ - autoconf \ - automake \ - build-base \ - bzip2-dev \ - curl \ - curl-dev \ - gcc \ - git \ - libcurl \ - libffi-dev \ - libressl-dev \ - linux-headers \ - make \ - musl-dev \ - perl \ - postgresql-dev \ - postgresql-libs \ - xz-dev \ - yaml-dev \ - zlib-dev +# Copy module repo as needed +COPY . /app/ -USER user -WORKDIR /app +# Alternatively, clone it: +# RUN git clone https://github.com/candig/.git -# clone or COPY module repo as needed -# example: -# RUN git clone https://github.com/c3g/chord_drs.git -# COPY chord_drs /app/chord_drs +# Set the working directory +WORKDIR /app/ -# run necessary steps to implement module -# example: -# WORKDIR /app/chord_drs +# Run necessary steps to implement module +# uncomment and add as needed: +# RUN apt-get update +# RUN apt-get -y install gcc libc-dev # RUN pip install --no-cache-dir -r requirements.txt && flask db upgrade # Run the model service server -# example: +# uncomment and add as needed: # ENTRYPOINT ["flask", "run"] diff --git a/lib/templates/README.md b/lib/templates/README.md new file mode 100644 index 000000000..d5d725184 --- /dev/null +++ b/lib/templates/README.md @@ -0,0 +1,9 @@ +# Adding a new service to CanDIGv2 + +If you've got a service to add from a separately-maintained repo, use these template instructions. + +* Create a standalone Docker container for your service, using the Dockerfile template. Verify that your repo can spin up in a Docker container successfully. +* Create a new directory under `lib` for your service. +* Add your repo as a submodule in this directory. +* Add a docker-compose file in this directory, based on the provided template. +* Create a pull request! diff --git a/lib/templates/docker-compose.yml b/lib/templates/docker-compose.yml index 1b64a48f9..6f04b16c5 100644 --- a/lib/templates/docker-compose.yml +++ b/lib/templates/docker-compose.yml @@ -3,8 +3,7 @@ version: '3.7' services: {{service_name}}: build: - context: $PWD/lib/{{service_name}} - #context: $PWD/lib/{{service_name}}/{{submodule_name}} + context: $PWD/lib/{{service_name}}/{{submodule_name}} args: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" diff --git a/lib/templates/run.sh b/lib/templates/run.sh deleted file mode 100755 index 7bf602b30..000000000 --- a/lib/templates/run.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -set -e - -source .env - -[ -z $CONDA_DEFAULT_ENV ] && \ - source $(pwd)/bin/miniconda3/etc/profile.d/conda.sh && \ - conda activate ${VENV_NAME} - -# add any exports here -# remember that secrets should be passed as opaque file tokens -#example: -#export SERVICE_PORT=${SERVICE_PORT_FROM_ENV_FILE} -#export SERVICE_SECRET=$(pwd)/{{path_to_secret}} -#bad example: -#export SERVICE_SECRET='plaintext_secret' - -pushd $(pwd)/lib/{{service_name}}/chord_drs -# use submodule name if needed -pushd $(pwd)/lib/{{service_name}}/chord_drs -#pushd $(pwd)/lib/{{service_name}}/{{submodule_name}} - # add the steps necessary to run service in virtualenv - # example: - #pip install -r requirements.txt - #flask db upgrade - #flask run --host 0.0.0.0 --port ${{{service_port}}} -#popd From 5c7286dea741a2a89d1fdd1e0bd75379503c77d7 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 26 Aug 2021 12:16:58 -0700 Subject: [PATCH 015/236] Hotfix: update htsget app pointer (#93) * update htsget_app pointer * update htsget_app pointer --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 4430b4d58..b4b65aff2 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 4430b4d584c0966ce0cc262e556da9fd3f51409a +Subproject commit b4b65aff20718f706925033d6ea7505e6ea0c595 From 9ff08c49aa61bf972584a8d119bd791bc7467c5e Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 27 Aug 2021 18:01:52 -0700 Subject: [PATCH 016/236] Move submodules to candig forks (#94) * update htsget_app pointer * update drs-server to candig fork * change repo for chord_metadata_service to candig fork * move Dockerfile to chord_drs submodule; adjust links --- lib/chord-metadata/chord_metadata_service | 2 +- lib/drs-server/Dockerfile | 40 ----------------------- lib/drs-server/chord_drs | 2 +- lib/drs-server/docker-compose.yml | 2 +- 4 files changed, 3 insertions(+), 43 deletions(-) delete mode 100644 lib/drs-server/Dockerfile diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 94945a443..351398253 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 94945a4432317a4c9ec4a9d7f27506f004e38c54 +Subproject commit 3513982539e45315b94019b1a6ec7406b3935d47 diff --git a/lib/drs-server/Dockerfile b/lib/drs-server/Dockerfile deleted file mode 100644 index b8f8b7118..000000000 --- a/lib/drs-server/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -ARG venv_python -ARG alpine_version -FROM python:${venv_python}-alpine${alpine_version} - -LABEL Maintainer="CanDIG Project" - -USER root - -RUN apk update - -RUN apk add --no-cache \ - autoconf \ - automake \ - make \ - gcc \ - perl \ - bash \ - build-base \ - musl-dev \ - zlib-dev \ - bzip2-dev \ - xz-dev \ - libcurl \ - curl \ - curl-dev \ - yaml-dev \ - libressl-dev \ - git \ - rust \ - cargo \ - postgresql-dev \ - libffi-dev - -COPY chord_drs /app/chord_drs -WORKDIR /app/chord_drs - -RUN pip install --no-cache-dir -r requirements.txt && flask db upgrade - -# Run the model service server -ENTRYPOINT ["flask", "run"] diff --git a/lib/drs-server/chord_drs b/lib/drs-server/chord_drs index 8396da368..5c017d470 160000 --- a/lib/drs-server/chord_drs +++ b/lib/drs-server/chord_drs @@ -1 +1 @@ -Subproject commit 8396da368baa149a199411ff6aa9e1d1f18707f8 +Subproject commit 5c017d47009256ff69b0fd706189d85b69c37d9b diff --git a/lib/drs-server/docker-compose.yml b/lib/drs-server/docker-compose.yml index da250e8fd..0b6cbcfdb 100644 --- a/lib/drs-server/docker-compose.yml +++ b/lib/drs-server/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.7' services: chord-drs: build: - context: $PWD/lib/drs-server + context: $PWD/lib/drs-server/chord_drs args: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" From 5b66f95e0400f317b4dd97c7f2795f6c28391341 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 27 Aug 2021 18:10:38 -0700 Subject: [PATCH 017/236] Hotfix/submodules redux (#95) * update htsget_app pointer * forgot to commit actual gitmodules file --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 5659de025..fce5a091f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,7 +6,7 @@ url = https://github.com/canDIG/htsget_app [submodule "lib/drs-server/chord_drs"] path = lib/drs-server/chord_drs - url = https://github.com/c3g/chord_drs.git + url = https://github.com/CanDIG/chord_drs.git [submodule "lib/federation-service/federation_service"] path = lib/federation-service/federation_service url = https://github.com/CanDIG/federation_service @@ -21,7 +21,7 @@ url = https://github.com/CanDIG/datasets_service [submodule "lib/chord-metadata/chord_metadata_service"] path = lib/chord-metadata/chord_metadata_service - url = https://github.com/c3g/chord_metadata_service.git + url = https://github.com/CanDIG/katsu.git [submodule "lib/cancogen-dashboard/cancogen_dashboard"] path = lib/cancogen-dashboard/cancogen_dashboard url = https://github.com/CanDIG/cancogen_dashboard.git From abae90f20f01d3e5c87f0c8ad1af13e8bfe8a571 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 27 Aug 2021 18:14:59 -0700 Subject: [PATCH 018/236] Hotfix/submodules redux redux (#96) * update htsget_app pointer * forgot to commit actual gitmodules file * update links for chord_metadata --- lib/chord-metadata/Dockerfile | 48 ----------------------- lib/chord-metadata/chord_metadata_service | 2 +- lib/chord-metadata/docker-compose.yml | 2 +- 3 files changed, 2 insertions(+), 50 deletions(-) delete mode 100644 lib/chord-metadata/Dockerfile diff --git a/lib/chord-metadata/Dockerfile b/lib/chord-metadata/Dockerfile deleted file mode 100644 index 8ea7586fc..000000000 --- a/lib/chord-metadata/Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ -ARG venv_python -ARG alpine_version -FROM python:${venv_python}-alpine${alpine_version} - -LABEL Maintainer="CanDIG Project" - -ENV PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PYTHONHASHSEED=random \ - PIP_NO_CACHE_DIR=off \ - PIP_DISABLE_PIP_VERSION_CHECK=on - -RUN adduser -h /app -D user - -RUN apk add --no-cache \ - autoconf \ - automake \ - bash \ - build-base \ - bzip2-dev \ - curl \ - curl-dev \ - gcc \ - git \ - libcurl \ - libffi-dev \ - libressl-dev \ - linux-headers \ - make \ - musl-dev \ - perl \ - rust \ - cargo \ - postgresql-dev \ - postgresql-libs \ - xz-dev \ - yaml-dev \ - zlib-dev - -USER user -WORKDIR /app - -COPY chord_metadata_service /app/chord_metadata_service - -WORKDIR /app/chord_metadata_service -RUN pip install --no-cache-dir -r requirements.txt - -ENTRYPOINT ["python", "manage.py", "runserver"] diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 351398253..0b83be602 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 3513982539e45315b94019b1a6ec7406b3935d47 +Subproject commit 0b83be602288d2eb065545a003e5f6ec5db549ba diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 223d33c30..919277a73 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.7' services: chord-metadata: build: - context: $PWD/lib/chord-metadata + context: $PWD/lib/chord-metadata/chord_metadata args: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" From a7f788c9dd982d1f9158e12a1d1d538b41fcdf38 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 14 Sep 2021 18:54:56 -0700 Subject: [PATCH 019/236] quick fix: correct name of htsget repo --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index fce5a091f..d59f89dcd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/DataBiosphere/toil.git [submodule "lib/htsget-server/htsget_app"] path = lib/htsget-server/htsget_app - url = https://github.com/canDIG/htsget_app + url = https://github.com/CanDIG/htsget_app.git [submodule "lib/drs-server/chord_drs"] path = lib/drs-server/chord_drs url = https://github.com/CanDIG/chord_drs.git From 48744000c1b0dea7d75cd97f1c4ef9bb9045245f Mon Sep 17 00:00:00 2001 From: daisie_local Date: Thu, 14 Oct 2021 12:27:10 -0700 Subject: [PATCH 020/236] update htsget-server to stable --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index b4b65aff2..47385de08 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit b4b65aff20718f706925033d6ea7505e6ea0c595 +Subproject commit 47385de080d1c599766dfa8621901720716817a9 From 0755e935b0157cc5176418a4c2ae2cd1523b719a Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 14 Oct 2021 12:28:17 -0700 Subject: [PATCH 021/236] update htsget-server to stable (#98) --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index b4b65aff2..47385de08 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit b4b65aff20718f706925033d6ea7505e6ea0c595 +Subproject commit 47385de080d1c599766dfa8621901720716817a9 From 791a3ce2c443aca17ef02267d78f98abdbfb2f8d Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 10 Nov 2021 22:06:23 -0800 Subject: [PATCH 022/236] update chord-metadata to use secrets file for password (#100) * update htsget-server to stable * fix typo in chord-metadata/docker-compose * add shared-data to lib/compose/docker-compose * add env POSTGRES_PASSWORD_FILE --- lib/chord-metadata/docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 919277a73..5097eb921 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.7' services: chord-metadata: build: - context: $PWD/lib/chord-metadata/chord_metadata + context: $PWD/lib/chord-metadata/chord_metadata_service args: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" @@ -35,6 +35,7 @@ services: - CHORD_DEBUG=${CHORD_METADATA_DEBUG} - POSTGRES_HOST=metadata-db - POSTGRES_USER=admin + - POSTGRES_PASSWORD_FILE=/run/secrets/metadata_db_secret secrets: - source: metadata-settings target: /app/chord_metadata_service/metadata/settings.py @@ -81,6 +82,7 @@ services: - POSTGRES_USER=admin - POSTGRES_DB=metadata - POSTGRES_PASSWORD_FILE=/run/secrets/metadata_db_secret + - POSTGRES_HOST_AUTH_METHOD=password secrets: - source: metadata-db-user target: metadata_db_user From e38dd73f7cddd54af96912da53694808414763a2 Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Mon, 15 Nov 2021 13:03:21 -0500 Subject: [PATCH 023/236] Post Auth Merge Fixes (#91) * fixing authx-down command * container_name cleanup * DIG-515 : authentication refactoring * DIG-515: authz * DIG-512 * DIG-513 * DIG-510 * DIG-511 + external compose volumes * update (conda): settings that allow for conda env setup without intervention DIG-633 DIG-633 * refactor (conda): use common variable for CONDA path * Add Authentication Tools - Tyk and Keycloak (#99) * feature (vagrant): add IP address option to Vagrantfile * feature (authx): add keycloak to the setup launch * feature (authx): minor formatting for keycloak scripts * refactor (authx): KEYCLOAK_SERVICE* to KEYCLOAK* * feature (authx): CHECKPOINT in case of fire * feature (authx): add + as exclusion in makefile secret generator * feature (authx): fix tyk confs url * feature (authx): remove candig-server from authx makefile because it is already launched; add image removal in cleanup; DIG-633 * feature (authx): fix tyk redirect uri instead of candig server in keycloak client redirect uri settings * feature (authx): Tyk api redirect works * docs (authx): document steps, and a todo * refactor (authx): renames TEMP_KEYCLOAK.. to KEYCLOACL...PROD because thats the purpose of that URL * feature (authx): analytics for tyk * feature (authx): remove check for local idp for now * feature (authx): add warning comments * feature (authx): add directory cleanup for tyk tmp * feature (authx): add directory cleanup for tyk tmp * docs (authx): adds new api section because we need to convey that * docs (authx): steps to add new api * feature (authx): add `tee` to logfile * feature (authx): fix failing incorrect health checks for containers * chore (authx): bumps up version of tyk and redis * fix (authx): remove repeat line * fix (authx): indentation should be tabs, not 4 spaces in Make * fix (authx): remove arbiter Co-authored-by: Brennan Brouillette Co-authored-by: Amanjeev Sethi --- .gitignore | 16 +- Makefile | 302 ++++++++---------- Makefile.authx | 56 ++++ README.md | 3 +- Vagrantfile | 20 +- docs/authx-setup.md | 53 +++ docs/authz-setup.md | 10 - docs/install-docker.md | 9 +- docs/install-vagrant.md | 2 + etc/env/example.env | 69 ++-- etc/setup/scripts/subtasks/arbiter_setup.sh | 24 -- etc/setup/scripts/subtasks/keycloak_setup.sh | 222 ------------- etc/setup/scripts/subtasks/opa_setup.sh | 4 +- etc/setup/scripts/subtasks/tyk_setup.sh | 62 ---- .../keycloak/configuration/secrets.env.tpl | 3 - etc/tests/integration/authx/conftest.py | 2 +- .../authx/src/test_authentication.py | 4 + .../integration/authx/src/test_logins.py | 1 + lib/authentication/docker-compose.yml | 107 ------- lib/authentication/keycloak/Dockerfile | 4 - lib/authentication/tyk/Dockerfile | 15 - lib/authorization/arbiter/Dockerfile | 16 - lib/authorization/arbiter/requirements.txt | 2 - lib/authorization/arbiter/server.py | 204 ------------ .../candig-server.policy.rego.tpl | 2 +- lib/candig-server/docker-compose.yml | 41 +-- lib/compose/docker-compose.yml | 15 + lib/keycloak/Dockerfile | 4 + .../application-users.properties | 0 .../logging.properties | 0 .../mgmt-users.properties | 0 .../standalone-ha.xml | 0 .../configuration_templates}/standalone.xml | 0 lib/keycloak/docker-compose.yml | 45 +++ lib/keycloak/keycloak_setup.sh | 163 ++++++++++ lib/tyk/Dockerfile | 16 + .../api_auth.json.tpl | 4 +- .../api_candig.json.tpl | 6 +- .../authMiddleware.js | 2 +- .../key_request.json.tpl | 0 .../permissionsStoreMiddleware.js | 0 .../policies.json.tpl | 0 .../tyk/configuration_templates}/tyk.conf.tpl | 4 +- .../tyk_analytics.conf.tpl | 6 +- .../configuration_templates}/virtualLogin.js | 0 .../configuration_templates}/virtualLogout.js | 0 .../configuration_templates}/virtualToken.js | 0 lib/tyk/docker-compose.yml | 60 ++++ lib/tyk/tyk_key_generation.sh | 14 + lib/tyk/tyk_setup.sh | 70 ++++ lib/{authorization => }/vault/Dockerfile | 0 .../vault-config.json.tpl | 0 .../vault-datastructure.json.tpl | 0 .../vault-entity-entitlements.json.tpl | 0 .../docker-compose.yml | 11 +- .../subtasks => lib/vault}/vault_setup.sh | 12 +- provision.sh | 2 +- setup_containers.sh | 2 +- setup_vagrant.sh | 2 +- 59 files changed, 712 insertions(+), 979 deletions(-) create mode 100644 Makefile.authx create mode 100644 docs/authx-setup.md delete mode 100644 docs/authz-setup.md delete mode 100755 etc/setup/scripts/subtasks/arbiter_setup.sh delete mode 100755 etc/setup/scripts/subtasks/keycloak_setup.sh delete mode 100755 etc/setup/scripts/subtasks/tyk_setup.sh delete mode 100644 etc/setup/templates/configs/keycloak/configuration/secrets.env.tpl delete mode 100644 lib/authentication/docker-compose.yml delete mode 100644 lib/authentication/keycloak/Dockerfile delete mode 100644 lib/authentication/tyk/Dockerfile delete mode 100644 lib/authorization/arbiter/Dockerfile delete mode 100644 lib/authorization/arbiter/requirements.txt delete mode 100644 lib/authorization/arbiter/server.py rename {etc/setup/templates/configs/opa => lib/candig-server/authorization/configuration_templates}/candig-server.policy.rego.tpl (94%) create mode 100644 lib/keycloak/Dockerfile rename {etc/setup/templates/configs/keycloak/configuration => lib/keycloak/configuration_templates}/application-users.properties (100%) rename {etc/setup/templates/configs/keycloak/configuration => lib/keycloak/configuration_templates}/logging.properties (100%) rename {etc/setup/templates/configs/keycloak/configuration => lib/keycloak/configuration_templates}/mgmt-users.properties (100%) rename {etc/setup/templates/configs/keycloak/configuration => lib/keycloak/configuration_templates}/standalone-ha.xml (100%) rename {etc/setup/templates/configs/keycloak/configuration => lib/keycloak/configuration_templates}/standalone.xml (100%) create mode 100644 lib/keycloak/docker-compose.yml create mode 100644 lib/keycloak/keycloak_setup.sh create mode 100644 lib/tyk/Dockerfile rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/api_auth.json.tpl (94%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/api_candig.json.tpl (95%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/authMiddleware.js (97%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/key_request.json.tpl (100%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/permissionsStoreMiddleware.js (100%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/policies.json.tpl (100%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/tyk.conf.tpl (98%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/tyk_analytics.conf.tpl (93%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/virtualLogin.js (100%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/virtualLogout.js (100%) rename {etc/setup/templates/configs/tyk/confs => lib/tyk/configuration_templates}/virtualToken.js (100%) create mode 100644 lib/tyk/docker-compose.yml create mode 100644 lib/tyk/tyk_key_generation.sh create mode 100755 lib/tyk/tyk_setup.sh rename lib/{authorization => }/vault/Dockerfile (100%) rename {etc/setup/templates/configs/vault => lib/vault/configuration_templates}/vault-config.json.tpl (100%) rename {etc/setup/templates/configs/vault => lib/vault/configuration_templates}/vault-datastructure.json.tpl (100%) rename {etc/setup/templates/configs/vault => lib/vault/configuration_templates}/vault-entity-entitlements.json.tpl (100%) rename lib/{authorization => vault}/docker-compose.yml (74%) rename {etc/setup/scripts/subtasks => lib/vault}/vault_setup.sh (89%) diff --git a/.gitignore b/.gitignore index 64a77a84e..9f1c588f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ #edit related *~ +.idea #data files/directories *.bam /data /tmp +/lib/*/tmp .tox #project files @@ -15,20 +17,6 @@ */miniconda3 .vagrant -# keycloak -lib/authentication/keycloak/tmp/* - -# tyk -lib/authentication/tyk/tmp/* - -# vault -lib/authorization/vault/tmp/* - -# opa -lib/candig_server/authorization/tmp/*.rego -lib/candig-server/authorization/tmp/*.rego - - # test metadata etc/tests/integration/*/__pycache__/* etc/tests/integration/*/.pytest_cache/* diff --git a/Makefile b/Makefile index 23c71f3d7..1612541b4 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,18 @@ #!make -#TODO: make debug optional # import global variables env ?= .env include $(env) +include Makefile.authx export $(shell sed 's/=.*//' $(env)) SHELL = bash DIR = $(PWD) -CONDA = $(DIR)/bin/miniconda3/condabin/conda -LOGFILE=$(DIR)/tmp/progress.txt - -ifeq ($(OVERRIDE_REGISTRY), registry-1.docker.io) - export DOCKER_REGISTRY = candig -endif -ifeq ($(OVERRIDE_REGISTRY), ghcr.io) - export DOCKER_REGISTRY = ghcr.io/candig -endif +CONDA_BASE = $(DIR)/bin/miniconda3 +CONDA = $(CONDA_BASE)/bin/conda +CONDA_ENV_SETTINGS = $(CONDA_BASE)/etc/profile.d/conda.sh +LOGFILE = $(DIR)/tmp/progress.txt .PHONY: all all: @@ -25,6 +20,7 @@ all: @echo "Type 'make help' to view available options" @echo "View README.md for additional information" + #>>> # create non-repo directories # make mkdir @@ -37,8 +33,8 @@ mkdir: mkdir -p $(DIR)/tmp/data mkdir -p $(DIR)/tmp/secrets mkdir -p $(DIR)/tmp/ssl - mkdir -p $(DIR)/tmp/authentication/{keycloak,tyk} - mkdir -p $(DIR)/tmp/authorization/vault + mkdir -p $(DIR)/tmp/{keycloak,tyk,vault} + #>>> # download all package binaries @@ -49,6 +45,7 @@ mkdir: bin-all: bin-conda bin-docker-machine bin-kompose bin-kubectl \ bin-minikube bin-minio bin-traefik bin-prometheus + #>>> # download miniconda package # make bin-conda @@ -65,8 +62,12 @@ ifeq ($(VENV_OS), darwin) https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh endif bash $(DIR)/bin/miniconda_install.sh -f -b -u -p $(DIR)/bin/miniconda3 + # init is needed to create bash aliases for conda but it won't work + # until you source the script that ships with conda + source $(CONDA_ENV_SETTINGS) && $(CONDA) init echo " finished bin-conda" >> $(LOGFILE) + #>>> # download docker-machine (for swarm deployment) # make bin-docker-machine @@ -79,6 +80,7 @@ bin-docker-machine: mkdir chmod 755 $(DIR)/bin/docker-machine echo " finished bin-docker-machine" >> $(LOGFILE) + #>>> # download kompose (for kubernetes deployment) # make bin-kompose @@ -91,6 +93,7 @@ bin-kompose: mkdir chmod 755 $(DIR)/bin/kompose echo " finished bin-kompose" >> $(LOGFILE) + #>>> # download latest kubectl (for kubernetes deployment) # make bin-kubectl @@ -103,6 +106,7 @@ bin-kubectl: mkdir chmod 755 $(DIR)/bin/kubectl echo " finished bin-kubectl" >> $(LOGFILE) + #>>> # download latest minikube binary from Google repo # make bin-minikube @@ -115,6 +119,7 @@ bin-minikube: mkdir chmod 755 $(DIR)/bin/minikube echo " finished bin-minikube" >> $(LOGFILE) + #>>> # download latest minio server/client from Minio repo # make bin-minio @@ -130,12 +135,13 @@ bin-minio: mkdir chmod 755 $(DIR)/bin/mc echo " finished bin-minio" >> $(LOGFILE) + #>>> # download prometheus binaries from Github repo # make bin-prometheus #<<< -bin-prometheus: +bin-prometheus: mkdir echo " started bin-prometheus" >> $(LOGFILE) mkdir -p $(DIR)/bin/prometheus curl -Lo $(DIR)/bin/prometheus/prometheus.tar.gz \ @@ -144,6 +150,7 @@ bin-prometheus: chmod 755 $(DIR)/bin/prometheus/prometheus echo " finished bin-prometheus" >> $(LOGFILE) + #>>> # download latest traefik binary from Github repo # make bin-traefik @@ -157,6 +164,7 @@ bin-traefik: mkdir chmod 755 $(DIR)/bin/traefik echo " finished bin-traefik" >> $(LOGFILE) + #>>> # (re)build service image and deploy/test using docker-compose # $module is the name of the sub-folder in lib/ @@ -172,6 +180,7 @@ build-%: | docker-compose -f - build $(BUILD_OPTS) echo " finished build-$*" >> $(LOGFILE) + #>>> # run all cleanup functions # WARNING: these are distructive steps, read through instructions before using @@ -183,6 +192,15 @@ clean-all: clean-stack clean-compose clean-containers clean-secrets clean-config clean-volumes clean-networks clean-images clean-swarm clean-machines \ clean-certs clean-conda clean-bin + +#>>> +# close all authentication and authorization services + +#<<< +clean-auth: + + + #>>> # clear downloaded binaries # removes $PWD/bin/ @@ -193,6 +211,7 @@ clean-all: clean-stack clean-compose clean-containers clean-secrets clean-config clean-bin: rm -rf $(DIR)/bin + #>>> # removed selfsigned-certs (including root-ca) # make clean-certs @@ -202,6 +221,7 @@ clean-bin: clean-certs: rm -f $(DIR)/tmp/ssl/selfsigned-* + #>>> # stops and removes docker-compose instances # make clean-compose @@ -213,16 +233,19 @@ clean-compose: cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml $(DIR)/lib/$(MODULE)/docker-compose.yml \ | docker-compose -f - down;) + #>>> # deactivate and remove conda env $VENV_NAME # make clean-conda + #<<< .PHONY: clean-conda clean-conda: $(CONDA) deactivate $(CONDA) env remove -n $(VENV_NAME) + #>>> # clear swarm configs and remove config files # make clean-configs @@ -233,6 +256,7 @@ clean-configs: -docker config rm `docker config ls -q` rm -rf $(DIR)/tmp/configs + #>>> # stop all running containers and remove all stopped containers # make clean-containers @@ -243,6 +267,7 @@ clean-containers: -docker stop `docker ps -q` docker container prune -f + #>>> # clear all images (including base images) # make clean-images @@ -252,6 +277,7 @@ clean-containers: clean-images: docker image prune -a -f + #>>> # shutdown kubernetes services # make clean-kubernetes @@ -263,6 +289,7 @@ clean-kubernetes: $(foreach MODULE, $(CANDIG_MODULES), --file $(DIR)/lib/$(MODULE)/docker-compose.yml) \ down + #>>> # destroy docker-machine cluster # make clean-machines @@ -272,6 +299,7 @@ clean-kubernetes: clean-machines: $(DIR)/bin/docker-machine rm -f `$(DIR)/bin/docker-machine ls -q` + #>>> # destroy minikube cluster # make clean-minikube @@ -281,6 +309,7 @@ clean-machines: clean-minikube: $(DIR)/bin/minikube delete + #>>> # remove all unused networks # make clean-networks @@ -290,6 +319,7 @@ clean-minikube: clean-networks: docker network prune -f + #>>> # clear swarm secrets and remove secret files # make clean-secrets @@ -300,6 +330,7 @@ clean-secrets: -docker secret rm `docker secret ls -q` rm -rf $(DIR)/tmp/secrets + #>>> # remove all stacks # make clean-stack @@ -309,6 +340,7 @@ clean-secrets: clean-stack: -docker stack rm `docker stack ls | awk '{print $$1}'` + #>>> # leave docker-swarm # make clean-swarm @@ -318,6 +350,7 @@ clean-stack: clean-swarm: docker swarm leave --force + #>>> # clear all tox screen sessions # make clean-tox @@ -327,6 +360,7 @@ clean-swarm: clean-tox: screen -ls | grep pts | cut -d. -f1 | awk '{print $$1}' | xargs kill + #>>> # remove all peristant volumes and local data # make clean-volumes @@ -335,7 +369,8 @@ clean-tox: .PHONY: clean-volumes clean-volumes: -docker volume rm `docker volume ls -q` - rm -rf $(DIR)/tmp/data +#rm -rf $(DIR)/tmp/data + #>>> # deploy/test all modules in $CANDIG_MODULES using docker-compose @@ -350,148 +385,6 @@ compose: # | docker-compose -f - up -#TODO: deprecate compose-authx-down -#>>> -# close all authentication and authorization services - -#<<< -compose-authx-down: - # closes primary authn and authx components - docker-compose -f ${PWD}/lib/compose/docker-compose.yml -f $(DIR)/lib/authentication/docker-compose.yml down - docker-compose -f ${PWD}/lib/compose/docker-compose.yml -f $(DIR)/lib/authorization/docker-compose.yml down - # - remove intermittent docker images - # -- authentication - docker rmi compose_keycloak:latest --force - docker rmi compose_tyk:latest --force - # -- authorization - #docker rmi compose_vault:latest --force - docker rmi compose_candig-server-authorization:latest --force - - # closes the candig server along with its corresponding arbiter and opa - docker-compose -f ${PWD}/lib/compose/docker-compose.yml -f $(DIR)/lib/candig-server/docker-compose.yml down - - -#TODO: deprecate compose-authx-clean -#>>> -# dismantle and remove all data of -# candig-server prototype instances with authentication -# and authorization services - -#<<< -compose-authx-clean: compose-authx-down - # clean keycloak - docker volume rm keycloak-data - # clean tyk - docker volume rm tyk-data - docker volume rm tyk-redis-data - # clean vault - docker volume rm vault-data - - -#>>> -# create instances of authentication and -# authorization services - -#<<< -#TODO: deprecate this method -#TODO: use init-auth to set up auth stack -#TODO: move data directories to tmp/ instead of lib/ - -compose-authx-setup: - # # sets up keycloak, tyk, vault, a candig-server-arbiter, and a candig-server-authorization - echo - - mkdir -p ${PWD}/lib/authentication/keycloak - mkdir -p ${PWD}/lib/authentication/tyk - - mkdir -p ${PWD}/lib/authorization/vault - - - # Generate dynamic environment variables - $(eval KEYCLOAK_CLIENT_ID_64=$(shell echo -n $(KEYCLOAK_CLIENT_ID) | base64)) - - # temp: in production, explicitly indicating port 443 breaks vaults internal oidc provider checks. - # simply remove the ":443 from the authentication services public url for this purpose: - if [[ ${KEYCLOAK_SERVICE_PUBLIC_URL} == *":443"* ]]; then \ - echo "option 1"; \ - $(eval TEMP_KEYCLOAK_SERVICE_PUBLIC_URL=$(shell echo ${KEYCLOAK_SERVICE_PUBLIC_URL} | sed -e 's/\(:443\)\$//g')) \ - elif [[ ${KEYCLOAK_SERVICE_PUBLIC_URL} == *":80"* ]]; then \ - echo "option 2"; \ - $(eval TEMP_KEYCLOAK_SERVICE_PUBLIC_URL=$(shell echo ${KEYCLOAK_SERVICE_PUBLIC_URL} | sed -e 's/\(:80\)\$//g')) \ - else \ - echo "option 3"; \ - $(eval TEMP_KEYCLOAK_SERVICE_PUBLIC_URL=$(shell echo ${KEYCLOAK_SERVICE_PUBLIC_URL})) \ - fi - - - # inject dynamic variables in a "Makefile-ish" way - # by chaining these calls "all in one line" so that - # variables properly get exported from each sub script - # and propogate to the next ones - export KEYCLOAK_CLIENT_ID_64=$(KEYCLOAK_CLIENT_ID_64); \ - export TEMP_KEYCLOAK_SERVICE_PUBLIC_URL=$(TEMP_KEYCLOAK_SERVICE_PUBLIC_URL); \ - \ - echo ; \ - echo "Setting up Keycloak;" ; \ - source ${PWD}/etc/setup/scripts/subtasks/keycloak_setup.sh; \ - echo ; \ - echo "Setting up Tyk;" ; \ - ${PWD}/etc/setup/scripts/subtasks/tyk_setup.sh; \ - echo ; \ - echo "Setting up Vault;" ; \ - source ${PWD}/etc/setup/scripts/subtasks/vault_setup.sh; \ - echo ; \ - echo "Setting up OPAs;" ; \ - ${PWD}/etc/setup/scripts/subtasks/opa_setup.sh ; \ - echo ; \ - echo "Setting up Arbiters;" ; \ - ${PWD}/etc/setup/scripts/subtasks/arbiter_setup.sh - - - # clean up - echo - echo "Moving temporary files to ${PWD}/tmp/authorization/*" - mkdir -p ${PWD}/tmp/configs/authentication - mkdir -p ${PWD}/tmp/configs/authorization - - cp -r ${PWD}/lib/authentication/keycloak/tmp ${PWD}/tmp/configs/authentication/keycloak/ - cp -r ${PWD}/lib/authentication/tyk/tmp ${PWD}/tmp/configs/authentication/tyk/ - - cp -r ${PWD}/lib/authorization/vault/tmp ${PWD}/tmp/configs/authorization/vault/ - cp -r ${PWD}/lib/candig-server/authorization/tmp ${PWD}/tmp/configs/authorization/candig-server - - rm -rf ${PWD}/lib/authentication/*/tmp - rm -rf ${PWD}/lib/authorization/*/tmp - rm -rf ${PWD}/lib/candig-server/authorization/tmp - - - echo - echo "-- authorization Setup Done! --" - echo - - -#>>> -# create an instance of a candig-server prototype -# with authentication and authorization services - -#<<< -#TODO: deprecate compose-authx-setup-candig-server -compose-authx-setup-candig-server: compose-authx-setup - # intended to run candig server alongside the authx modules - docker-compose -f ${DIR}/lib/compose/docker-compose.yml -f $(DIR)/lib/candig-server/docker-compose.yml up -d candig-server 2>&1 - - -#>>> -# run authentication and authorization -# tests with both chrome and firefox front-ends - -#<<< -#TODO: fix broken tests -#test-authx-prototype: - #$(DIR)/etc/tests/integration/authx/run_tests.sh 20 chrome True - #$(DIR)/etc/tests/integration/authx/run_tests.sh 20 firefox True - - #>>> # deploy/test individual modules using docker-compose # $module is the name of the sub-folder in lib/ @@ -502,9 +395,10 @@ compose-%: echo " started compose-$*" >> $(LOGFILE) cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ $(DIR)/lib/$*/docker-compose.yml \ - | docker-compose -f - up -d + | docker-compose -f - up -d $(SERVICE) echo " finished compose-$*" >> $(LOGFILE) + #>>> # create docker bridge networks # make docker-networks @@ -517,7 +411,8 @@ docker-networks: -o com.docker.network.bridge.enable_icc=false \ -o com.docker.network.bridge.name=docker_gwbridge \ -o com.docker.network.bridge.enable_ip_masquerade=true \ - docker_gwbridge + docker_gwbridge || echo "docker_gwbridge already exists..." + #>>> # pull images from $DOCKER_REGISTRY @@ -529,6 +424,7 @@ docker-pull: $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) pull-$(MODULE);) $(foreach MODULE, $(TOIL_MODULES), docker pull $(DOCKER_REGISTRY)/$(MODULE):latest;) + #>>> # push docker images to $DOCKER_REGISTRY # make docker-push @@ -537,6 +433,8 @@ docker-pull: .PHONY: docker-push docker-push: $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) push-$(MODULE);) + #$(foreach MODULE, $(TOIL_MODULES), docker push $(DOCKER_REGISTRY)/$(MODULE):latest;) + #>>> # create secrets for CanDIG services @@ -548,12 +446,20 @@ docker-secrets: minio-secrets @echo admin > $(DIR)/tmp/secrets/portainer-user $(MAKE) secret-portainer-secret $(MAKE) secret-metadata-app-secret + @echo admin > $(DIR)/tmp/secrets/metadata-db-user $(MAKE) secret-metadata-db-secret - @echo ${KEYCLOAK_ADMIN_USER} > $(DIR)/tmp/secrets/keycloak-admin-user - #TODO: use random generated pw instead of env-var - @echo ${KEYCLOAK_ADMIN_PW} > $(DIR)/tmp/secrets/keycloak-admin-password - #$(MAKE) secret-keycloak-admin-password + + @echo admin > $(DIR)/tmp/secrets/keycloak-admin-user + $(MAKE) secret-keycloak-admin-password + + @echo user > $(DIR)/tmp/secrets/keycloak-test-user + $(MAKE) secret-keycloak-test-user-password + + $(MAKE) secret-tyk-secret-key + $(MAKE) secret-tyk-node-secret-key + $(MAKE) secret-tyk-analytics-admin-key + #>>> # create persistant volumes for docker containers @@ -578,6 +484,7 @@ docker-volumes: docker volume create tyk-redis-data docker volume create vault-data + #>>> # (re)build service image for all modules # add BUILD_OPTS='--no-cache' to ignore cached builds @@ -589,6 +496,7 @@ docker-volumes: images: #toil-docker $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) build-$(MODULE);) + #>>> # initialize conda environment # make init-conda @@ -597,12 +505,20 @@ images: #toil-docker .PHONY: init-conda init-conda: echo " started init-conda" >> $(LOGFILE) - $(CONDA) create -y -n $(VENV_NAME) python=$(VENV_PYTHON) pip=$(VENV_PIP) - @echo "Load local conda: source $(DIR)/bin/miniconda3/etc/profile.d/conda.sh" - @echo "Activate conda env: conda activate $(VENV_NAME)" - @echo "Install requirements: pip install -U -r $(DIR)/etc/venv/requirements.txt" + # source conda's script to be safe, so the conda command is found + source $(CONDA_ENV_SETTINGS) \ + && $(CONDA) create -y -n $(VENV_NAME) python=$(VENV_PYTHON) pip=$(VENV_PIP) + + source $(CONDA_ENV_SETTINGS) \ + && conda activate $(VENV_NAME) \ + && pip install -U -r $(DIR)/etc/venv/requirements.txt + + #@echo "Load local conda: source $(DIR)/bin/miniconda3/etc/profile.d/conda.sh" + #@echo "Activate conda env: conda activate $(VENV_NAME)" + #@echo "Install requirements: pip install -U -r $(DIR)/etc/venv/requirements.txt" echo " finished init-conda" >> $(LOGFILE) + #>>> # initialize docker and create required docker networks, volumes, certs, secrets, and conda env # make init-docker @@ -611,6 +527,7 @@ init-conda: .PHONY: init-docker init-docker: docker-networks docker-volumes ssl-cert docker-secrets + #>>> # initialize kubernetes environment # make init-kubernetes @@ -620,6 +537,7 @@ init-docker: docker-networks docker-volumes ssl-cert docker-secrets init-kubernetes:ssl-cert docker-secrets docker-pull $(DIR)/bin/kubectl create namespace $(DOCKER_NAMESPACE) + #>>> # initialize docker-swarm environment and create swarm networks, configs, and secrets # make init-swarm @@ -628,6 +546,7 @@ init-kubernetes:ssl-cert docker-secrets docker-pull .PHONY: init-swarm init-swarm: swarm-init swarm-networks swarm-configs swarm-secrets + #>>> # deploy/test all modules in $CANDIG_MODULES using Kubernetes # make kubernetes @@ -639,6 +558,7 @@ kubernetes: $(foreach MODULE, $(CANDIG_MODULES), --file $(DIR)/lib/$(MODULE)/docker-compose.yml) \ up + #>>> # deploys individual module using kompose # $module is the name of the sub-folder in lib @@ -649,6 +569,7 @@ kube-%: $(DIR)/bin/kompose --file $(DIR)/lib/kubernetes/docker-compose.yml \ --file $(DIR)/lib/$*/docker-compose.yml up + #>>> # create docker-machine instance(s) for Docker Compose/Swarm development # NOTE: only virtualbox is supported at this time @@ -664,6 +585,7 @@ machine-%: --virtualbox-hostonly-nicpromisc "deny" --virtualbox-hostonly-nictype "82540EM" \ $* + #>>> # create minikube environment for (kubernetes) integration testing # make minikube @@ -676,6 +598,7 @@ minikube: --network-plugin cni --cni $(MINIKUBE_CNI) --driver $(MINIKUBE_DRIVER) \ --dns-domain $(CANDIG_DOMAIN) --nodes $(MINIKUBE_NODES) + #>>> # generate secrets for minio server/client # make minio-secrets @@ -688,6 +611,7 @@ minio-secrets: @echo "aws_access_key_id=`cat tmp/secrets/minio-access-key`" >> $(DIR)/tmp/secrets/aws-credentials @echo "aws_secret_access_key=`cat tmp/secrets/minio-secret-key`" >> $(DIR)/tmp/secrets/aws-credentials + #>>> # pull docker image to $DOCKER_REGISTRY # $module is the name of the sub-folder in lib/ @@ -699,6 +623,7 @@ pull-%: $(DIR)/lib/$*/docker-compose.yml \ | docker-compose -f - pull + #>>> # push docker image to $DOCKER_REGISTRY # $module is the name of the sub-folder in lib/ @@ -710,6 +635,7 @@ push-%: $(DIR)/lib/$*/docker-compose.yml \ | docker-compose -f - push + #>>> # create a random secret and add it to tmp/secrets/$secret_name # make secret-$secret_name @@ -717,7 +643,8 @@ push-%: #<<< secret-%: @dd if=/dev/urandom bs=1 count=16 2>/dev/null \ - | base64 | rev | cut -b 2- | rev | tr -d '\n\r' > $(DIR)/tmp/secrets/$* + | base64 | rev | cut -b 2- | rev | tr -d '\n\r+' > $(DIR)/tmp/secrets/$* + #>>> # generate root-ca and site ssl certs using openssl @@ -743,6 +670,7 @@ ssl-cert: -CAcreateserial -out $(DIR)/tmp/ssl/selfsigned-site.crt \ -extfile $(DIR)/etc/ssl/site.cnf -extensions server + #>>> # deploy/test all modules in $CANDIG_MODULES using docker stack # make stack @@ -752,6 +680,7 @@ ssl-cert: stack: $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) stack-$(MODULE);) + #>>> # deploy/test indivudual modules using docker stack # $module is the name of the sub-folder in lib/ @@ -764,10 +693,12 @@ stack-%: $(DIR)/lib/$*/docker-compose.yml > $(DIR)/tmp/data/docker-compose.yml docker stack deploy --compose-file $(DIR)/tmp/data/docker-compose.yml $(DOCKER_NAMESPACE) + #>>> # initialize primary docker-swarm master node # make swarm-init + #>>> # create docker configs for CanDIG services (swarm only) # configs are distributed to all swarm nodes @@ -786,6 +717,7 @@ swarm-init: @docker swarm join-token manager -q > $(DIR)/tmp/secrets/swarm-manager-token @docker swarm join-token worker -q > $(DIR)/tmp/secrets/swarm-worker-token + #>>> # join a docker swarm cluster using manager/worker token # make swarm-join @@ -796,6 +728,12 @@ swarm-join: @docker swarm join --advertise-addr $(SWARM_ADVERTISE_IP) --listen-addr $(SWARM_LISTEN_IP) \ --token `cat $(DIR)/tmp/secrets/swarm-$(SWARM_MODE)-token` $(SWARM_MANAGER_IP) + +#>>> +# create docker swarm overlay networks +# make-swarm-networks + +#<<< .PHONY: swarm-networks swarm-networks: docker network create --driver overlay --opt encrypted=true traefik-net @@ -808,19 +746,31 @@ swarm-networks: #<<< .PHONY: swarm-secrets swarm-secrets: + docker secret create aws-credentials $(DIR)/tmp/secrets/aws-credentials docker secret create minio-access-key $(DIR)/tmp/secrets/minio-access-key docker secret create minio-secret-key $(DIR)/tmp/secrets/minio-secret-key - docker secret create aws-credentials $(DIR)/tmp/secrets/aws-credentials + docker secret create portainer-user $(DIR)/tmp/secrets/portainer-user docker secret create portainer-secret $(DIR)/tmp/secrets/portainer-secret + docker secret create traefik-ssl-key $(DIR)/tmp/ssl/$(TRAEFIK_SSL_CERT).key docker secret create traefik-ssl-crt $(DIR)/tmp/ssl/$(TRAEFIK_SSL_CERT).crt + docker secret create metadata-app-secret $(DIR)/tmp/secrets/metadata-app-secret docker secret create metadata-db-user $(DIR)/tmp/secrets/metadata-db-user docker secret create metadata-db-secret $(DIR)/tmp/secrets/metadata-db-secret + docker secret create keycloak-admin-user $(DIR)/tmp/secrets/keycloak-admin-user docker secret create keycloak-admin-password $(DIR)/tmp/secrets/keycloak-admin-password + docker secret create tyk-secret-key $(DIR)/tmp/secrets/tyk-secret-key + docker secret create tyk-node-secret-key $(DIR)/tmp/secrets/tyk-node-secret-key + + # TODO: review + #docker secret create keycloak-test-password-1 $(DIR)/tmp/secrets/keycloak-test-password-1 + #docker secret create keycloak-test-password-2 $(DIR)/tmp/secrets/keycloak-test-password-2 + + #>>> # create toil images using upstream CanDIG Toil repo # make toil-docker @@ -839,6 +789,7 @@ toil-docker: $(foreach MODULE, $(TOIL_MODULES), docker push $(DOCKER_REGISTRY)/$(MODULE):latest;) echo " finished toil-docker" >> $(LOGFILE) + #>>> # deploys all modules using Tox # make tox @@ -848,6 +799,7 @@ toil-docker: tox: dotenv -f .env run tox + #>>> # deploys individual module using tox # $module is the name of the sub-folder in lib/ @@ -857,17 +809,23 @@ tox: tox-%: dotenv -f .env run tox -e $* -# test print global variables -print-%: - @echo '$*=$($*)' #>>> # view available options # make help #<<< -# Find sections of docstrings #>>> #<<< and print .PHONY: help help: +# Find sections of docstrings #>>> #<<< and print @sed -n -e '/^#>>>/,/^#<<>>/d; /^#<<>> +# test print global variables +# make print-ENV_VARIABLE + +#<<< +print-%: + @echo '$*=$($*)' diff --git a/Makefile.authx b/Makefile.authx new file mode 100644 index 000000000..57811b9e6 --- /dev/null +++ b/Makefile.authx @@ -0,0 +1,56 @@ +#>>> +# close all authentication and authorization services + +#<<< +clean-authx: + cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ + $(DIR)/lib/keycloak/docker-compose.yml | docker-compose -f - down + + cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ + $(DIR)/lib/tyk/docker-compose.yml | docker-compose -f - down + + # - remove intermittent docker images + docker rmi candigv2_keycloak:latest --force + docker rmi candig_keycloak:latest --force + + docker rmi candigv2_tyk:latest --force + docker rmi candig_tyk:latest --force + + # - clean tmp dir inside lib/tyk + rm -r $(DIR)/lib/tyk/tmp + + +#>>> +# authx, common settings + +#<<< +init-authx: mkdir + # Generate dynamic environment variables + # ========== HACK ALERT ============== + # This setup with backslashes (\) is required because Make runs each command + # in its own shell. So to pass the environment from previous commands, one has + # to do this backslash dance. This could have been resolved but we decided to + # move to a better solution so Aman made a decision to keep this hack as-is. + $(eval KEYCLOAK_CLIENT_ID_64=$(shell echo -n ${KEYCLOAK_CLIENT_ID} | base64)) + @echo $(KEYCLOAK_CLIENT_ID_64) > $(DIR)/tmp/secrets/keycloak-client-local-candig-id-64 + # temp: in production, explicitly indicating port 443 breaks vaults internal oidc provider checks. + # simply remove the ":443 from the authentication services public url for this purpose: + if [[ ${KEYCLOAK_PUBLIC_URL} == *":443"* ]]; then \ + echo "option 1"; \ + $(eval KEYCLOAK_PUBLIC_URL_PROD=$(shell echo ${KEYCLOAK_PUBLIC_URL} | sed -e 's/\(:443\)\$//g')) \ + elif [[ ${KEYCLOAK_PUBLIC_URL} == *":80"* ]]; then \ + echo "option 2"; \ + $(eval KEYCLOAK_PUBLIC_URL_PROD=$(shell echo ${KEYCLOAK_PUBLIC_URL} | sed -e 's/\(:80\)\$//g')) \ + else \ + echo "option 3"; \ + $(eval KEYCLOAK_PUBLIC_URL_PROD=$(shell echo ${KEYCLOAK_PUBLIC_URL})) \ + fi ;\ + export KEYCLOAK_CLIENT_ID_64=$(KEYCLOAK_CLIENT_ID_64); \ + export KEYCLOAK_PUBLIC_URL_PROD=$(KEYCLOAK_PUBLIC_URL_PROD); \ + echo "Setting up Keycloak" ; \ + source ${PWD}/lib/keycloak/keycloak_setup.sh; \ + export SERVICE=tyk; \ + ${PWD}/lib/tyk/tyk_setup.sh; \ + echo ; \ + $(MAKE) compose-tyk \ + ${PWD}/lib/tyk/tyk_key_generation.sh; diff --git a/README.md b/README.md index 41ca10d91..8f9e56eba 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Overview -The CanDIG v2 project is a collection of heterogeneos services designed to work together to facilitate end to end +The CanDIG v2 project is a collection of heterogeneous services designed to work together to facilitate end to end dataflow for genomic data. ```plaintext @@ -146,6 +146,7 @@ To deploy CanDIGv2, follow one of the available install guides in `docs/`: * [Docker Deployment Guide](./docs/install-docker.md) * [Kubernetes Deployment Guide](./docs/install-kubernetes.md) * [Tox Deployment Guide](./docs/install-tox.md) +* [Authentication and Authorization Deployment Guide](./docs/authx-setup.md) View additional Makefile options with `make help`. diff --git a/Vagrantfile b/Vagrantfile index db14ff12a..bfb25b094 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -13,6 +13,8 @@ end Vagrant.configure('2') do |config| config.vm.hostname = 'candig-dev' + ip_address = ENV['CUSTOM_IP'] || "192.168.33.33" + config.vm.network "private_network", ip: ip_address config.vm.provider 'virtualbox' do |vb, override| override.vm.synced_folder '.', '/home/vagrant/candig', type: 'virtualbox' @@ -24,13 +26,27 @@ Vagrant.configure('2') do |config| vb.name = 'candig-dev' vb.gui = false vb.customize ['modifyvm', :id, '--cpus', 4] - vb.customize ['modifyvm', :id, '--memory', '4096'] + vb.customize ['modifyvm', :id, '--memory', '8096'] # run custom shell on provision override.vm.provision 'shell', privileged: false, path: "provision.sh", args: ["/home/vagrant/candig"] override.vm.provision :reload override.vm.provision 'shell', privileged: false, path: "setup_containers.sh", args: ["/home/vagrant/candig"] end + config.vm.provider :libvirt do |libvirt, override| + override.vm.synced_folder '.', '/home/vagrant/candig', type: 'nfs' + override.vm.box = 'debian-sandbox/contrib-buster64' + override.vm.hostname = 'candig.local' + override.disksize.size = '50GB' + libvirt.memory = 4096 + libvirt.nested = true + libvirt.cpus = 4 + override.vm.provision 'shell', privileged: false, path: "provision.sh", args: ["/home/vagrant/candig"] + override.vm.provision :reload + override.vm.provision 'shell', privileged: false, path: "setup_containers.sh", args: ["/home/vagrant/candig"] + end + + config.vm.provider :openstack do |os, override| override.vm.synced_folder '.', '/home/vagrant/candig', type: 'virtualbox', disabled: true override.ssh.username = 'ubuntu' @@ -45,7 +61,7 @@ Vagrant.configure('2') do |config| os.openstack_auth_url = ENV["OS_AUTH_URL"] os.interface_type = ENV["OS_INTERFACE"] os.keypair_name = ENV["OS_KEYPAIR"] - + os.flavor = 'm1.large' os.image = 'UbuntuServer-1804-2019Nov20' os.server_name = 'candig-vagrant' diff --git a/docs/authx-setup.md b/docs/authx-setup.md new file mode 100644 index 000000000..65c0d0d18 --- /dev/null +++ b/docs/authx-setup.md @@ -0,0 +1,53 @@ +# CanDIGv2 Authentication and Authorization Module + +## Components + +- Keycloak +- Tyk + +## Deploy + +Make sure the relevant details in `.env` are correct. + +`make init-authx` + +## Clean + +`make clean-authx` + +## Adding New API (WIP) + +Let's say the new API is called `example` and the route it redirects to us `http://example.org`. +This section will help you figure out how to add the details to the setup but is still a work in progress. +The code to deploy a new API does not exist yet. + +- Copy an API template file like `lib/tyk/configuration_templates/api_candig.json.tpl` and give it a name. + e.g. api_example.json.tpl +- Change the appropriate pieces inside this template. +- Add the following variables to your environment file `.env` + ``` + TYK_EXAMPLE_API_ID=666 + TYK_EXAMPLE_API_NAME=Example + TYK_EXAMPLE_API_SLUG=example + TYK_EXAMPLE_API_TARGET=http://example.org + TYK_EXAMPLE_API_LISTEN_PATH=/example + ``` + See section `## Extra APIs can be added here` +- Add the new section of the API to `lib/tyk/configuration_templates/policies.json.tpl` under + the key `access_rights` +- Add the new section of the API tp `lib/tyk/configuration_templates/key_request.json.tpl` under + the key `access_rights` +- Add the new line to copy the file to the image in the `lib/tyk/Dockerfile` +- Add the new line to `envsubst` in `lib/tyk/tyk_setup.sh` (see section `# Extra APIs can be added here`) +- Redeploy the container OR use Tyk API (TODO: ask Jimmy about this) +- Regenerate the key. `lib/tyk/tyk_key_generation.sh` has clues for now. + If the environment variables needed by the `tyk_key_generation.sh` are set, then the script should work + +## Technical Debt Notes + +- This setup is flaky at best because of a myriad of styles used: +- Tyk's setup adds a `tmp` directory inside the lib/tyk which is sad because it deviates + from the repo's setup of a global `tmp` directory. +- Tyk's setup does not have a way via this repo/make to add new APIs or call key requests etc. +- Keycloak's setup `curl`s APIs with `-k` option which is insecure. +- Add commands to add new APIs (see above). \ No newline at end of file diff --git a/docs/authz-setup.md b/docs/authz-setup.md deleted file mode 100644 index 677b1d9b6..000000000 --- a/docs/authz-setup.md +++ /dev/null @@ -1,10 +0,0 @@ -## CanDIGv2 AuthZ Module - -##### Configuration -Ensure a project `.env` is properly configured. - -##### Run the module - -From the project directory; -- Run `make compose-authx-setup-candig-server` to boot and configure a `candig-server` and all of the necessary authentication and authorization containers. These include `Tyk`, `Keycloak`, `Vault`, `OPA`, and a custom wrapper called an `Arbiter` to control access levels. It mediates inbound requests based on the policies registered in `OPA` and a resource (by default, this will be a `candig-server`, see the project `.env`). -- Run `make compose-authx-clean` to shutdown the server and all authN and authZ containers and delete their docker volumes diff --git a/docs/install-docker.md b/docs/install-docker.md index 0b85bdbe3..8b346e538 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -96,17 +96,16 @@ sudo usermod -aG docker $(whoami) ```bash # 1. initialize repo and submodules -git clone -b stable https://github.com/CanDIG/CanDIGv2.git +git clone -b develop https://github.com/CanDIG/CanDIGv2.git +cd CanDIGv2 git submodule update --init --recursive # 2. copy and edit .env with your site's local configuration cp -i etc/env/example.env .env - # 3. fetch binaries and initialize candig virtualenv make bin-all make init-conda -source etc/venv/activate.sh ``` ## Create CanDIGv2 Development VM @@ -136,13 +135,13 @@ make init-docker ```bash # create images (optional) make images - # pull latest CanDIGv2 images (instead of make images) make docker-pull # deploy stack (if using docker-compose environment) -make comopose-authx-setup +make init-auth make compose +# TODO: post deploy auth configuration # push updated images to $DOCKER_REGISTRY (optional) docker login diff --git a/docs/install-vagrant.md b/docs/install-vagrant.md index f040e392c..407c78d4d 100644 --- a/docs/install-vagrant.md +++ b/docs/install-vagrant.md @@ -23,6 +23,8 @@ vagrant plugin install vagrant-reload By default, `vagrant up` will use the VirtualBox provider to create a VM on your local machine. +You can set the IP address of your virtual machine by setting the environment variable `CUSTOM_IP` (default: 192.168.33.33). + You can also use Vagrant to deploy an instance on OpenStack: * Get your credentials from your OpenStack dashboard: go to Project > API Access on the sidebar, then click the "Download OpenStack RC File" button and download the OpenStack RC File (Identity API v3). * Either run the downloaded shell script to load the required environment variables, or export them into your shell directly. diff --git a/etc/env/example.env b/etc/env/example.env index 48cf3b1d4..d11c8f8da 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,21 +2,13 @@ # - - - # site options -CANDIG_MODULES=authentication authorization weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard #wes-server jupyter igv +CANDIG_MODULES=weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard #wes-server jupyter igv authentication authorization +CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] CANDIG_DOMAIN=docker.localhost -#TODO: use only one domain for candig site CANDIG_AUTH_DOMAIN=auth.docker.localhost -# candig server -CANDIG_SERVER_VERSION=1.4.0 -CANDIG_SERVER_HOST=0.0.0.0 -CANDIG_SERVER_PORT=3000 -CANDIG_SERVER_CONTAINER_NAME=compose_candig-server_1 -CANDIG_SERVER_PUBLIC_PORT=80 -CANDIG_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PUBLIC_PORT} - # miniconda venv # options are [linux, darwin] VENV_OS=linux @@ -35,7 +27,6 @@ DOCKER_NAMESPACE=candig DOCKER_REGISTRY=candig # options are [json, fluentd] DOCKER_LOG_DRIVER=json - ALPINE_VERSION=3.13 # docker swarm @@ -204,6 +195,7 @@ CANDIG_SERVER_VERSION=1.4.0 CANDIG_SERVER_HOST=0.0.0.0 CANDIG_SERVER_PORT=3001 CANDIG_INGEST_VERSION=1.5.0 +CANDIG_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} # rnaget service RNAGET_VERSION=v0.9.4-8-g187b583 @@ -222,52 +214,44 @@ AUTHORIZATION_SERVICE_PORT=7000 # keycloak service -KEYCLOAK_VERSION=9.0.2 -#TODO: remove keycloak passwords from .env -KEYCLOAK_ADMIN_USER=default_admin_user -#TODO: use keycloak-admin-password file instead -KEYCLOAK_ADMIN_PW=default_admin_password -KEYCLOAK_TEST_USER=test_user_1 -KEYCLOAK_TEST_PW=test_password_1 -KEYCLOAK_TEST_USER_TWO=test_user_2 -KEYCLOAK_TEST_PW_TWO=test_password_2 +KEYCLOAK_VERSION=15.0.0 KEYCLOAK_REALM=candig KEYCLOAK_CLIENT_ID=local_candig KEYCLOAK_LOGIN_REDIRECT_PATH=/auth/login -KEYCLOAK_SERVICE_PUBLIC_PORT=8080 -KEYCLOAK_SERVICE_CONTAINER_PORT=8080 -KEYCLOAK_SERVICE_HOST=0.0.0.0 -KEYCLOAK_SERVICE_PUBLIC_PROTO=http -KEYCLOAK_SERVICE_PRIVATE_PROTO=http -#TODO: consolidate keycloak public and private domain -KEYCLOAK_SERVICE_PUBLIC_URL=${KEYCLOAK_SERVICE_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_SERVICE_PUBLIC_PORT} -KEYCLOAK_SERVICE_PRIVATE_URL=${KEYCLOAK_SERVICE_PRIVATE_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_SERVICE_CONTAINER_PORT} +KEYCLOAK_PORT=8080 +KEYCLOAK_CONTAINER_PORT=8080 +KEYCLOAK_HOST=0.0.0.0 +KEYCLOAK_PUBLIC_PROTO=http +KEYCLOAK_PRIVATE_PROTO=http +KEYCLOAK_PUBLIC_URL=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_PORT} +KEYCLOAK_PUBLIC_URL_PROD=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN} +KEYCLOAK_PRIVATE_URL=${KEYCLOAK_PRIVATE_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_CONTAINER_PORT} +KEYCLOAK_GENERATE_TEST_USER=0 # tyk service -TYK_VERSION=v2.9.3.1 -TYK_REDIS_VERSION=4.0.14-alpine -#TODO: remove service container names -TYK_SERVICE_CONTAINER_NAME=tyk +TYK_VERSION=v3.2 +TYK_REDIS_VERSION=5.0-alpine TYK_SERVICE_PUBLIC_PORT=5080 TYK_SERVICE_HOST=0.0.0.0 #TODO: consolidate tyk public and private domains -TYK_SERVICE_PRIVATE_URL=http://${TYK_SERVICE_CONTAINER_NAME}:${TYK_SERVICE_PUBLIC_PORT} -TYK_TARGET_URL=http://${CANDIG_ARBITER_HOST}:${CANDIG_ARBITER_SERVICE_PORT} -TYK_LOGIN_TARGET_URL=http://${TYK_SERVICE_HOST}:${TYK_SERVICE_PUBLIC_PORT} +TYK_LOGIN_TARGET_URL=http://${CANDIG_DOMAIN}:${TYK_SERVICE_PUBLIC_PORT} +TYK_ANALYTICS_FROM_EMAIL=admin@distributedgenomics.ca +TYK_ANALYTICS_FROM_NAME=CanDIG Admin TYK_LISTEN_PATH= TYK_POLICY_ID=candig_policy +## api - authentication TYK_AUTH_API_ID=11 TYK_AUTH_API_NAME=Authentication TYK_AUTH_API_SLUG=authentication +## api - candig-server (v1) TYK_CANDIG_API_ID=21 TYK_CANDIG_API_NAME=CanDIG TYK_CANDIG_API_SLUG=candig +TYK_CANDIG_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} # vault service -#TODO: remove service container names -VAULT_CONTAINER_NAME=vault VAULT_FILE_PATH="/vault/data" VAULT_TLS_DISABLE=1 VAULT_UI=true @@ -275,26 +259,19 @@ VAULT_SERVICE_PORT=8200 VAULT_SERVICE_HOST=0.0.0.0 #TODO: consolidate vault public and private domains VAULT_SERVICE_PUBLIC_URL=http://${VAULT_SERVICE_HOST}:${VAULT_SERVICE_PORT} -VAULT_SERVICE_URL=http://${VAULT_CONTAINER_NAME}:${VAULT_SERVICE_PORT} +VAULT_SERVICE_URL=http://candigv2_vault_1:${VAULT_SERVICE_PORT} ## VAULT_JWKS= ###(generated in setup.sh) # OPA OPA_VERSION=latest -#TODO: remove service container names -OPA_CONTAINER_NAME=opa OPA_PORT=8181 OPA_LOG_LEVEL=debug #TODO: consolidate opa public and private domains -OPA_URL=http://${OPA_CONTAINER_NAME}:${OPA_PORT} +OPA_URL=http://opa:${OPA_PORT} CANDIG_AUTHZ_SERVICE_PORT=8182 -# same as former candig-server port - used to relay to new c-s port -#TODO: deprecate arbiter -CANDIG_ARBITER_SERVICE_PORT=3003 -CANDIG_ARBITER_HOST=compose_candig-server-arbiter_1 - # cancogen_dashboard CANCOGEN_DASHBOARD_VERSION=v0.3.0 diff --git a/etc/setup/scripts/subtasks/arbiter_setup.sh b/etc/setup/scripts/subtasks/arbiter_setup.sh deleted file mode 100755 index 862a11347..000000000 --- a/etc/setup/scripts/subtasks/arbiter_setup.sh +++ /dev/null @@ -1,24 +0,0 @@ -#! /usr/bin/env bash -set -e - -# This script will set up all of the arbiters needed on your local CanDIGv2 cluster - - -ARBITER_IMAGES=$(echo $(docker ps | grep arbiter | wc -l)) -if [[ $ARBITER_IMAGES -eq 0 ]]; then - echo "Building Arbiter Image!" - docker build --tag ${DOCKER_REGISTRY}/arbiter:latest --build-arg venv_python=3.7 ${PWD}/lib/authorization/arbiter -fi - - - -# Verify if any arbiter containers are running -ARBITER_CONTAINERS=$(echo $(docker ps | grep arbiter | wc -l)) -echo "Number of arbiter containers running: ${ARBITER_CONTAINERS}" -if [[ $ARBITER_CONTAINERS -eq 0 ]]; then - # First arbiter of potentially many.. - echo "Booting candig server arbiter container!" - docker-compose -f ${PWD}/lib/compose/docker-compose.yml -f ${PWD}/lib/candig-server/docker-compose.yml up -d candig-server-arbiter -fi - -echo "-- Arbiter Setup Done! --" \ No newline at end of file diff --git a/etc/setup/scripts/subtasks/keycloak_setup.sh b/etc/setup/scripts/subtasks/keycloak_setup.sh deleted file mode 100755 index 0d8cd7aa8..000000000 --- a/etc/setup/scripts/subtasks/keycloak_setup.sh +++ /dev/null @@ -1,222 +0,0 @@ -#! /usr/bin/env bash -set -e - -# This script will set up a full keycloak environment on your local CanDIGv2 cluster - -usage () { - echo "Make sure to set relevant environment variables!" - echo "Current setup: " - echo "KEYCLOAK_ADMIN_USER: ${KEYCLOAK_ADMIN_USER}" - echo "KEYCLOAK_ADMIN_PW: ${KEYCLOAK_ADMIN_PW}" - echo "KEYCLOAK_TEST_USER: ${KEYCLOAK_TEST_USER}" - echo "KEYCLOAK_TEST_PW: ${KEYCLOAK_TEST_PW}" - echo "KEYCLOAK_TEST_USER: ${KEYCLOAK_TEST_USER_TWO}" - echo "KEYCLOAK_TEST_PW: ${KEYCLOAK_TEST_PW_TWO}" - echo "KEYCLOAK_SERVICE_PUBLIC_URL: ${KEYCLOAK_SERVICE_PUBLIC_URL}" - echo "KEYCLOAK_SERVICE_PUBLIC_PORT: ${KEYCLOAK_SERVICE_PUBLIC_PORT}" - echo "CANDIG_AUTH_DOMAIN: ${CANDIG_AUTH_DOMAIN}" - - echo -} - - -# Load Keycloak template (.tpl) files, populate them -# with project .env variables, and then spit -# them out to ./lib/keycloak/tmp/* - - -# echo -mkdir -p ${PWD}/lib/authentication/keycloak/tmp - -# Copy files from template configs -echo "Copying application-users.properties .." -cp ${PWD}/etc/setup/templates/configs/keycloak/configuration/application-users.properties ${PWD}/lib/authentication/keycloak/tmp/application-users.properties - -echo "Copying logging.properties .." -cp ${PWD}/etc/setup/templates/configs/keycloak/configuration/logging.properties ${PWD}/lib/authentication/keycloak/tmp/logging.properties - -echo "Copying mgmt-users.properties .." -cp ${PWD}/etc/setup/templates/configs/keycloak/configuration/mgmt-users.properties ${PWD}/lib/authentication/keycloak/tmp/mgmt-users.properties - -echo "Copying standalone.xml .." -cp ${PWD}/etc/setup/templates/configs/keycloak/configuration/standalone.xml ${PWD}/lib/authentication/keycloak/tmp/standalone.xml - -echo "Copying standalone-ha.xml .." -cp ${PWD}/etc/setup/templates/configs/keycloak/configuration/standalone-ha.xml ${PWD}/lib/authentication/keycloak/tmp/standalone-ha.xml - - - -# Verify if keycloak container is running -KEYCLOAK_CONTAINERS=$(echo $(docker ps | grep keycloak | wc -l)) -echo "Number of keycloak containers running: ${KEYCLOAK_CONTAINERS}" -if [[ $KEYCLOAK_CONTAINERS -eq 0 ]]; then - echo "Booting keycloak container!" - docker-compose -f ${PWD}/lib/compose/docker-compose.yml -f ${PWD}/lib/authentication/docker-compose.yml up -d keycloak - sleep 5 - echo ">> .. waiting for keycloak to start..." - while ! docker logs --tail 1000 $(docker ps | grep keycloak | awk '{print $1}') | grep "Undertow HTTPS listener https listening on 0.0.0.0" ; do sleep 1 ; done - echo ">> .. ready..." -fi - - -############### - -add_users() { - # CANDIG_AUTH_DOMAIN is the name of the keycloak server inside the compose network - echo "Adding ${KEYCLOAK_TEST_USER}" - docker exec ${CANDIG_AUTH_DOMAIN} /opt/jboss/keycloak/bin/add-user-keycloak.sh -u ${KEYCLOAK_TEST_USER} -p ${KEYCLOAK_TEST_PW} -r ${KEYCLOAK_REALM} - - echo "Adding ${KEYCLOAK_TEST_USER_TWO}" - docker exec ${CANDIG_AUTH_DOMAIN} /opt/jboss/keycloak/bin/add-user-keycloak.sh -u ${KEYCLOAK_TEST_USER_TWO} -p ${KEYCLOAK_TEST_PW_TWO} -r ${KEYCLOAK_REALM} - - echo "Restarting the keycloak container" - docker restart ${CANDIG_AUTH_DOMAIN} -} - -############### - -get_token () { - KEYCLOAK_ADMIN_PW=$(cat tmp/secrets/keycloak-admin-password) - BID=$(curl \ - -d "client_id=admin-cli" \ - -d "username=$KEYCLOAK_ADMIN_USER" \ - -d "password=$KEYCLOAK_ADMIN_PW" \ - -d "grant_type=password" \ - "${KEYCLOAK_SERVICE_PUBLIC_URL}/auth/realms/master/protocol/openid-connect/token" -k 2> /dev/null ) - echo ${BID} | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["access_token"])' -} - -############### - -set_realm () { - realm=$1 - - JSON='{ - "realm": "candig", - "enabled": true - }' - - curl \ - -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ - -X POST -H "Content-Type: application/json" -d "${JSON}" \ - "${KEYCLOAK_SERVICE_PUBLIC_URL}/auth/admin/realms" -k -} - - -get_realm () { - realm=$1 - - curl \ - -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ - "${KEYCLOAK_SERVICE_PUBLIC_URL}/auth/admin/realms/${realm}" -k | jq . -} - -################################# - -get_realm_clients () { - realm=$1 - - curl \ - -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ - "${KEYCLOAK_SERVICE_PUBLIC_URL}/auth/admin/realms/${realm}/clients" -k | jq -S . -} - - -################################# -set_client () { - realm=$1 - client=$2 - listen=$3 - redirect=$4 - - # Will add / to listen only if it is present - - JSON='{ - "clientId": "'"${client}"'", - "enabled": true, - "protocol": "openid-connect", - "implicitFlowEnabled": true, - "standardFlowEnabled": true, - "publicClient": false, - "redirectUris": [ - "'${CANDIG_PUBLIC_URL}${redirect}'" - ], - "attributes": { - "saml.assertion.signature": "false", - "saml.authnstatement": "false", - "saml.client.signature": "false", - "saml.encrypt": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.onetimeuse.condition": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "saml_force_name_id_format": "false" - } - }' - echo $JSON - curl \ - -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ - -X POST -H "Content-Type: application/json" -d "${JSON}" \ - "${KEYCLOAK_SERVICE_PUBLIC_URL}/auth/admin/realms/${realm}/clients" -k -} - -get_secret () { - id=$(curl -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ - ${KEYCLOAK_SERVICE_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/clients -k 2> /dev/null \ - | python3 -c 'import json,sys;obj=json.load(sys.stdin); print([l["id"] for l in obj if l["clientId"] == - "'"$KEYCLOAK_CLIENT_ID"'" ][0])') - - curl -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ - ${KEYCLOAK_SERVICE_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/clients/$id/client-secret -k 2> /dev/null |\ - python3 -c 'import json,sys;obj=json.load(sys.stdin); print(obj["value"])' -} - -get_public_key () { - curl \ - ${KEYCLOAK_SERVICE_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM} -k 2> /dev/null |\ - python3 -c 'import json,sys;obj=json.load(sys.stdin); print(obj["public_key"])' -} -################################## - -# SCRIPT START - -echo "-- Starting setup calls to keycloak --" -echo "$KEYCLOAK_ADMIN_USER $KEYCLOAK_ADMIN_PW $KEYCLOAK_SERVICE_PUBLIC_URL" - -echo ">> Getting KEYCLOAK_TOKEN .." -KEYCLOAK_TOKEN=$(get_token) -#echo ">> retrieved KEYCLOAK_TOKEN ${KEYCLOAK_TOKEN}" -echo ">> .. got it..." - -echo ">> Creating Realm ${KEYCLOAK_REALM} .." -set_realm ${KEYCLOAK_REALM} -echo ">> .. created..." - - -echo ">> Setting client KEYCLOAK_CLIENT_ID .." -set_client ${KEYCLOAK_REALM} ${KEYCLOAK_CLIENT_ID} "${TYK_LISTEN_PATH}" ${KEYCLOAK_LOGIN_REDIRECT_PATH} -echo ">> .. set..." - -echo ">> Getting KEYCLOAK_SECRET .." -export KEYCLOAK_SECRET=$(get_secret ${KEYCLOAK_REALM}) -echo "** Retrieved KEYCLOAK_SECRET as ${KEYCLOAK_SECRET} **" -echo ">> .. got it..." -echo - - -echo ">> Getting KEYCLOAK_PUBLIC_KEY .." -export KEYCLOAK_PUBLIC_KEY=$(get_public_key ${KEYCLOAK_REALM}) -echo "** Retrieved KEYCLOAK_PUBLIC_KEY as ${KEYCLOAK_PUBLIC_KEY} **" -echo ">> .. got it..." -echo - - -echo ">> Adding user .." -add_users -echo ">> .. added..." -echo - -echo ">> .. waiting for keycloak to restart..." -while ! docker logs --tail 5 ${CANDIG_AUTH_DOMAIN} | grep "Admin console listening on http://127.0.0.1:9990" ; do sleep 1 ; done -echo ">> .. ready..." diff --git a/etc/setup/scripts/subtasks/opa_setup.sh b/etc/setup/scripts/subtasks/opa_setup.sh index 19a6082df..54c0a8976 100755 --- a/etc/setup/scripts/subtasks/opa_setup.sh +++ b/etc/setup/scripts/subtasks/opa_setup.sh @@ -7,7 +7,7 @@ mkdir -p ${PWD}/lib/candig-server/authorization/tmp # policy.rego echo "Working on candig-server.policy.rego .." -envsubst < ${PWD}/etc/setup/templates/configs/opa/candig-server.policy.rego.tpl > ${PWD}/lib/candig-server/authorization/tmp/policy.rego +envsubst < ${PWD}/lib/candig-server/authorization/configuration_templates/candig-server.policy.rego.tpl > ${PWD}/lib/candig-server/authorization/tmp/policy.rego # Verify if opa container is running @@ -15,7 +15,7 @@ OPA_CONTAINERS=$(echo $(docker ps | grep candig-server-opa | wc -l)) echo "Number of candig-server_policy containers running: ${OPA_CONTAINERS}" if [[ $OPA_CONTAINERS -eq 0 ]]; then echo "Booting opa container!" - docker-compose -f ${PWD}/lib/compose/docker-compose.yml -f ${PWD}/lib/candig-server/docker-compose.yml up -d candig-server-opa + export SERVICE=candig-server-opa && make compose-candig-server sleep 5 fi diff --git a/etc/setup/scripts/subtasks/tyk_setup.sh b/etc/setup/scripts/subtasks/tyk_setup.sh deleted file mode 100755 index 927b30d68..000000000 --- a/etc/setup/scripts/subtasks/tyk_setup.sh +++ /dev/null @@ -1,62 +0,0 @@ -#! /usr/bin/env bash -set -e - -# This script will set up a full tyk environment on your local CanDIGv2 cluster - -# ** Be sure to invoke this from the Makefile at the project's root directory [CanDigV2] ** - -# Load Tyk template (.tpl) files, populate them -# with project .env variables, and then spit -# them out to ./lib/authentication/tyk/tmp/* - -mkdir -p ${PWD}/lib/authentication/tyk/tmp - -# api_auth.json -echo "Working on api_auth.json .." -envsubst < ${PWD}/etc/setup/templates/configs/tyk/confs/api_auth.json.tpl > ${PWD}/lib/authentication/tyk/tmp/api_auth.json - -# api_candig.json -echo "Working on api_candig.json .." -envsubst < ${PWD}/etc/setup/templates/configs/tyk/confs/api_candig.json.tpl > ${PWD}/lib/authentication/tyk/tmp/api_candig.json - -# policies.json -echo "Working on policies.json .." -envsubst < ${PWD}/etc/setup/templates/configs/tyk/confs/policies.json.tpl > ${PWD}/lib/authentication/tyk/tmp/policies.json - -# tyk.conf -echo "Working on tyk.conf .." -envsubst < ${PWD}/etc/setup/templates/configs/tyk/confs/tyk.conf.tpl > ${PWD}/lib/authentication/tyk/tmp/tyk.conf - -echo "Working on authMiddleware.js .." -envsubst < ${PWD}/etc/setup/templates/configs/tyk/confs/authMiddleware.js > ${PWD}/lib/authentication/tyk/tmp/authMiddleware.js - -## TODO: tyk_analytics.conf , key_request.json.tpl - - -# Copy files from template configs - -echo "Copying permissionsStoreMiddleware.js .." -cp ${PWD}/etc/setup/templates/configs/tyk/confs/permissionsStoreMiddleware.js ${PWD}/lib/authentication/tyk/tmp/permissionsStoreMiddleware.js - -echo "Copying virtualLogin.js .." -cp ${PWD}/etc/setup/templates/configs/tyk/confs/virtualLogin.js ${PWD}/lib/authentication/tyk/tmp/virtualLogin.js - -echo "Copying virtualLogout.js .." -cp ${PWD}/etc/setup/templates/configs/tyk/confs/virtualLogout.js ${PWD}/lib/authentication/tyk/tmp/virtualLogout.js - -echo "Copying virtualToken.js .." -cp ${PWD}/etc/setup/templates/configs/tyk/confs/virtualToken.js ${PWD}/lib/authentication/tyk/tmp/virtualToken.js - - -echo "-- Tyk Setup Done! --" -echo - - -# Verify if tyk container is running -TYK_CONTAINERS=$(echo $(docker ps | grep tyk | wc -l)) -echo "Number of tyk containers running: ${TYK_CONTAINERS}" -if [[ $TYK_CONTAINERS -eq 0 ]]; then - echo "Booting tyk container!" - docker-compose -f ${PWD}/lib/compose/docker-compose.yml -f ${PWD}/lib/authentication/docker-compose.yml up -d tyk - sleep 5 -fi diff --git a/etc/setup/templates/configs/keycloak/configuration/secrets.env.tpl b/etc/setup/templates/configs/keycloak/configuration/secrets.env.tpl deleted file mode 100644 index 4f9defac3..000000000 --- a/etc/setup/templates/configs/keycloak/configuration/secrets.env.tpl +++ /dev/null @@ -1,3 +0,0 @@ -KEYCLOAK_USER=${KEYCLOAK_ADMIN_USER} -KEYCLOAK_PASSWORD=${KEYCLOAK_ADMIN_PW} -###PROXY_ADDRESS_FORWARDING=${PROXY_ADDRESS_FORWARDING} diff --git a/etc/tests/integration/authx/conftest.py b/etc/tests/integration/authx/conftest.py index 8ec15b8f3..2301df7de 100644 --- a/etc/tests/integration/authx/conftest.py +++ b/etc/tests/integration/authx/conftest.py @@ -56,7 +56,7 @@ def setup(request): driver=None candig_url= os.environ["CANDIG_PUBLIC_URL"] - candigauth_url= os.environ["KEYCLOAK_SERVICE_PUBLIC_URL"] + candigauth_url= os.environ["KEYCLOAK_PUBLIC_URL"] permissions_data_store_url=os.environ['VAULT_SERVICE_PUBLIC_URL'] #temp candig_server_authz_url='http://0.0.0.0:8182/v1/data/permissions/allowed' diff --git a/etc/tests/integration/authx/src/test_authentication.py b/etc/tests/integration/authx/src/test_authentication.py index a3dab8400..17f84b4c2 100644 --- a/etc/tests/integration/authx/src/test_authentication.py +++ b/etc/tests/integration/authx/src/test_authentication.py @@ -69,6 +69,7 @@ def test_authentication_does_defend_against_hs256_alg_token_tampering_with_login # credentials u1 = os.environ["KEYCLOAK_TEST_USER"] + #TODO: fix , retrieve PW from secret file instead of env p1 = os.environ["KEYCLOAK_TEST_PW"] login(self.driver, u1, p1) @@ -137,6 +138,7 @@ def test_authentication_does_defend_against_none_alg_token_tampering_with_login( # credentials u1 = os.environ["KEYCLOAK_TEST_USER"] + #TODO: fix , retrieve PW from secret file instead of env p1 = os.environ["KEYCLOAK_TEST_PW"] login(self.driver, u1, p1) @@ -189,6 +191,7 @@ def test_authentication_does_defend_against_non_valid_signature_bruteforce_with_ # credentials u1 = os.environ["KEYCLOAK_TEST_USER"] + #TODO: fix , retrieve PW from secret file instead of env p1 = os.environ["KEYCLOAK_TEST_PW"] login(self.driver, u1, p1) @@ -234,6 +237,7 @@ def test_authentication_does_defend_against_secret_key_bruteforce_with_login(sel # credentials u1 = os.environ["KEYCLOAK_TEST_USER"] + #TODO: fix , retrieve PW from secret file instead of env p1 = os.environ["KEYCLOAK_TEST_PW"] login(self.driver, u1, p1) diff --git a/etc/tests/integration/authx/src/test_logins.py b/etc/tests/integration/authx/src/test_logins.py index 13ac3bf8d..ff1dd9f8d 100644 --- a/etc/tests/integration/authx/src/test_logins.py +++ b/etc/tests/integration/authx/src/test_logins.py @@ -35,6 +35,7 @@ def test_login_user1(self): # credentials u1 = os.environ["KEYCLOAK_TEST_USER"] + #TODO: fix , retrieve PW from secret file instead of env p1 = os.environ["KEYCLOAK_TEST_PW"] login(self.driver, u1, p1) diff --git a/lib/authentication/docker-compose.yml b/lib/authentication/docker-compose.yml deleted file mode 100644 index 696682e57..000000000 --- a/lib/authentication/docker-compose.yml +++ /dev/null @@ -1,107 +0,0 @@ -version: '3.7' - -#TODO: rewrite docker-compose for authentication so it conforms with spec in lib/templates/docker-compose.yml -services: - keycloak: - build: - context: ${PWD}/lib/authentication/keycloak - args: - - BASE_IMAGE=jboss/keycloak:${KEYCLOAK_VERSION} - container_name: ${CANDIG_AUTH_DOMAIN} - #TODO: define image: tag - command: ["-b", "${KEYCLOAK_SERVICE_HOST}", "-Dkeycloak.migration.strategy=IGNORE_EXISTING"] - ports: - - "${KEYCLOAK_SERVICE_CONTAINER_PORT}:8080" - networks: - - ${DOCKER_NET} - volumes: - - keycloak-data:/opt/jboss/keycloak/standalone - deploy: - placement: - constraints: - - node.role == manager - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.keycloak.rule=Host(`auth.${CANDIG_DOMAIN}`)" - - "traefik.http.services.keycloak.loadbalancer.server.port=${KEYCLOAK_SERVICE_CONTAINER_PORT}" - #TODO: fix logging - #logging: *default-logging - environment: - - KEYCLOAK_USER_FILE=/run/secrets/keycloak-admin-user - - KEYCLOAK_PASSWORD_FILE=/run/secrets/keycloak-admin-password - secrets: - - source: keycloak-admin-user - target: /run/secrets/keycloak-admin-user - - source: keycloak-admin-password - target: /run/secrets/keycloak-admin-password - healthcheck: - test: ["CMD", "curl", "-f", "http://0.0.0.0:${KEYCLOAK_SERVICE_CONTAINER_PORT}"] - interval: 30s - timeout: 20s - retries: 3 - - tyk: - #container_name: tyk - build: - context: ${PWD}/lib/authentication/tyk - args: - - BASE_IMAGE=tykio/tyk-gateway:${TYK_VERSION} - networks: - - ${DOCKER_NET} - ports: - - "${TYK_SERVICE_PUBLIC_PORT}:8080" - deploy: - placement: - constraints: - - node.role == manager - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.tyk.rule=Host(`tyk.${CANDIG_DOMAIN}`)" - - "traefik.http.services.tyk.loadbalancer.server.port=${TYK_SERVICE_PUBLIC_PORT}" - #logging: *default-logging - volumes: - - tyk-data:/opt/tyk-gateway - depends_on: - - tyk-redis - healthcheck: - test: ["CMD", "curl", "-f", "http://0.0.0.0:${TYK_SERVICE_PUBLIC_PORT}"] - interval: 30s - timeout: 20s - retries: 3 - - tyk-redis: - #container_name: tyk-redis - image: redis:${TYK_REDIS_VERSION} - networks: - - ${DOCKER_NET} - volumes: - - tyk-redis-data:/data - ports: - - "6379:6379" - deploy: - placement: - constraints: - - node.role == manager - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - #logging: *default-logging - healthcheck: - test: ["CMD", "curl", "-f", "http://0.0.0.0:6379"] - interval: 30s - timeout: 20s - retries: 3 diff --git a/lib/authentication/keycloak/Dockerfile b/lib/authentication/keycloak/Dockerfile deleted file mode 100644 index 3ebac7f43..000000000 --- a/lib/authentication/keycloak/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -ARG BASE_IMAGE -FROM ${BASE_IMAGE} - -COPY ./tmp/* /opt/jboss/keycloak/standalone/ \ No newline at end of file diff --git a/lib/authentication/tyk/Dockerfile b/lib/authentication/tyk/Dockerfile deleted file mode 100644 index 7b598c886..000000000 --- a/lib/authentication/tyk/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -ARG BASE_IMAGE -FROM ${BASE_IMAGE} - -#TODO: add maintainer labels - -#TODO: fix this image build -COPY ./tmp/authMiddleware.js /opt/tyk-gateway/middleware/authMiddleware.js -COPY ./tmp/permissionsStoreMiddleware.js /opt/tyk-gateway/middleware/permissionsStoreMiddleware.js -COPY ./tmp/tyk.conf /opt/tyk-gateway/tyk.conf -COPY ./tmp/virtualLogin.js /opt/tyk-gateway/middleware/virtualLogin.js -COPY ./tmp/virtualLogout.js /opt/tyk-gateway/middleware/virtualLogout.js -COPY ./tmp/virtualToken.js /opt/tyk-gateway/middleware/virtualToken.js -COPY ./tmp/api_candig.json /opt/tyk-gateway/apps/api_candig.json -COPY ./tmp/api_auth.json /opt/tyk-gateway/apps/api_auth.json -COPY ./tmp/policies.json /opt/tyk-gateway/policies/policies.json diff --git a/lib/authorization/arbiter/Dockerfile b/lib/authorization/arbiter/Dockerfile deleted file mode 100644 index ef195e9ce..000000000 --- a/lib/authorization/arbiter/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -ARG venv_python - -FROM python:${venv_python}-buster - -LABEL Maintainer="CanDIG Project" - -RUN mkdir -p /arbiter - -COPY server.py /arbiter -COPY requirements.txt /arbiter - -WORKDIR /arbiter - -RUN pip install -r requirements.txt - -ENTRYPOINT ["python3", "-u", "/arbiter/server.py"] diff --git a/lib/authorization/arbiter/requirements.txt b/lib/authorization/arbiter/requirements.txt deleted file mode 100644 index 93d76e98b..000000000 --- a/lib/authorization/arbiter/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -aiohttp -requests diff --git a/lib/authorization/arbiter/server.py b/lib/authorization/arbiter/server.py deleted file mode 100644 index 0274e7e25..000000000 --- a/lib/authorization/arbiter/server.py +++ /dev/null @@ -1,204 +0,0 @@ -import json -import requests - -import os -import time, threading - -import asyncio -from aiohttp import web - -# References: -# https://stackoverflow.com/questions/8600161/executing-periodic-actions-in-python - - -# --- environment variables -try: - mode = os.environ["ARBITER_MODE"] # debug | prod -except Exception as e: - #print(e) - mode="prod" - print(f"Default Running Mode : {mode}") - #raise e - -try: - port = os.environ["ARBITER_INTERNAL_PORT"] -except Exception as e: - port="3002" - print(f"Default Port : {port}") - -try: - resource_authz_host = os.environ["RESOURCE_AUTHZ_HOST"] -except Exception as e: - resource_authz_host="0.0.0.0" - print(f"Default Resource Authz Host : {resource_authz_host}") - -try: - resource_authz_port = os.environ["RESOURCE_AUTHZ_PORT"] -except Exception as e: - resource_authz_port="8181" - print(f"Default Resource Authz Port : {resource_authz_port}") - -try: - resource_host = os.environ["RESOURCE_HOST"] -except Exception as e: - resource_host="0.0.0.0" - print(f"Default Resource Host : {resource_host}") - -try: - resource_port = os.environ["RESOURCE_PORT"] -except Exception as e: - resource_port="3001" - print(f"Default Resource Port : {resource_port}") - - -try: - permissions_store_keys_url = os.environ["PERMISSIONS_STORE_URL"] + "/v1/identity/oidc/.well-known/keys" -except Exception as e: - permissions_store_keys_url="http://vault:8200/v1/identity/oidc/.well-known/keys" - print(f"Default Permissions Store URL : {permissions_store_keys_url}") - - - -print(f"Sources: {resource_authz_host}:{resource_authz_port}, {resource_host}:{resource_port}, {permissions_store_keys_url}") - - -authz_url=f"http://{resource_authz_host}:{resource_authz_port}/v1/data/permissions/allowed" - -# --- - - - -authZ_jwks = "" -def refresh_vault_jwks(): - print(time.ctime()) - - try: - # get public jwks from permissions store - res = requests.get(permissions_store_keys_url).json() - - if 'keys' not in res : - raise Exception("Permissions store error") - else : - global authZ_jwks - - candidate = str(res).replace("'", "\"") # ensure double quotes are used. OPA complains otherwise - - if candidate != authZ_jwks: - print("Discovered new Vault JWKS! Updating...") - - if mode=="debug": - print(f"[DEBUG] {res}") - - authZ_jwks = candidate - - except Exception as e: - print(e) - raise e - - threading.Timer(60, refresh_vault_jwks).start() - -refresh_vault_jwks() - -@asyncio.coroutine -async def handle(request): - - try: - authN_token_header = request.headers['Authorization'] - authZ_token_header = request.headers['X-CanDIG-Authz'] - except Exception as e: - print(e) - return web.HTTPInternalServerError(body=json.dumps({'error': 'Authorization Error'})) - - authN_token = authN_token_header - authZ_token = authZ_token_header - - - # split from 'Bearer ' - if "Bearer " in authN_token: - authN_token = authN_token.split()[1] - - if "Bearer " in authZ_token: - authZ_token = authZ_token.split()[1] - - - print(f"Path: {request.path}") - - - - if mode=="debug": - print(f"[DEBUG] Found token: {authN_token}") - print(f"[DEBUG] Found token: {authZ_token}") - - - # reach resource authz server - opa_request = { - "input" : { - "kcToken" : authN_token, - "vaultToken": authZ_token, - "authZjwks": authZ_jwks - } - } - - try: - response = requests.post(authz_url, json=opa_request) - # check response - allow = response.json() - if 'code' in allow and allow['code'] == 'internal_error': - return web.HTTPInternalServerError(body=json.dumps({'error': f"Resource authz agent error: {json.dumps(allow)}"})) - - except Exception as e: - print(e) - return web.HTTPInternalServerError(body=json.dumps({'error': 'Resource authz agent unreachable'})) - - if 'result' in allow : - if allow['result'] == True: - # forward request to resource server - try: - url=f"http://{resource_host}:{resource_port}{request.path}" - - print(f"Calling URL : {url} using method {request.method}") - - if request.method == "GET" : - # simply get resource - resource = requests.get(url) - else: - # assume json payload from inbound request - payload = await request.text() - - # relay payload to resource - resource = requests.post(url, data=payload) - - #print(f'resource returned status {resource}') - - # naively return all headers and all content - return web.Response(headers=resource.headers, body=resource.content) - - except Exception as e: - print(e) - return web.HTTPUnauthorized(body=json.dumps({'error': 'Unknown'})) - else: - return web.HTTPUnauthorized(body=json.dumps({'error': 'Access Denied'})) - else: - return web.HTTPInternalServerError(body=json.dumps({'error': 'Resource authz agent misconfigured'})) - - -@asyncio.coroutine -def init(loop): - app = web.Application(loop=loop) - - # accept all GET and POST calls with any path - app.router.add_route('GET', '/{tail:.*}', handle) - app.router.add_route('POST', '/{tail:.*}', handle) - - # start server - srv = yield from loop.create_server(app.make_handler(), '0.0.0.0', port) - print(f"Server started at http://0.0.0.0:{port}") - return srv - -loop = asyncio.get_event_loop() -loop.run_until_complete(init(loop)) - -try: - loop.run_forever() -except KeyboardInterrupt: - pass \ No newline at end of file diff --git a/etc/setup/templates/configs/opa/candig-server.policy.rego.tpl b/lib/candig-server/authorization/configuration_templates/candig-server.policy.rego.tpl similarity index 94% rename from etc/setup/templates/configs/opa/candig-server.policy.rego.tpl rename to lib/candig-server/authorization/configuration_templates/candig-server.policy.rego.tpl index 3be644d7d..07791bb7b 100644 --- a/etc/setup/templates/configs/opa/candig-server.policy.rego.tpl +++ b/lib/candig-server/authorization/configuration_templates/candig-server.policy.rego.tpl @@ -6,7 +6,7 @@ now := time.now_ns()/1000000000 default allowed = false -default iss = "${TEMP_KEYCLOAK_SERVICE_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" +default iss = "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}" default aud = "${KEYCLOAK_CLIENT_ID}" default full_authn_pk=`-----BEGIN PUBLIC KEY----- diff --git a/lib/candig-server/docker-compose.yml b/lib/candig-server/docker-compose.yml index e0610407b..f4f1b93e1 100644 --- a/lib/candig-server/docker-compose.yml +++ b/lib/candig-server/docker-compose.yml @@ -27,8 +27,7 @@ services: - "traefik.docker.lbswarm=true" - "traefik.http.routers.candig-server.rule=Host(`candig-server.${CANDIG_DOMAIN}`)" - "traefik.http.services.candig-server.loadbalancer.server.port=${CANDIG_SERVER_PORT}" - #TODO: fix logging - #logging: *default-logging + logging: *default-logging command: ["--host", "0.0.0.0", "--port", "3000"] candig-server-opa: @@ -37,7 +36,6 @@ services: args: - BASE_IMAGE=openpolicyagent/opa:${OPA_VERSION} image: ${DOCKER_REGISTRY}/candig-server-opa:${CANDIG_SERVER_VERSION} - #container_name: ${CANDIG_SERVER_AUTHORIZATION_CONTAINER_NAME} networks: - ${DOCKER_NET} ports: @@ -56,8 +54,7 @@ services: - "traefik.docker.lbswarm=true" - "traefik.http.routers.candig-server-opa.rule=Host(`candig-server-opa.${CANDIG_DOMAIN}`)" - "traefik.http.services.candig-server-opa.loadbalancer.server.port=${CANDIG_SERVER_PORT}" - #TODO: fix logging - #logging: *default-logging + logging: *default-logging command: - "run" - "--server" @@ -68,37 +65,3 @@ services: interval: 30s timeout: 20s retries: 3 - - candig-server-arbiter: - image: ${DOCKER_REGISTRY}/arbiter:latest - #container_name: ${CANDIG_ARBITER_HOST} - networks: - - ${DOCKER_NET} - ports: - - "${CANDIG_ARBITER_SERVICE_PORT}:${CANDIG_ARBITER_SERVICE_PORT}" - environment: - - ARBITER_MODE=prod - - ARBITER_INTERNAL_PORT=${CANDIG_ARBITER_SERVICE_PORT} - - RESOURCE_AUTHZ_HOST=${CANDIG_SERVER_AUTHORIZATION_CONTAINER_NAME} - - RESOURCE_AUTHZ_PORT=8181 - - RESOURCE_HOST=${CANDIG_SERVER_CONTAINER_NAME} - - RESOURCE_PORT=${CANDIG_SERVER_PORT} - - PERMISSIONS_STORE_URL=${VAULT_SERVICE_URL} - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.candig-server-arbiter.rule=Host(`candig-server-arbiter.${CANDIG_DOMAIN}`)" - - "traefik.http.services.candig-server-arbiter.loadbalancer.server.port=${CANDIG_SERVER_PORT}" - #TODO: fix logging - #logging: *default-logging - command: ["--host", "0.0.0.0", "--port", "${CANDIG_ARBITER_SERVICE_PORT}"] - diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index 98c2be2f5..e9d633db7 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -4,25 +4,40 @@ networks: bridge: external: true bridge-net: + external: true ingress: traefik-net: agent-net: volumes: datasets-data: + external: true minio-data: + external: true minio-config: + external: true mc-config: + external: true toil-jobstore: + external: true portainer-data: + external: true prometheus-data: + external: true consul-data: + external: true grafana-data: + external: true traefik-data: + external: true keycloak-data: + external: true tyk-data: + external: true tyk-redis-data: + external: true vault-data: + external: true secrets: aws-credentials: diff --git a/lib/keycloak/Dockerfile b/lib/keycloak/Dockerfile new file mode 100644 index 000000000..34290e575 --- /dev/null +++ b/lib/keycloak/Dockerfile @@ -0,0 +1,4 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} + +COPY ./configuration_templates/* /opt/jboss/keycloak/standalone/ \ No newline at end of file diff --git a/etc/setup/templates/configs/keycloak/configuration/application-users.properties b/lib/keycloak/configuration_templates/application-users.properties similarity index 100% rename from etc/setup/templates/configs/keycloak/configuration/application-users.properties rename to lib/keycloak/configuration_templates/application-users.properties diff --git a/etc/setup/templates/configs/keycloak/configuration/logging.properties b/lib/keycloak/configuration_templates/logging.properties similarity index 100% rename from etc/setup/templates/configs/keycloak/configuration/logging.properties rename to lib/keycloak/configuration_templates/logging.properties diff --git a/etc/setup/templates/configs/keycloak/configuration/mgmt-users.properties b/lib/keycloak/configuration_templates/mgmt-users.properties similarity index 100% rename from etc/setup/templates/configs/keycloak/configuration/mgmt-users.properties rename to lib/keycloak/configuration_templates/mgmt-users.properties diff --git a/etc/setup/templates/configs/keycloak/configuration/standalone-ha.xml b/lib/keycloak/configuration_templates/standalone-ha.xml similarity index 100% rename from etc/setup/templates/configs/keycloak/configuration/standalone-ha.xml rename to lib/keycloak/configuration_templates/standalone-ha.xml diff --git a/etc/setup/templates/configs/keycloak/configuration/standalone.xml b/lib/keycloak/configuration_templates/standalone.xml similarity index 100% rename from etc/setup/templates/configs/keycloak/configuration/standalone.xml rename to lib/keycloak/configuration_templates/standalone.xml diff --git a/lib/keycloak/docker-compose.yml b/lib/keycloak/docker-compose.yml new file mode 100644 index 000000000..ec22aa714 --- /dev/null +++ b/lib/keycloak/docker-compose.yml @@ -0,0 +1,45 @@ +version: '3.7' + +services: + keycloak: + build: + context: ${PWD}/lib/keycloak + args: + - BASE_IMAGE=jboss/keycloak:${KEYCLOAK_VERSION} + container_name: ${CANDIG_AUTH_DOMAIN} + #TODO: define image: tag + command: [ "-b", "${KEYCLOAK_HOST}", "-Dkeycloak.migration.strategy=IGNORE_EXISTING" ] + ports: + - "${KEYCLOAK_CONTAINER_PORT}:8080" + networks: + - ${DOCKER_NET} + volumes: + - keycloak-data:/opt/jboss/keycloak/standalone + deploy: + placement: + constraints: + - node.role == manager + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + labels: + - "traefik.enable=true" + - "traefik.docker.lbswarm=true" + - "traefik.http.routers.keycloak.rule=Host(`auth.${CANDIG_DOMAIN}`)" + - "traefik.http.services.keycloak.loadbalancer.server.port=${KEYCLOAK_CONTAINER_PORT}" + logging: *default-logging + environment: + - KEYCLOAK_USER_FILE=/run/secrets/keycloak-admin-user + - KEYCLOAK_PASSWORD_FILE=/run/secrets/keycloak-admin-password + secrets: + - source: keycloak-admin-user + target: /run/secrets/keycloak-admin-user + - source: keycloak-admin-password + target: /run/secrets/keycloak-admin-password + healthcheck: + test: [ "CMD", "curl", "-f", "http://0.0.0.0:${KEYCLOAK_CONTAINER_PORT}" ] + interval: 30s + timeout: 20s + retries: 3 \ No newline at end of file diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh new file mode 100644 index 000000000..3853eb023 --- /dev/null +++ b/lib/keycloak/keycloak_setup.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +LOGFILE=$PWD/tmp/progress.txt + + +# Verify if keycloak container is running +KEYCLOAK_CONTAINERS=$(echo "$(docker ps | grep keycloak | wc -l)") +echo "Number of keycloak containers running: ${KEYCLOAK_CONTAINERS}" | tee -a $LOGFILE +if [[ $KEYCLOAK_CONTAINERS -eq 0 ]]; then + echo "Booting keycloak container!" | tee -a $LOGFILE + export SERVICE=keycloak && make compose-keycloak + sleep 5 + echo "Waiting for keycloak to start" | tee -a $LOGFILE + while ! docker logs --tail 1000 "$(docker ps | grep keycloak | awk '{print $1}')" | grep "Undertow HTTPS listener https listening on 0.0.0.0"; do sleep 1; done + echo "Keycloak container started." | tee -a $LOGFILE +fi + +add_user() { + # CANDIG_AUTH_DOMAIN is the name of the keycloak server inside the compose network + local username=$1 + local password=$2 + + echo " Adding user ${username}" | tee -a $LOGFILE + docker exec ${CANDIG_AUTH_DOMAIN} /opt/jboss/keycloak/bin/add-user-keycloak.sh -u ${username} -p ${password} -r ${KEYCLOAK_REALM} + + echo " Restarting the keycloak container" | tee -a $LOGFILE + docker restart ${CANDIG_AUTH_DOMAIN} +} + +get_token() { + local keycloak_admin_user=$(cat tmp/secrets/keycloak-admin-user) + local keycloak_admin_password=$(cat tmp/secrets/keycloak-admin-password) + + local BID=$(curl \ + -d "client_id=admin-cli" \ + -d "username=${keycloak_admin_user}" \ + -d "password=${keycloak_admin_password}" \ + -d "grant_type=password" \ + "${KEYCLOAK_PUBLIC_URL}/auth/realms/master/protocol/openid-connect/token" -k 2>/dev/null) + # TODO: security issue fix this, -k flag above ignores cert, even if the url is https + + echo ${BID} | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["access_token"])' +} + +set_realm() { + local realm=$1 + + local JSON='{ + "realm": "candig", + "enabled": true + }' + + curl \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X POST -H "Content-Type: application/json" -d "${JSON}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms" -k +} + +get_realm() { + local realm=$1 + + curl \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}" -k | jq . +} + +get_realm_clients() { + local realm=$1 + + curl \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/clients" -k | jq -S . +} + +set_client() { + local realm=$1 + local client=$2 + local listen=$3 + local redirect=$4 + + # Will add / to listen only if it is present + + local JSON='{ + "clientId": "'"${client}"'", + "enabled": true, + "protocol": "openid-connect", + "implicitFlowEnabled": true, + "standardFlowEnabled": true, + "publicClient": false, + "redirectUris": [ + "'${TYK_LOGIN_TARGET_URL}${redirect}'" + ], + "attributes": { + "saml.assertion.signature": "false", + "saml.authnstatement": "false", + "saml.client.signature": "false", + "saml.encrypt": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.onetimeuse.condition": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "saml_force_name_id_format": "false" + } + }' + + curl \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X POST -H "Content-Type: application/json" -d "${JSON}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/clients" -k + # TODO: security issue fix this, -k flag above ignores cert, even if the url is https +} + +get_secret() { + id=$(curl -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + ${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/clients -k 2>/dev/null | + python3 -c 'import json,sys;obj=json.load(sys.stdin); print([l["id"] for l in obj if l["clientId"] == + "'"$KEYCLOAK_CLIENT_ID"'" ][0])') + + curl -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + ${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/clients/$id/client-secret -k 2>/dev/null | + python3 -c 'import json,sys;obj=json.load(sys.stdin); print(obj["value"])' +} + +get_public_key() { + curl \ + ${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM} -k 2>/dev/null | + python3 -c 'import json,sys;obj=json.load(sys.stdin); print(obj["public_key"])' +} + +# SCRIPT START + +echo " Starting setup calls to keycloak" | tee -a $LOGFILE + +echo "Getting keycloak token" | tee -a $LOGFILE +KEYCLOAK_TOKEN=$(get_token) + +echo "Creating Realm ${KEYCLOAK_REALM}" | tee -a $LOGFILE +set_realm ${KEYCLOAK_REALM} + +echo "Setting client ${KEYCLOAK_CLIENT_ID}" | tee -a $LOGFILE +set_client ${KEYCLOAK_REALM} ${KEYCLOAK_CLIENT_ID} "${TYK_LISTEN_PATH}" ${KEYCLOAK_LOGIN_REDIRECT_PATH} + +echo "Getting keycloak secret" | tee -a $LOGFILE +KEYCLOAK_SECRET_RESPONSE=$(get_secret ${KEYCLOAK_REALM}) +export KEYCLOAK_SECRET=$KEYCLOAK_SECRET_RESPONSE +echo $KEYCLOAK_SECRET > tmp/secrets/keycloak-client-local-candig-secret | tee -a $LOGFILE + +echo "Getting keycloak public key" | tee -a $LOGFILE +KEYCLOAK_PUBLIC_KEY_RESPONSE=$(get_public_key ${KEYCLOAK_REALM}) +export KEYCLOAK_PUBLIC_KEY=$KEYCLOAK_PUBLIC_KEY_RESPONSE +echo "Retrieved keycloak public key as ${KEYCLOAK_PUBLIC_KEY}" | tee -a $LOGFILE + +if [[ ${KEYCLOAK_GENERATE_TEST_USER} == 1 ]]; then + echo "Adding test user" | tee -a $LOGFILE + add_user "$(cat tmp/secrets/keycloak-test-user)" "$(cat tmp/secrets/keycloak-test-user-password)" +fi + +echo "Waiting for keycloak to restart" | tee -a $LOGFILE +while ! docker logs --tail 5 ${CANDIG_AUTH_DOMAIN} | grep "Admin console listening on http://127.0.0.1:9990"; do sleep 1; done +echo "Keycloak setup done!" | tee -a $LOGFILE diff --git a/lib/tyk/Dockerfile b/lib/tyk/Dockerfile new file mode 100644 index 000000000..5fd428935 --- /dev/null +++ b/lib/tyk/Dockerfile @@ -0,0 +1,16 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} + +LABEL Maintainer="CanDIG Project" + +# TODO: this image uses temp dir inside the lib/tyk which deviates from convention of this repo +# See tyk_setup.sh for the same. +COPY ./tmp/tyk.conf /opt/tyk-gateway/tyk.conf +COPY ./tmp/middleware/authMiddleware.js /opt/tyk-gateway/middleware/authMiddleware.js +COPY ./tmp/middleware/permissionsStoreMiddleware.js /opt/tyk-gateway/middleware/permissionsStoreMiddleware.js +COPY ./tmp/middleware/virtualLogin.js /opt/tyk-gateway/middleware/virtualLogin.js +COPY ./tmp/middleware/virtualLogout.js /opt/tyk-gateway/middleware/virtualLogout.js +COPY ./tmp/middleware/virtualToken.js /opt/tyk-gateway/middleware/virtualToken.js +COPY ./tmp/apps/api_candig.json /opt/tyk-gateway/apps/api_candig.json +COPY ./tmp/apps/api_auth.json /opt/tyk-gateway/apps/api_auth.json +COPY ./tmp/policies/policies.json /opt/tyk-gateway/policies/policies.json diff --git a/etc/setup/templates/configs/tyk/confs/api_auth.json.tpl b/lib/tyk/configuration_templates/api_auth.json.tpl similarity index 94% rename from etc/setup/templates/configs/tyk/confs/api_auth.json.tpl rename to lib/tyk/configuration_templates/api_auth.json.tpl index cbddaa790..169d456b6 100644 --- a/etc/setup/templates/configs/tyk/confs/api_auth.json.tpl +++ b/lib/tyk/configuration_templates/api_auth.json.tpl @@ -8,12 +8,12 @@ "KEYCLOAK_RTYPE": "code", "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", - "KEYCLOAK_SERVER": "${KEYCLOAK_SERVICE_PUBLIC_URL}", + "KEYCLOAK_SERVER": "${KEYCLOAK_PUBLIC_URL}", "KEYCLOAK_SCOPE": "openid+email", "KEYCLOAK_RMODE": "query", "USE_SSL": false, "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", - "TYK_SERVER": "${CANDIG_PUBLIC_URL}", + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", "MAX_TOKEN_AGE": 43200 }, diff --git a/etc/setup/templates/configs/tyk/confs/api_candig.json.tpl b/lib/tyk/configuration_templates/api_candig.json.tpl similarity index 95% rename from etc/setup/templates/configs/tyk/confs/api_candig.json.tpl rename to lib/tyk/configuration_templates/api_candig.json.tpl index d84e3cb5d..b3fc994c8 100644 --- a/etc/setup/templates/configs/tyk/confs/api_candig.json.tpl +++ b/lib/tyk/configuration_templates/api_candig.json.tpl @@ -15,7 +15,7 @@ "base_identity_provided_by": "", "proxy": { - "target_url": "${TYK_TARGET_URL}", + "target_url": "${TYK_CANDIG_API_TARGET}", "strip_listen_path": true, "disable_strip_slash": false, "listen_path": "/${TYK_LISTEN_PATH}", @@ -92,7 +92,7 @@ "/api_info", "/serverinfo" ], - "TYK_SERVER": "${CANDIG_PUBLIC_URL}", + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" @@ -101,7 +101,7 @@ "segregate_by_client": false, "providers": [ { - "issuer": "${TEMP_KEYCLOAK_SERVICE_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", + "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" } diff --git a/etc/setup/templates/configs/tyk/confs/authMiddleware.js b/lib/tyk/configuration_templates/authMiddleware.js similarity index 97% rename from etc/setup/templates/configs/tyk/confs/authMiddleware.js rename to lib/tyk/configuration_templates/authMiddleware.js index ceae2d952..ccba3f510 100644 --- a/etc/setup/templates/configs/tyk/confs/authMiddleware.js +++ b/lib/tyk/configuration_templates/authMiddleware.js @@ -2,7 +2,7 @@ var authMiddleware = new TykJS.TykMiddleware.NewMiddleware({}); -var iss = "${TEMP_KEYCLOAK_SERVICE_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" +var iss = "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}" var aud = "${KEYCLOAK_CLIENT_ID}" var full_authn_pk="-----BEGIN PUBLIC KEY-----\n${KEYCLOAK_PUBLIC_KEY}\n-----END PUBLIC KEY-----" diff --git a/etc/setup/templates/configs/tyk/confs/key_request.json.tpl b/lib/tyk/configuration_templates/key_request.json.tpl similarity index 100% rename from etc/setup/templates/configs/tyk/confs/key_request.json.tpl rename to lib/tyk/configuration_templates/key_request.json.tpl diff --git a/etc/setup/templates/configs/tyk/confs/permissionsStoreMiddleware.js b/lib/tyk/configuration_templates/permissionsStoreMiddleware.js similarity index 100% rename from etc/setup/templates/configs/tyk/confs/permissionsStoreMiddleware.js rename to lib/tyk/configuration_templates/permissionsStoreMiddleware.js diff --git a/etc/setup/templates/configs/tyk/confs/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl similarity index 100% rename from etc/setup/templates/configs/tyk/confs/policies.json.tpl rename to lib/tyk/configuration_templates/policies.json.tpl diff --git a/etc/setup/templates/configs/tyk/confs/tyk.conf.tpl b/lib/tyk/configuration_templates/tyk.conf.tpl similarity index 98% rename from etc/setup/templates/configs/tyk/confs/tyk.conf.tpl rename to lib/tyk/configuration_templates/tyk.conf.tpl index 95707e0e8..fad0e6537 100644 --- a/etc/setup/templates/configs/tyk/confs/tyk.conf.tpl +++ b/lib/tyk/configuration_templates/tyk.conf.tpl @@ -101,7 +101,7 @@ "logstash_transport": "", "max_idle_connections_per_host": 500, "middleware_path": "./middleware", - "node_secret": "${SECRET_KEY}", + "node_secret": "${TYK_NODE_SECRET_KEY}", "oauth_redirect_uri_separator": ";", "oauth_refresh_token_expire": 0, "oauth_token_expire": 0, @@ -109,7 +109,7 @@ "pid_file_location": "./tyk-gateway.pid", "public_key_path": "", - "secret": "${SECRET_KEY}", + "secret": "${TYK_SECRET_KEY}", "sentry_code": "", "service_discovery": { "default_cache_timeout": 0 diff --git a/etc/setup/templates/configs/tyk/confs/tyk_analytics.conf.tpl b/lib/tyk/configuration_templates/tyk_analytics.conf.tpl similarity index 93% rename from etc/setup/templates/configs/tyk/confs/tyk_analytics.conf.tpl rename to lib/tyk/configuration_templates/tyk_analytics.conf.tpl index 9b23d31fc..7557f9a1e 100644 --- a/etc/setup/templates/configs/tyk/confs/tyk_analytics.conf.tpl +++ b/lib/tyk/configuration_templates/tyk_analytics.conf.tpl @@ -1,7 +1,7 @@ { "listen_port": 3000, "tyk_api_config": { - "Host": "${CANDIG_PUBLIC_URL}", + "Host": "${TYK_LOGIN_TARGET_URL}", "Port": "8080", "Secret": "${TYK_NODE_SECRET}" }, @@ -25,8 +25,8 @@ "settings": { "ClientKey": "" }, - "default_from_email": "${TYK_DASH_FROM_EMAIL}", - "default_from_name": "${TYK_DASH_FROM_NAME}" + "default_from_email": "${TYK_ANALYTICS_FROM_EMAIL}", + "default_from_name": "${TYK_ANALYTICS_FROM_NAME}" }, "hide_listen_path": false, "sentry_code": "", diff --git a/etc/setup/templates/configs/tyk/confs/virtualLogin.js b/lib/tyk/configuration_templates/virtualLogin.js similarity index 100% rename from etc/setup/templates/configs/tyk/confs/virtualLogin.js rename to lib/tyk/configuration_templates/virtualLogin.js diff --git a/etc/setup/templates/configs/tyk/confs/virtualLogout.js b/lib/tyk/configuration_templates/virtualLogout.js similarity index 100% rename from etc/setup/templates/configs/tyk/confs/virtualLogout.js rename to lib/tyk/configuration_templates/virtualLogout.js diff --git a/etc/setup/templates/configs/tyk/confs/virtualToken.js b/lib/tyk/configuration_templates/virtualToken.js similarity index 100% rename from etc/setup/templates/configs/tyk/confs/virtualToken.js rename to lib/tyk/configuration_templates/virtualToken.js diff --git a/lib/tyk/docker-compose.yml b/lib/tyk/docker-compose.yml new file mode 100644 index 000000000..8bf7f5291 --- /dev/null +++ b/lib/tyk/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3.7' + +services: + tyk: + build: + context: ${PWD}/lib/tyk + args: + - BASE_IMAGE=tykio/tyk-gateway:${TYK_VERSION} + networks: + - ${DOCKER_NET} + ports: + - "${TYK_SERVICE_PUBLIC_PORT}:8080" + deploy: + placement: + constraints: + - node.role == manager + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + labels: + - "traefik.enable=true" + - "traefik.docker.lbswarm=true" + - "traefik.http.routers.tyk.rule=Host(`tyk.${CANDIG_DOMAIN}`)" + - "traefik.http.services.tyk.loadbalancer.server.port=${TYK_SERVICE_PUBLIC_PORT}" + logging: *default-logging + volumes: + - tyk-data:/opt/tyk-gateway + depends_on: + - tyk-redis + healthcheck: + test: [ "CMD", "curl", "-fI", "http://0.0.0.0:8080" ] + interval: 30s + timeout: 20s + retries: 3 + + tyk-redis: + image: redis:${TYK_REDIS_VERSION} + networks: + - ${DOCKER_NET} + volumes: + - tyk-redis-data:/data + ports: + - "6379:6379" + deploy: + placement: + constraints: + - node.role == manager + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + logging: *default-logging + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 20s + retries: 3 \ No newline at end of file diff --git a/lib/tyk/tyk_key_generation.sh b/lib/tyk/tyk_key_generation.sh new file mode 100644 index 000000000..18f7375f3 --- /dev/null +++ b/lib/tyk/tyk_key_generation.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +LOGFILE=$PWD/tmp/progress.txt + + +echo "Starting Tyk key setup, post launch" | tee -a $LOGFILE + +TYK_KEY_REQUEST=$(cat "${CONFIG_DIR}/key_request.json") +curl "${TYK_LOGIN_TARGET_URL}/tyk/keys/create" -H "x-tyk-authorization: ${TYK_SECRET_KEY}" -s -H "Content-Type: application/json" -X POST -d "${TYK_KEY_REQUEST}" +curl -H "x-tyk-authorization: ${TYK_SECRET_KEY}" -s "${TYK_LOGIN_TARGET_URL}/tyk/reload/group" + +echo "Finished Tyk key setup" | tee -a $LOGFILE \ No newline at end of file diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh new file mode 100755 index 000000000..d643bccf4 --- /dev/null +++ b/lib/tyk/tyk_setup.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +LOGFILE=$PWD/tmp/progress.txt + +# This script will set up a full tyk environment on your local CanDIGv2 cluster. +# Be sure to invoke this from the Makefile at the project's root directory [CanDIGv2]. +# Load Tyk template (.tpl) files, populate them with project .env variables, and then spit +# them out to ./lib/tyk/tmp/*. + +# TODO: this image uses temp dir inside the lib/tyk which deviates from convention of this repo +# see Makefile.authx for other details. +CONFIG_DIR="$PWD/lib/tyk/tmp" + +KEYCLOAK_SECRET_VAL=$(cat $PWD/tmp/secrets/keycloak-client-local-candig-secret) +export KEYCLOAK_SECRET=$KEYCLOAK_SECRET_VAL + +KEYCLOAK_CLIENT_ID_64_VAL=$(cat $PWD/tmp/secrets/keycloak-client-local-candig-id-64) +export KEYCLOAK_CLIENT_ID_64=$KEYCLOAK_CLIENT_ID_64_VAL + +TYK_SECRET_KEY_VAL=$(cat $PWD/tmp/secrets/tyk-secret-key) +export TYK_SECRET_KEY=$TYK_SECRET_KEY_VAL + +TYK_NODE_SECRET_KEY_VAL=$(cat $PWD/tmp/secrets/tyk-node-secret-key) +export TYK_NODE_SECRET_KEY=$TYK_NODE_SECRET_KEY_VAL + +TYK_ANALYTIC_ADMIN_SECRET_VAL=$(cat $PWD/tmp/secrets/tyk-analytics-admin-key) +export TYK_ANALYTIC_ADMIN_SECRET=$TYK_ANALYTIC_ADMIN_SECRET_VAL + +mkdir -p $CONFIG_DIR $CONFIG_DIR/apps $CONFIG_DIR/policies $CONFIG_DIR/middleware + +echo "Working on tyk.conf" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/tyk.conf.tpl > ${CONFIG_DIR}/tyk.conf + +echo "Working on authMiddleware.js" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/authMiddleware.js > ${CONFIG_DIR}/middleware/authMiddleware.js + +echo "Working on api_auth.json" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/api_auth.json.tpl > ${CONFIG_DIR}/apps/api_auth.json + +echo "Working on api_candig.json" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/api_candig.json.tpl > ${CONFIG_DIR}/apps/api_candig.json + +echo "Working on policies.json" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/policies.json.tpl > ${CONFIG_DIR}/policies/policies.json + +echo "Working on key_request.json" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/key_request.json.tpl > ${CONFIG_DIR}/key_request.json + +echo "Working on tyk_analytics" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/tyk_analytics.conf.tpl > ${CONFIG_DIR}/tyk_analytics.conf + +# Copy files from template configs + +echo "Copying virtualLogin.js" | tee -a $LOGFILE +cp ${PWD}/lib/tyk/configuration_templates/virtualLogin.js ${CONFIG_DIR}/middleware/virtualLogin.js + +echo "Copying virtualLogout.js" | tee -a $LOGFILE +cp ${PWD}/lib/tyk/configuration_templates/virtualLogout.js ${CONFIG_DIR}/middleware/virtualLogout.js + +echo "Copying virtualToken.js" | tee -a $LOGFILE +cp ${PWD}/lib/tyk/configuration_templates/virtualToken.js ${CONFIG_DIR}/middleware/virtualToken.js + +echo "Copying permissionsStoreMiddleware.js" | tee -a $LOGFILE +cp ${PWD}/lib/tyk/configuration_templates/permissionsStoreMiddleware.js ${CONFIG_DIR}/middleware/permissionsStoreMiddleware.js + + + +echo "Tyk configuration generated!" | tee -a $LOGFILE diff --git a/lib/authorization/vault/Dockerfile b/lib/vault/Dockerfile similarity index 100% rename from lib/authorization/vault/Dockerfile rename to lib/vault/Dockerfile diff --git a/etc/setup/templates/configs/vault/vault-config.json.tpl b/lib/vault/configuration_templates/vault-config.json.tpl similarity index 100% rename from etc/setup/templates/configs/vault/vault-config.json.tpl rename to lib/vault/configuration_templates/vault-config.json.tpl diff --git a/etc/setup/templates/configs/vault/vault-datastructure.json.tpl b/lib/vault/configuration_templates/vault-datastructure.json.tpl similarity index 100% rename from etc/setup/templates/configs/vault/vault-datastructure.json.tpl rename to lib/vault/configuration_templates/vault-datastructure.json.tpl diff --git a/etc/setup/templates/configs/vault/vault-entity-entitlements.json.tpl b/lib/vault/configuration_templates/vault-entity-entitlements.json.tpl similarity index 100% rename from etc/setup/templates/configs/vault/vault-entity-entitlements.json.tpl rename to lib/vault/configuration_templates/vault-entity-entitlements.json.tpl diff --git a/lib/authorization/docker-compose.yml b/lib/vault/docker-compose.yml similarity index 74% rename from lib/authorization/docker-compose.yml rename to lib/vault/docker-compose.yml index e5eab24cc..c1c63285e 100644 --- a/lib/authorization/docker-compose.yml +++ b/lib/vault/docker-compose.yml @@ -1,11 +1,9 @@ version: '3.7' -#TODO: rewrite docker-compose for authentication so it conforms with spec in lib/templates/docker-compose.yml services: vault: - #container_name: vault build: - context: ${PWD}/lib/authorization/vault + context: ${PWD}/lib/vault args: - VAULT_VERSION=1.5.0 - venv_python=${VENV_PYTHON} @@ -34,11 +32,10 @@ services: - "traefik.docker.lbswarm=true" - "traefik.http.routers.vault.rule=Host(`vault.${CANDIG_DOMAIN}`)" - "traefik.http.services.vault.loadbalancer.server.port=${VAULT_SERVICE_PORT}" - #TODO: fix logging - #logging: *default-logging + logging: *default-logging command: server -config=/vault/config/vault-config.json healthcheck: - test: ["CMD", "curl", "-f", "http://0.0.0.0:${VAULT_SERVICE_PORT}/ui/"] + test: [ "CMD", "curl", "-f", "http://0.0.0.0:${VAULT_SERVICE_PORT}/ui/" ] interval: 30s timeout: 20s - retries: 3 + retries: 3 \ No newline at end of file diff --git a/etc/setup/scripts/subtasks/vault_setup.sh b/lib/vault/vault_setup.sh similarity index 89% rename from etc/setup/scripts/subtasks/vault_setup.sh rename to lib/vault/vault_setup.sh index c6c2b87f7..7c60cb2e5 100755 --- a/etc/setup/scripts/subtasks/vault_setup.sh +++ b/lib/vault/vault_setup.sh @@ -16,10 +16,10 @@ mkdir -p ${PWD}/lib/authorization/vault/tmp # vault-config.json echo "Working on vault-config.json .." -envsubst < ${PWD}/etc/setup/templates/configs/vault/vault-config.json.tpl > ${PWD}/lib/authorization/vault/tmp/vault-config.json +envsubst < ${PWD}/lib/authorization/vault/configuration_templates/vault-config.json.tpl > ${PWD}/lib/authorization/vault/tmp/vault-config.json # boot container -docker-compose -f ${PWD}/lib/compose/docker-compose.yml -f ${PWD}/lib/authorization/docker-compose.yml up -d vault +export SERVICE=vault && make compose-authorization # -- todo: run only if not already initialized -- # --- temp @@ -92,7 +92,7 @@ docker exec $vault sh -c "vault write auth/jwt/role/researcher user_claim=prefer echo echo ">> configuring jwt stuff" -docker exec $vault sh -c "vault write auth/jwt/config oidc_discovery_url=\"${TEMP_KEYCLOAK_SERVICE_PUBLIC_URL}/auth/realms/candig\" bound_issuer=\"${TEMP_KEYCLOAK_SERVICE_PUBLIC_URL}/auth/realms/candig\" default_role=\"researcher\"" +docker exec $vault sh -c "vault write auth/jwt/config oidc_discovery_url=\"${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/candig\" bound_issuer=\"${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/candig\" default_role=\"researcher\"" # create users echo @@ -100,7 +100,7 @@ echo ">> creating user $KEYCLOAK_TEST_USER" export TEMPLATE_USER=$(echo $KEYCLOAK_TEST_USER) export TEMPLATE_DATASET_PERMISSIONS=4 -TEST_USER_PERMISSIONS_DATASTRUCTURE=$(envsubst < ${PWD}/etc/setup/templates/configs/vault/vault-entity-entitlements.json.tpl) +TEST_USER_PERMISSIONS_DATASTRUCTURE=$(envsubst < ${PWD}/lib/authorization/vault/configuration_templates/vault-entity-entitlements.json.tpl) test_user_output=$(docker exec $vault sh -c "echo '${TEST_USER_PERMISSIONS_DATASTRUCTURE}' > ${KEYCLOAK_TEST_USER}.json; vault write identity/entity @${KEYCLOAK_TEST_USER}.json; rm ${KEYCLOAK_TEST_USER}.json;") @@ -113,7 +113,7 @@ echo ">> creating user $KEYCLOAK_TEST_USER_TWO" export TEMPLATE_USER=$(echo $KEYCLOAK_TEST_USER_TWO) export TEMPLATE_DATASET_PERMISSIONS=1 -TEST_USER_TWO_PERMISSIONS_DATASTRUCTURE=$(envsubst < ${PWD}/etc/setup/templates/configs/vault/vault-entity-entitlements.json.tpl) +TEST_USER_TWO_PERMISSIONS_DATASTRUCTURE=$(envsubst < ${PWD}/lib/authorization/vault/configuration_templates/vault-entity-entitlements.json.tpl) test_user_output_two=$(docker exec $vault sh -c "echo '${TEST_USER_TWO_PERMISSIONS_DATASTRUCTURE}' > ${KEYCLOAK_TEST_USER_TWO}.json; vault write identity/entity @${KEYCLOAK_TEST_USER_TWO}.json; rm ${KEYCLOAK_TEST_USER_TWO}.json;") @@ -156,7 +156,7 @@ echo ">> matching key and inserting custom info into the jwt" # json escaped or base64 escaped string and the braces have to be spaced apart # because templating code requres {{}} which when followed by another brace # messes up Vault and it complains that there is a mismatch in balance of braces -VAULT_IDENTITY_ROLE_TEMPLATE=$(envsubst < ${PWD}/etc/setup/templates/configs/vault/vault-datastructure.json.tpl) +VAULT_IDENTITY_ROLE_TEMPLATE=$(envsubst < ${PWD}/lib/authorization/vault/configuration_templates/vault-datastructure.json.tpl) docker exec $vault sh -c "echo '${VAULT_IDENTITY_ROLE_TEMPLATE}' > researcher.json; vault write identity/oidc/role/researcher @researcher.json; rm researcher.json;" echo diff --git a/provision.sh b/provision.sh index 28e8f7961..e1a3961aa 100644 --- a/provision.sh +++ b/provision.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Optional args: CanDIGv2 repo path, CanDIGv2 repo branch -LOGFILE=$PWD/tmp/progress.txt +LOGFILE=$PWD/candig/tmp/progress.txt echo "started provision.sh" | tee -a $LOGFILE # sudo apt-get update diff --git a/setup_containers.sh b/setup_containers.sh index f3919fc3c..d15b91a26 100644 --- a/setup_containers.sh +++ b/setup_containers.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -LOGFILE=$PWD/tmp/progress.txt +LOGFILE=$PWD/candig/tmp/progress.txt grep -q "finished provision.sh" $LOGFILE if [ $? -ne 0 ]; then diff --git a/setup_vagrant.sh b/setup_vagrant.sh index b10fa85d6..695ff8df6 100644 --- a/setup_vagrant.sh +++ b/setup_vagrant.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# works for a Debian-based box: +# works for a Debian-based box: sudo apt-get update sudo apt-get install -y unzip bsdtar From bf1e4d507bad2a344090f376cbe990c6015415a6 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 17 Nov 2021 15:33:10 -0800 Subject: [PATCH 024/236] Forgot to tee to the logfile on a couple of lines (#103) --- provision.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/provision.sh b/provision.sh index e1a3961aa..11a1da3f6 100644 --- a/provision.sh +++ b/provision.sh @@ -45,10 +45,10 @@ fi cd $path if grep -qs "CanDIGv2" .git/config; then - echo "Specified path is a CanDIGv2 repo" + echo "Specified path is a CanDIGv2 repo" | tee -a $LOGFILE git checkout $2 else - echo "Cloning CanDIGv2..." + echo "Cloning CanDIGv2..." | tee -a $LOGFILE git clone $branch https://github.com/CanDIG/CanDIGv2.git cd CanDIGv2/ fi From 5ed239a7bd76ce67e1a8b2c1ee1d5d3e537d49fd Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Thu, 18 Nov 2021 12:05:04 -0500 Subject: [PATCH 025/236] Update of submodules (#102) * submodules are up to date * bridge-net creation error handling * update htsget_app to v0.1.5 * update chord-drs to v0.4.0 * update katsu, cancogen-dashboard versions Co-authored-by: daisie_local --- Makefile | 3 +- etc/env/example.env | 40 +++++++++++------------ lib/cancogen-dashboard/cancogen_dashboard | 2 +- lib/chord-metadata/chord_metadata_service | 2 +- lib/drs-server/chord_drs | 2 +- lib/htsget-server/htsget_app | 2 +- lib/portainer/docker-compose.yml | 4 +-- lib/toil/toil-docker | 2 +- 8 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 1612541b4..c06d44750 100644 --- a/Makefile +++ b/Makefile @@ -406,7 +406,8 @@ compose-%: #<<< .PHONY: docker-networks docker-networks: - docker network create --driver bridge --subnet=$(DOCKER_BRIDGE_IP) --attachable bridge-net + docker network create --driver bridge --subnet=$(DOCKER_BRIDGE_IP) --attachable \ + bridge-net || echo "bridge-net already exists..." docker network create --driver bridge --subnet=$(DOCKER_GWBRIDGE_IP) --attachable \ -o com.docker.network.bridge.enable_icc=false \ -o com.docker.network.bridge.name=docker_gwbridge \ diff --git a/etc/env/example.env b/etc/env/example.env index d11c8f8da..8f00fc9bc 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard #wes-server jupyter igv authentication authorization +CANDIG_MODULES=weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets rnaget candig-server federation-service #cancogen-dashboard cnv-service wes-server jupyter igv CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -52,35 +52,38 @@ MINIKUBE_DISK=80000 MINIKUBE_DRIVER=virtualbox # weavescope app +#TODO: update weave version WEAVE_VERSION=1.13.1 WEAVE_UI_PORT=4040 # logging services -FLUENTD_VERSION=v1.9-1 +#TODO: test monitoring version updates +FLUENTD_VERSION=v1.14-1 FLUENTD_PORT=24224 -ELASTIC_SEARCH_VERSION=7.8.0 +ELASTIC_SEARCH_VERSION=7.14.2 ELASTIC_SEARCH_HTTP_PORT=9200 ELASTIC_SEARCH_TRANSPORT=9300 -KIBANA_VERSION=7.8.0 +KIBANA_VERSION=7.14.2 KIBANA_PORT=5601 # monitoring services -PROMETHEUS_VERSION=2.25.0 +PROMETHEUS_VERSION=2.31.1 PROMETHEUS_PORT=9090 NODE_EXPORTER_PORT=9100 -ALERT_MANAGER_VERSION=v0.21.0 +ALERT_MANAGER_VERSION=v0.23.0 ALERT_MANAGER_PORT=9093 CADVISOR_PORT=9080 -GRAFANA_VERSION=latest +GRAFANA_VERSION=8.2.4 GRAFANA_PORT=9888 # portainer controller +PORTAINER_VERSION=2.9.3-alpine PORTAINER_UI_PORT=9010 #options are [unix:///var/run/docker.sock, tcp://tasks.portainer-agent:9001] PORTAINER_SOCKET=unix:///var/run/docker.sock # consul server -CONSUL_VERSION=1.9.4 +CONSUL_VERSION=1.9 CONSUL_RPC_PORT=8502 CONSUL_HTTP_PORT=8500 CONSUL_DNS_PORT=8600 @@ -88,7 +91,7 @@ CONSUL_LAN_PORT=8301 CONSUL_WAN_PORT=8302 # traefik controller -TRAEFIK_VERSION=2.4.7 +TRAEFIK_VERSION=v2.5 # enable swarm operations # options are [true, false] TRAEFIK_SWARM=false @@ -118,11 +121,11 @@ MINIO_DATA_DIR=/data #MINIO_VOLUME_OPT+=--opt=device=/dev/sdb1 # chord-drs -CHORD_DRS_VERSION=v0.2.0 +CHORD_DRS_VERSION=v0.4.0 CHORD_DRS_PORT=6000 # htsget-app -HTSGET_APP_VERSION=0.1.3 +HTSGET_APP_VERSION=v0.1.5 HTSGET_APP_PORT=3333 # wes server @@ -153,8 +156,8 @@ WES_OPT+=--opt=extra=--metrics #--- # toil executor -TOIL_VERSION=5.3.1a1 -TOIL_BUILD_HASH=dd4d4725f51c0e59a58a4ebffe143410dee4722e-py3.7 +TOIL_VERSION=5.5.0 +TOIL_BUILD_HASH=b0ff5be051f2fd55352e00450b7848dcf8354a3b-py3.7 TOIL_MODULES=toil toil-grafana toil-mtail toil-prometheus TOIL_IP=0.0.0.0 TOIL_PORT=5050 @@ -180,7 +183,7 @@ FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 # chord metadata service -CHORD_METADATA_VERSION=v1.3.5 +CHORD_METADATA_VERSION=v1.3.0 CHORD_METADATA_PORT=8008 CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false @@ -198,7 +201,7 @@ CANDIG_INGEST_VERSION=1.5.0 CANDIG_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} # rnaget service -RNAGET_VERSION=v0.9.4-8-g187b583 +RNAGET_VERSION=v0.9.5 RNAGET_HOST=0.0.0.0 RNAGET_PORT=3005 @@ -212,7 +215,6 @@ AUTHORIZATION_SERVICE_VERSION=v0.0.1-alpha AUTHORIZATION_SERVICE_HOST=0.0.0.0 AUTHORIZATION_SERVICE_PORT=7000 - # keycloak service KEYCLOAK_VERSION=15.0.0 KEYCLOAK_REALM=candig @@ -226,7 +228,6 @@ KEYCLOAK_PRIVATE_PROTO=http KEYCLOAK_PUBLIC_URL=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_PORT} KEYCLOAK_PUBLIC_URL_PROD=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN} KEYCLOAK_PRIVATE_URL=${KEYCLOAK_PRIVATE_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_CONTAINER_PORT} - KEYCLOAK_GENERATE_TEST_USER=0 # tyk service @@ -250,8 +251,8 @@ TYK_CANDIG_API_NAME=CanDIG TYK_CANDIG_API_SLUG=candig TYK_CANDIG_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} - # vault service +#TODO: pin vault version VAULT_FILE_PATH="/vault/data" VAULT_TLS_DISABLE=1 VAULT_UI=true @@ -272,9 +273,8 @@ OPA_URL=http://opa:${OPA_PORT} CANDIG_AUTHZ_SERVICE_PORT=8182 - # cancogen_dashboard -CANCOGEN_DASHBOARD_VERSION=v0.3.0 +CANCOGEN_DASHBOARD_VERSION=v0.4.0 CANCOGEN_DASHBOARD_HOST=0.0.0.0 CANCOGEN_DASHBOARD_PORT=3002 CANCOGEN_BASE_URL=http://candig-server:3001 diff --git a/lib/cancogen-dashboard/cancogen_dashboard b/lib/cancogen-dashboard/cancogen_dashboard index 946b72fa9..1efb52759 160000 --- a/lib/cancogen-dashboard/cancogen_dashboard +++ b/lib/cancogen-dashboard/cancogen_dashboard @@ -1 +1 @@ -Subproject commit 946b72fa941e1b00184191f95f83f73b7dec91e6 +Subproject commit 1efb52759c4a6bde2920766f74452ece8431d5d7 diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 0b83be602..671892569 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 0b83be602288d2eb065545a003e5f6ec5db549ba +Subproject commit 671892569f403d4dda47880e5619206cfbd5db9f diff --git a/lib/drs-server/chord_drs b/lib/drs-server/chord_drs index 5c017d470..fa14c5e3c 160000 --- a/lib/drs-server/chord_drs +++ b/lib/drs-server/chord_drs @@ -1 +1 @@ -Subproject commit 5c017d47009256ff69b0fd706189d85b69c37d9b +Subproject commit fa14c5e3cb26c7b90c926cbf14ba2c894c09e9fd diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 47385de08..f4070b5f4 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 47385de080d1c599766dfa8621901720716817a9 +Subproject commit f4070b5f413dc4f46319bd3ed0dc1e4301b59e36 diff --git a/lib/portainer/docker-compose.yml b/lib/portainer/docker-compose.yml index 00d77a00b..2c0d1ad7a 100644 --- a/lib/portainer/docker-compose.yml +++ b/lib/portainer/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: portainer-agent: - image: portainer/agent + image: portainer/agent:${PORTAINER_VERSION:-latest} volumes: - /var/run/docker.sock:/var/run/docker.sock - /var/lib/docker/volumes:/var/lib/docker/volumes @@ -23,7 +23,7 @@ services: # LOG_LEVEL: debug portainer: - image: portainer/portainer-ce + image: portainer/portainer-ce:${PORTAINER_VERSION:-latest} command: -H ${PORTAINER_SOCKET} --tlsskipverify --admin-password-file /run/secrets/secret_key ports: - "${PORTAINER_UI_PORT}:9000" diff --git a/lib/toil/toil-docker b/lib/toil/toil-docker index dd4d4725f..b0ff5be05 160000 --- a/lib/toil/toil-docker +++ b/lib/toil/toil-docker @@ -1 +1 @@ -Subproject commit dd4d4725f51c0e59a58a4ebffe143410dee4722e +Subproject commit b0ff5be051f2fd55352e00450b7848dcf8354a3b From 1093d7b52ce03fb36fa127c6f163b4941edd16d6 Mon Sep 17 00:00:00 2001 From: Amanjeev Sethi Date: Fri, 3 Dec 2021 16:39:41 -0500 Subject: [PATCH 026/236] Tyk add new API feature + New API for Katsu/Chord Metadata (#104) * fixing authx-down command * container name patches * container_name cleanup * DIG-515 : authentication refactoring * DIG-515: authz * DIG-512 * DIG-513 * DIG-510 * begin authx setup skipping local idp * DIG-511 + external compose volumes * update (conda): settings that allow for conda env setup without intervention DIG-633 * add (vagrant): libvirt section to launch using stuff like QEMU DIG-633 * refactor (authx): reorganizing structure; WIP; DIG-633 * chore (README): spelling * chore (gitignore): add .idea directory * feature (vagrant): add IP address option to Vagrantfile * feature (authx): add keycloak to the setup launch * feature (authx): minor formatting for keycloak scripts DIG-633 * feature (authx): minor formatting for keycloak scripts DIG-633 * feature (authx): WIP tyk service, simplifying setup DIG-633 * feature (authx): WIP tyk service, simplifying setup moving tmp inside lib/tyk alleviates this pain for now but this is not a good solution as it breaks the repo convention. DIG-633 * refactor (authx): KEYCLOAK_SERVICE* to KEYCLOAK* DIG-633 * feature (authx): CHECKPOINT in case of fire DIG-633 * feature (authx): add + as exclusion in makefile secret generator DIG-633 * feature (authx): fix tyk confs url DIG-633 * feature (authx): remove candig-server from authx makefile because it is already launched; add image removal in cleanup; DIG-633 * feature (authx): refactor variables in keycloak script to remove global and rename locals; DIG-633 * feature (authx): formatting; DIG-633 * feature (authx): fix tyk redirect uri instead of candig server in keycloak client redirect uri settings; DIG-633 * feature (authx): add security TODO warning; DIG-633 * feature (authx): CHECKPOINT in case of fire, working on tyk; DIG-633 * feature (authx): Tyk api redirect works DIG-633 * docs (authx): document steps, and a todo DIG-633 * refactor (authx): renames TEMP_KEYCLOAK.. to KEYCLOACL...PROD because thats the purpose of that URL adds the variable to environment DIG-633 * feature (authx): analytics for tyk DIG-633 * feature (authx): remove check for local idp for now DIG-633 * feature (authx): add warning comments DIG-633 * feature (authx): add directory cleanup for tyk tmp DIG-633 * feature (authx): add directory cleanup for tyk tmp DIG-633 * docs (authx): adds new api section because we need to convey that right now the tyk setup is adhoc at best, it deploys fine with single api (candig) but it is not enough. this section documents how to achieve this in a hacky way. it is rather sad but it is also need of the hour. DIG-633 * docs (authx): steps to add new api * update (conda): settings that allow for conda env setup without intervention DIG-633 * add (vagrant): libvirt section to launch using stuff like QEMU DIG-633 * fix (conda): removes hard-coded instances of CONDA, uses single CONDA to avoid edge case DIG-633 * refactor (conda): use common variable for CONDA path * feature (authx): resolve conflicts because I clearly cannot read; DIG-633 * feature (authx): add `tee` to logfile Suggestion at https://github.com/CanDIG/CanDIGv2/pull/99#discussion_r736020301 DIG-633 * feature (authx): add a way to add new api to tyk * feature (authx): fix failing incorrect health checks for containers DIG-633 * chore (authx): bumps up version of tyk and redis DIG-633 * chore (authx): remove test example from policies DIG-633 * CHECKPOINT DIG-652 * fix (authx): remove repeat line https://github.com/CanDIG/CanDIGv2/pull/99#discussion_r746730247 DIG-633 * fix (authx): indentation should be tabs, not 4 spaces in Make https://github.com/CanDIG/CanDIGv2/pull/99#discussion_r746732718 DIG-633 * fix (authx): missing new line https://github.com/CanDIG/CanDIGv2/pull/99#discussion_r746745183 DIG-633 * fix (authx): remove arbiter https://github.com/CanDIG/CanDIGv2/pull/99#discussion_r746992537 DIG-633 * chore (authx): remove tabs from template file * chore (authx): comma fix * fix (authx): keycloak public key needs to be saved DIG-633 DIG-652 DIG-653 * docs (authx): add usage comments to make recipes DIG-633 DIG-652 DIG-653 * fix (authx): better docker image deletion DIG-633 DIG-652 DIG-653 * fix (authx): better consolidation of keycloak setup inside the script DIG-633 DIG-652 DIG-653 * fix (authx): use proper segments in SESSION_ENDPOINTS for proper login redirect DIG-656 Co-authored-by: Brennan Brouillette Co-authored-by: Shaikh Farhan Rashid --- Makefile.authx | 86 +++++++--- docs/authx-setup.md | 11 +- docs/install-docker.md | 1 + etc/env/example.env | 23 ++- lib/keycloak/keycloak_setup.sh | 25 ++- lib/tyk/Dockerfile | 4 + .../api_candig.json.tpl | 92 +++++------ .../api_katsu_chord.json.tpl | 156 ++++++++++++++++++ .../configuration_templates/policies.json.tpl | 40 +++-- lib/tyk/docker-compose.yml | 2 +- lib/tyk/tyk_key_generation.sh | 41 ++++- lib/tyk/tyk_setup.sh | 14 +- 12 files changed, 387 insertions(+), 108 deletions(-) create mode 100644 lib/tyk/configuration_templates/api_katsu_chord.json.tpl diff --git a/Makefile.authx b/Makefile.authx index 57811b9e6..22cad4fd7 100644 --- a/Makefile.authx +++ b/Makefile.authx @@ -1,8 +1,9 @@ #>>> -# close all authentication and authorization services +# close keycloak services +# make clean-keycloak #<<< -clean-authx: +clean-keycloak: cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ $(DIR)/lib/keycloak/docker-compose.yml | docker-compose -f - down @@ -10,18 +11,39 @@ clean-authx: $(DIR)/lib/tyk/docker-compose.yml | docker-compose -f - down # - remove intermittent docker images - docker rmi candigv2_keycloak:latest --force - docker rmi candig_keycloak:latest --force + @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'keycloak' | xargs -I{} docker rmi --force {} + + +#>>> +# close tyk services +# make clean-tyk + +#<<< +clean-tyk: + cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ + $(DIR)/lib/tyk/docker-compose.yml | docker-compose -f - down - docker rmi candigv2_tyk:latest --force - docker rmi candig_tyk:latest --force + # - remove intermittent docker images + docker images --format '{{.Repository}}:{{.Tag}}' | grep -E 'tyk|redis' | xargs -I{} docker rmi --force {} # - clean tmp dir inside lib/tyk - rm -r $(DIR)/lib/tyk/tmp + rm -r $(DIR)/lib/tyk/tmp || true + + docker volume rm tyk-data + docker volume rm tyk-redis-data + + +#>>> +# close all authentication and authorization services +# make clean-authx + +#<<< +clean-authx: clean-keycloak clean-tyk #>>> # authx, common settings +# make init-authx #<<< init-authx: mkdir @@ -31,26 +53,36 @@ init-authx: mkdir # in its own shell. So to pass the environment from previous commands, one has # to do this backslash dance. This could have been resolved but we decided to # move to a better solution so Aman made a decision to keep this hack as-is. - $(eval KEYCLOAK_CLIENT_ID_64=$(shell echo -n ${KEYCLOAK_CLIENT_ID} | base64)) - @echo $(KEYCLOAK_CLIENT_ID_64) > $(DIR)/tmp/secrets/keycloak-client-local-candig-id-64 - # temp: in production, explicitly indicating port 443 breaks vaults internal oidc provider checks. - # simply remove the ":443 from the authentication services public url for this purpose: - if [[ ${KEYCLOAK_PUBLIC_URL} == *":443"* ]]; then \ - echo "option 1"; \ - $(eval KEYCLOAK_PUBLIC_URL_PROD=$(shell echo ${KEYCLOAK_PUBLIC_URL} | sed -e 's/\(:443\)\$//g')) \ - elif [[ ${KEYCLOAK_PUBLIC_URL} == *":80"* ]]; then \ - echo "option 2"; \ - $(eval KEYCLOAK_PUBLIC_URL_PROD=$(shell echo ${KEYCLOAK_PUBLIC_URL} | sed -e 's/\(:80\)\$//g')) \ - else \ - echo "option 3"; \ - $(eval KEYCLOAK_PUBLIC_URL_PROD=$(shell echo ${KEYCLOAK_PUBLIC_URL})) \ - fi ;\ - export KEYCLOAK_CLIENT_ID_64=$(KEYCLOAK_CLIENT_ID_64); \ - export KEYCLOAK_PUBLIC_URL_PROD=$(KEYCLOAK_PUBLIC_URL_PROD); \ - echo "Setting up Keycloak" ; \ + $(MAKE) docker-volumes + echo "Setting up Keycloak"; \ + export SERVICE=keycloak; \ source ${PWD}/lib/keycloak/keycloak_setup.sh; \ + echo "Setting up Tyk"; \ export SERVICE=tyk; \ ${PWD}/lib/tyk/tyk_setup.sh; \ - echo ; \ - $(MAKE) compose-tyk \ - ${PWD}/lib/tyk/tyk_key_generation.sh; + echo ; \ + $(MAKE) compose-tyk; \ + source ${PWD}/lib/tyk/tyk_key_generation.sh; \ + echo ; + + +#>>> +# authx, redeploy tyk +# make redeploy-tyk + +#<<< +redeploy-tyk: mkdir + $(MAKE) clean-tyk + $(MAKE) docker-volumes + # Generate dynamic environment variables + # ========== HACK ALERT ============== + # This setup with backslashes (\) is required because Make runs each command + # in its own shell. So to pass the environment from previous commands, one has + # to do this backslash dance. This could have been resolved but we decided to + # move to a better solution so Aman made a decision to keep this hack as-is. + export SERVICE=tyk; \ + source ${PWD}/lib/tyk/tyk_setup.sh; \ + echo ; \ + $(MAKE) compose-tyk; \ + source ${PWD}/lib/tyk/tyk_key_generation.sh; \ + echo ; diff --git a/docs/authx-setup.md b/docs/authx-setup.md index 65c0d0d18..58a486ee2 100644 --- a/docs/authx-setup.md +++ b/docs/authx-setup.md @@ -15,11 +15,10 @@ Make sure the relevant details in `.env` are correct. `make clean-authx` -## Adding New API (WIP) +## Adding New API Let's say the new API is called `example` and the route it redirects to us `http://example.org`. -This section will help you figure out how to add the details to the setup but is still a work in progress. -The code to deploy a new API does not exist yet. +This section will help you figure out how to add the details to the setup. - Copy an API template file like `lib/tyk/configuration_templates/api_candig.json.tpl` and give it a name. e.g. api_example.json.tpl @@ -30,12 +29,14 @@ The code to deploy a new API does not exist yet. TYK_EXAMPLE_API_NAME=Example TYK_EXAMPLE_API_SLUG=example TYK_EXAMPLE_API_TARGET=http://example.org - TYK_EXAMPLE_API_LISTEN_PATH=/example + TYK_EXAMPLE_API_LISTEN_PATH=example ``` See section `## Extra APIs can be added here` +- Add the path in the `SESSION_ENDPOINTS` array. If you fail to add proper paths, then your application + will not redirect to login page properly. - Add the new section of the API to `lib/tyk/configuration_templates/policies.json.tpl` under the key `access_rights` -- Add the new section of the API tp `lib/tyk/configuration_templates/key_request.json.tpl` under +- Add the new section of the API to `lib/tyk/tyk_key_generation.sh` under the key `access_rights` - Add the new line to copy the file to the image in the `lib/tyk/Dockerfile` - Add the new line to `envsubst` in `lib/tyk/tyk_setup.sh` (see section `# Extra APIs can be added here`) diff --git a/docs/install-docker.md b/docs/install-docker.md index 8b346e538..eee6c5a27 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -135,6 +135,7 @@ make init-docker ```bash # create images (optional) make images + # pull latest CanDIGv2 images (instead of make images) make docker-pull diff --git a/etc/env/example.env b/etc/env/example.env index 8f00fc9bc..70f04e9f8 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets rnaget candig-server federation-service #cancogen-dashboard cnv-service wes-server jupyter igv +CANDIG_MODULES=weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard #wes-server jupyter igv authentication authorization CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -228,6 +228,7 @@ KEYCLOAK_PRIVATE_PROTO=http KEYCLOAK_PUBLIC_URL=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_PORT} KEYCLOAK_PUBLIC_URL_PROD=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN} KEYCLOAK_PRIVATE_URL=${KEYCLOAK_PRIVATE_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_CONTAINER_PORT} + KEYCLOAK_GENERATE_TEST_USER=0 # tyk service @@ -241,18 +242,35 @@ TYK_ANALYTICS_FROM_EMAIL=admin@distributedgenomics.ca TYK_ANALYTICS_FROM_NAME=CanDIG Admin TYK_LISTEN_PATH= TYK_POLICY_ID=candig_policy + ## api - authentication TYK_AUTH_API_ID=11 TYK_AUTH_API_NAME=Authentication TYK_AUTH_API_SLUG=authentication + ## api - candig-server (v1) TYK_CANDIG_API_ID=21 TYK_CANDIG_API_NAME=CanDIG TYK_CANDIG_API_SLUG=candig TYK_CANDIG_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} +TYK_CANDIG_API_LISTEN_PATH=candig + +## api - katsu - chord-metadata +TYK_KATSU_API_ID=31 +TYK_KATSU_API_NAME=Katsu +TYK_KATSU_API_SLUG=katsu +TYK_KATSU_API_TARGET=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} +TYK_KATSU_API_LISTEN_PATH=katsu + +## Extra APIs can be added here +## api - example +#TYK_EXAMPLE_API_ID=666 +#TYK_EXAMPLE_API_NAME=Example +#TYK_EXAMPLE_API_SLUG=example +#TYK_EXAMPLE_API_TARGET=http://example.org +#TYK_EXAMPLE_API_LISTEN_PATH=example # vault service -#TODO: pin vault version VAULT_FILE_PATH="/vault/data" VAULT_TLS_DISABLE=1 VAULT_UI=true @@ -273,6 +291,7 @@ OPA_URL=http://opa:${OPA_PORT} CANDIG_AUTHZ_SERVICE_PORT=8182 + # cancogen_dashboard CANCOGEN_DASHBOARD_VERSION=v0.4.0 CANCOGEN_DASHBOARD_HOST=0.0.0.0 diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh index 3853eb023..d04efc513 100644 --- a/lib/keycloak/keycloak_setup.sh +++ b/lib/keycloak/keycloak_setup.sh @@ -10,7 +10,7 @@ KEYCLOAK_CONTAINERS=$(echo "$(docker ps | grep keycloak | wc -l)") echo "Number of keycloak containers running: ${KEYCLOAK_CONTAINERS}" | tee -a $LOGFILE if [[ $KEYCLOAK_CONTAINERS -eq 0 ]]; then echo "Booting keycloak container!" | tee -a $LOGFILE - export SERVICE=keycloak && make compose-keycloak + make compose-keycloak sleep 5 echo "Waiting for keycloak to start" | tee -a $LOGFILE while ! docker logs --tail 1000 "$(docker ps | grep keycloak | awk '{print $1}')" | grep "Undertow HTTPS listener https listening on 0.0.0.0"; do sleep 1; done @@ -77,8 +77,7 @@ get_realm_clients() { set_client() { local realm=$1 local client=$2 - local listen=$3 - local redirect=$4 + local redirect=$3 # Will add / to listen only if it is present @@ -140,8 +139,25 @@ KEYCLOAK_TOKEN=$(get_token) echo "Creating Realm ${KEYCLOAK_REALM}" | tee -a $LOGFILE set_realm ${KEYCLOAK_REALM} +echo "Setting client in base64" | tee -a $LOGFILE +export KEYCLOAK_CLIENT_ID_64=$(echo -n ${KEYCLOAK_CLIENT_ID} | base64) +echo $KEYCLOAK_CLIENT_ID_64 > tmp/secrets/keycloak-client-local-candig-id-64 + +echo "Remove ports on prod" | tee -a $LOGFILE +if [[ ${KEYCLOAK_PUBLIC_URL} == *":443"* ]]; then + echo "option 1"; + export KEYCLOAK_PUBLIC_URL_PROD=$(echo ${KEYCLOAK_PUBLIC_URL} | sed -e 's/\(:443\)\$//g') +elif [[ ${KEYCLOAK_PUBLIC_URL} == *":80"* ]]; then + echo "option 2"; + export KEYCLOAK_PUBLIC_URL_PROD=$(echo ${KEYCLOAK_PUBLIC_URL} | sed -e 's/\(:80\)\$//g') +else + echo "option 3"; + export KEYCLOAK_PUBLIC_URL_PROD=$KEYCLOAK_PUBLIC_URL +fi ; + echo "Setting client ${KEYCLOAK_CLIENT_ID}" | tee -a $LOGFILE -set_client ${KEYCLOAK_REALM} ${KEYCLOAK_CLIENT_ID} "${TYK_LISTEN_PATH}" ${KEYCLOAK_LOGIN_REDIRECT_PATH} +echo $KEYCLOAK_CLIENT_ID_64 +set_client "${KEYCLOAK_REALM}" "${KEYCLOAK_CLIENT_ID_64}" "${KEYCLOAK_LOGIN_REDIRECT_PATH}" echo "Getting keycloak secret" | tee -a $LOGFILE KEYCLOAK_SECRET_RESPONSE=$(get_secret ${KEYCLOAK_REALM}) @@ -152,6 +168,7 @@ echo "Getting keycloak public key" | tee -a $LOGFILE KEYCLOAK_PUBLIC_KEY_RESPONSE=$(get_public_key ${KEYCLOAK_REALM}) export KEYCLOAK_PUBLIC_KEY=$KEYCLOAK_PUBLIC_KEY_RESPONSE echo "Retrieved keycloak public key as ${KEYCLOAK_PUBLIC_KEY}" | tee -a $LOGFILE +echo $KEYCLOAK_PUBLIC_KEY > tmp/secrets/keycloak-public-key | tee -a $LOGFILE if [[ ${KEYCLOAK_GENERATE_TEST_USER} == 1 ]]; then echo "Adding test user" | tee -a $LOGFILE diff --git a/lib/tyk/Dockerfile b/lib/tyk/Dockerfile index 5fd428935..55c7896d8 100644 --- a/lib/tyk/Dockerfile +++ b/lib/tyk/Dockerfile @@ -14,3 +14,7 @@ COPY ./tmp/middleware/virtualToken.js /opt/tyk-gateway/middleware/virtualToken.j COPY ./tmp/apps/api_candig.json /opt/tyk-gateway/apps/api_candig.json COPY ./tmp/apps/api_auth.json /opt/tyk-gateway/apps/api_auth.json COPY ./tmp/policies/policies.json /opt/tyk-gateway/policies/policies.json +COPY ./tmp/apps/api_katsu.json /opt/tyk-gateway/apps/api_katsu.json + +## Extra APIs can be added here +#COPY ./tmp/apps/api_example.json /opt/tyk-gateway/apps/api_example.json diff --git a/lib/tyk/configuration_templates/api_candig.json.tpl b/lib/tyk/configuration_templates/api_candig.json.tpl index b3fc994c8..a41f62952 100644 --- a/lib/tyk/configuration_templates/api_candig.json.tpl +++ b/lib/tyk/configuration_templates/api_candig.json.tpl @@ -15,10 +15,10 @@ "base_identity_provided_by": "", "proxy": { - "target_url": "${TYK_CANDIG_API_TARGET}", - "strip_listen_path": true, + "target_url": "${TYK_CANDIG_API_TARGET}", + "strip_listen_path": true, "disable_strip_slash": false, - "listen_path": "/${TYK_LISTEN_PATH}", + "listen_path": "/${TYK_CANDIG_API_LISTEN_PATH}", "transport": { "ssl_insecure_skip_verify": false, "ssl_ciphers": [], @@ -30,13 +30,13 @@ }, "version_data": { - "not_versioned": true, - "versions": { - "Default": { - "name": "Default", - "use_extended_paths": true - } - }, + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true + } + }, "extended_paths": { "ignored": [ { @@ -53,44 +53,44 @@ } }, "custom_middleware": { - "pre": [ + "pre": [ + { + "name": "authMiddleware", + "path": "/opt/tyk-gateway/middleware/authMiddleware.js", + "require_session": false + } + ], + "post": [ { - "name": "authMiddleware", - "path": "/opt/tyk-gateway/middleware/authMiddleware.js", - "require_session": false - } + "name": "permissionsStoreMiddleware", + "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", + "require_session": false + } ], - "post": [ - { - "name": "permissionsStoreMiddleware", - "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", - "require_session": false - } - ], - "id_extractor": { - "extract_with": "", - "extract_from": "", - "extractor_config": {} - }, - "driver": "", - "auth_check": { - "path": "", - "require_session": false, - "name": "" - }, - "post_key_auth": [], - "response": [] + "id_extractor": { + "extract_with": "", + "extract_from": "", + "extractor_config": {} + }, + "driver": "", + "auth_check": { + "path": "", + "require_session": false, + "name": "" + }, + "post_key_auth": [], + "response": [] }, "config_data": { "SESSION_ENDPOINTS": [ - "/", - "/gene_search", - "/patients_overview", - "/sample_analysis", - "/custom_visualization", - "/api_info", - "/serverinfo" + "/candig", + "/candig/gene_search", + "/candig/patients_overview", + "/candig/sample_analysis", + "/candig/custom_visualization", + "/candig/api_info", + "/candig/serverinfo" ], "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", @@ -98,8 +98,8 @@ "VAULT_ROLE":"researcher" }, "openid_options": { - "segregate_by_client": false, - "providers": [ + "segregate_by_client": false, + "providers": [ { "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { @@ -111,8 +111,8 @@ "definition": { - "location": "header", - "key": "x-api-version" + "location": "header", + "key": "x-api-version" }, diff --git a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl new file mode 100644 index 000000000..69e44acb8 --- /dev/null +++ b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl @@ -0,0 +1,156 @@ +{ + "api_id": "${TYK_KATSU_API_ID}", + "name": "${TYK_KATSU_API_NAME}", + "use_openid": true, + "active": true, + "slug": "${TYK_KATSU_API_SLUG}", + + "enable_signature_checking": false, + + "jwt_issued_at_validation_skew": 0, + "jwt_expires_at_validation_skew": 0, + "upstream_certificates": {}, + "use_keyless": false, + "enable_coprocess_auth": false, + "base_identity_provided_by": "", + + "proxy": { + "target_url": "${TYK_KATSU_API_TARGET}", + "strip_listen_path": true, + "disable_strip_slash": false, + "listen_path": "/${TYK_KATSU_API_LISTEN_PATH}", + "transport": { + "ssl_insecure_skip_verify": false, + "ssl_ciphers": [], + "ssl_min_version": 0, + "proxy_url": "" + }, + "target_list": [], + "preserve_host_header": false + }, + + "version_data": { + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true + } + }, + "extended_paths": { + "ignored": [ + { + "path": "${KEYCLOAK_LOGIN_REDIRECT_PATH}", + "method_actions": { + "GET": { + "action": "no_action", + "code": 200, + "headers": {} + } + } + } + ] + } + }, + "custom_middleware": { + "pre": [ + { + "name": "authMiddleware", + "path": "/opt/tyk-gateway/middleware/authMiddleware.js", + "require_session": false + } + ], + "post": [ + { + "name": "permissionsStoreMiddleware", + "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", + "require_session": false + } + ], + "id_extractor": { + "extract_with": "", + "extract_from": "", + "extractor_config": {} + }, + "driver": "", + "auth_check": { + "path": "", + "require_session": false, + "name": "" + }, + "post_key_auth": [], + "response": [] + }, + + "config_data": { + "SESSION_ENDPOINTS": [ + "/katsu" + ], + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", + "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", + "VAULT_ROLE":"researcher" + }, + "openid_options": { + "segregate_by_client": false, + "providers": [ + { + "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "client_ids": { + "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" + } + } + ] + }, + + + "definition": { + "location": "header", + "key": "x-api-version" + }, + + + "internal": false, + "jwt_skip_kid": false, + "enable_batch_request_support": false, + "response_processors": [], + "use_mutual_tls_auth": false, + "basic_auth": { + "disable_caching": false, + "cache_ttl": 0, + "extract_from_body": false, + "body_user_regexp": "", + "body_password_regexp": "" + }, + "use_standard_auth": false, + "session_lifetime": 0, + "use_oauth2": false, + "jwt_source": "", + "jwt_signing_method": "", + "jwt_not_before_validation_skew": 0, + "jwt_identity_base_field": "", + + "session_provider": { + "name": "", + "storage_engine": "", + "meta": {} + }, + + "auth": { + "use_param": false, + "param_name": "", + "use_cookie": false, + "cookie_name": "", + "auth_header_name": "", + "use_certificate": false, + "validate_signature": false, + "signature": { + "algorithm": "", + "header": "", + "secret": "", + "allowed_clock_skew": 0, + "error_code": 0, + "error_message": "" + } + } +} diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 68dd925c9..6706c4317 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -8,8 +8,16 @@ "versions": [ "Default" ] + }, + "${TYK_KATSU_API_ID}": { + "allowed_urls": [], + "api_id": "${TYK_KATSU_API_ID}", + "api_name": "${TYK_KATSU_API_NAME}", + "versions": [ + "Default" + ] } - }, + }, "active": true, "name": "CanDIG Policy", "rate": 100, @@ -20,20 +28,20 @@ }, "default": { - "rate": 1000, - "per": 1, - "quota_max": 100, - "quota_renewal_rate": 60, - "access_rights": { - "41433797848f41a558c1573d3e55a410": { - "api_name": "My API", - "api_id": "41433797848f41a558c1573d3e55a410", - "versions": [ - "Default" - ] - } - }, - "org_id": "54de205930c55e15bd000001", - "hmac_enabled": false + "rate": 1000, + "per": 1, + "quota_max": 100, + "quota_renewal_rate": 60, + "access_rights": { + "41433797848f41a558c1573d3e55a410": { + "api_name": "My API", + "api_id": "41433797848f41a558c1573d3e55a410", + "versions": [ + "Default" + ] + } + }, + "org_id": "54de205930c55e15bd000001", + "hmac_enabled": false } } diff --git a/lib/tyk/docker-compose.yml b/lib/tyk/docker-compose.yml index 8bf7f5291..b3bbc826c 100644 --- a/lib/tyk/docker-compose.yml +++ b/lib/tyk/docker-compose.yml @@ -30,7 +30,7 @@ services: depends_on: - tyk-redis healthcheck: - test: [ "CMD", "curl", "-fI", "http://0.0.0.0:8080" ] + test: [ "CMD", "curl", "http://0.0.0.0:8080/hello" ] interval: 30s timeout: 20s retries: 3 diff --git a/lib/tyk/tyk_key_generation.sh b/lib/tyk/tyk_key_generation.sh index 18f7375f3..5deca62ed 100644 --- a/lib/tyk/tyk_key_generation.sh +++ b/lib/tyk/tyk_key_generation.sh @@ -7,8 +7,43 @@ LOGFILE=$PWD/tmp/progress.txt echo "Starting Tyk key setup, post launch" | tee -a $LOGFILE -TYK_KEY_REQUEST=$(cat "${CONFIG_DIR}/key_request.json") -curl "${TYK_LOGIN_TARGET_URL}/tyk/keys/create" -H "x-tyk-authorization: ${TYK_SECRET_KEY}" -s -H "Content-Type: application/json" -X POST -d "${TYK_KEY_REQUEST}" -curl -H "x-tyk-authorization: ${TYK_SECRET_KEY}" -s "${TYK_LOGIN_TARGET_URL}/tyk/reload/group" +TYK_SECRET_KEY_VAL=$(cat $PWD/tmp/secrets/tyk-secret-key) +export TYK_SECRET_KEY=$TYK_SECRET_KEY_VAL + +generate_key() { + + # Extra APIs can be added here in the `access_rights` section + + local tyk_key_request='{ + "allowance": 1000, + "rate": 1000, + "per": 1, + "expires": -1, + "quota_max": -1, + "org_id": "", + "quota_renews": 1449051461, + "quota_remaining": -1, + "quota_renewal_rate": 60, + "access_rights": { + "'"${TYK_CANDIG_API_ID}"'": { + "api_id": "'"${TYK_CANDIG_API_ID}"'", + "api_name": "'"${TYK_CANDIG_API_NAME}"'", + "Versions": ["Default"] + }, + "'"${TYK_KATSU_API_ID}"'": { + "api_id": "'"${TYK_KATSU_API_ID}"'", + "api_name": "'"${TYK_KATSU_API_NAME}"'", + "Versions": ["Default"] + } + } + }' + + curl "${TYK_LOGIN_TARGET_URL}/tyk/keys/create" -H "x-tyk-authorization: ${TYK_SECRET_KEY}" -s -H "Content-Type: application/json" -X POST -d "${tyk_key_request}" + + curl -H "x-tyk-authorization: ${TYK_SECRET_KEY}" -s "${TYK_LOGIN_TARGET_URL}/tyk/reload/group" + +} + +generate_key echo "Finished Tyk key setup" | tee -a $LOGFILE \ No newline at end of file diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh index d643bccf4..b1e665df2 100755 --- a/lib/tyk/tyk_setup.sh +++ b/lib/tyk/tyk_setup.sh @@ -11,7 +11,7 @@ LOGFILE=$PWD/tmp/progress.txt # TODO: this image uses temp dir inside the lib/tyk which deviates from convention of this repo # see Makefile.authx for other details. -CONFIG_DIR="$PWD/lib/tyk/tmp" +export CONFIG_DIR="$PWD/lib/tyk/tmp" KEYCLOAK_SECRET_VAL=$(cat $PWD/tmp/secrets/keycloak-client-local-candig-secret) export KEYCLOAK_SECRET=$KEYCLOAK_SECRET_VAL @@ -28,6 +28,9 @@ export TYK_NODE_SECRET_KEY=$TYK_NODE_SECRET_KEY_VAL TYK_ANALYTIC_ADMIN_SECRET_VAL=$(cat $PWD/tmp/secrets/tyk-analytics-admin-key) export TYK_ANALYTIC_ADMIN_SECRET=$TYK_ANALYTIC_ADMIN_SECRET_VAL +KEYCLOAK_PUBLIC_KEY_VAL=$(cat $PWD/tmp/secrets/keycloak-public-key) +export KEYCLOAK_PUBLIC_KEY=$KEYCLOAK_PUBLIC_KEY_VAL + mkdir -p $CONFIG_DIR $CONFIG_DIR/apps $CONFIG_DIR/policies $CONFIG_DIR/middleware echo "Working on tyk.conf" | tee -a $LOGFILE @@ -45,9 +48,6 @@ envsubst < ${PWD}/lib/tyk/configuration_templates/api_candig.json.tpl > ${CONFIG echo "Working on policies.json" | tee -a $LOGFILE envsubst < ${PWD}/lib/tyk/configuration_templates/policies.json.tpl > ${CONFIG_DIR}/policies/policies.json -echo "Working on key_request.json" | tee -a $LOGFILE -envsubst < ${PWD}/lib/tyk/configuration_templates/key_request.json.tpl > ${CONFIG_DIR}/key_request.json - echo "Working on tyk_analytics" | tee -a $LOGFILE envsubst < ${PWD}/lib/tyk/configuration_templates/tyk_analytics.conf.tpl > ${CONFIG_DIR}/tyk_analytics.conf @@ -65,6 +65,12 @@ cp ${PWD}/lib/tyk/configuration_templates/virtualToken.js ${CONFIG_DIR}/middlewa echo "Copying permissionsStoreMiddleware.js" | tee -a $LOGFILE cp ${PWD}/lib/tyk/configuration_templates/permissionsStoreMiddleware.js ${CONFIG_DIR}/middleware/permissionsStoreMiddleware.js +echo "Working on api_katsu_chord.json" +envsubst < ${PWD}/lib/tyk/configuration_templates/api_katsu_chord.json.tpl > ${CONFIG_DIR}/apps/api_katsu.json + +# Extra APIs can be added here +#echo "Working on api_example.json" +#envsubst < ${PWD}/lib/tyk/configuration_templates/api_example.json.tpl > ${CONFIG_DIR}/apps/api_example.json echo "Tyk configuration generated!" | tee -a $LOGFILE From 84ee5c8cdf5f8215249f01330ce14d625ac48c5f Mon Sep 17 00:00:00 2001 From: Amanjeev Sethi Date: Thu, 9 Dec 2021 16:45:26 -0500 Subject: [PATCH 027/236] Add CanDIG Data Portal to the stack (#107) * feature (candig-data-server): add git submodule for the candig-data-server service * feature (candig-data-server): add candig-data-portal service DIG-650 * feature (candig-data-server): add candig-data-portal service; add to example env; DIG-650 * docs: update README links to template, adds candig-data-portal in the list; DIG-650 * feature (candig-data-server): add health checks DIG-650 --- .gitmodules | 3 +++ README.md | 10 ++++++- etc/env/example.env | 7 ++++- lib/candig-data-portal/Dockerfile | 14 ++++++++++ lib/candig-data-portal/candig-data-portal | 1 + lib/candig-data-portal/docker-compose.yml | 33 +++++++++++++++++++++++ 6 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 lib/candig-data-portal/Dockerfile create mode 160000 lib/candig-data-portal/candig-data-portal create mode 100644 lib/candig-data-portal/docker-compose.yml diff --git a/.gitmodules b/.gitmodules index d59f89dcd..3fc4de4aa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "lib/cancogen-dashboard/cancogen_dashboard"] path = lib/cancogen-dashboard/cancogen_dashboard url = https://github.com/CanDIG/cancogen_dashboard.git +[submodule "lib/candig-data-portal/candig-data-portal"] + path = lib/candig-data-portal/candig-data-portal + url = https://github.com/CanDIG/candig-data-portal.git \ No newline at end of file diff --git a/README.md b/README.md index 8f9e56eba..8953049ae 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,14 @@ View additional Makefile options with `make help`. ## Services and Components -The follwing table lists the details from the Data Flow Diagram in the "Overview" section. +### Add new service + +New services can be added under `lib` directory. Please refer to the +[template for new services README](./lib/templates/README.md) for more details. + +### List of services + +The following table lists the details from the Data Flow Diagram in the "Overview" section. | Service/Component Name | Source | Notes | |------------------------|--------|------------------------------| @@ -168,3 +175,4 @@ The follwing table lists the details from the Data Flow Diagram in the "Overview | CHORD DRS | links | DFD: `chord_drs` | | IGV JS | links | DFD: `igv_js` | | WES Server | links | DFD: `wes_server` | +| CanDIG Data Portal | links | DFD: | \ No newline at end of file diff --git a/etc/env/example.env b/etc/env/example.env index 70f04e9f8..16ef0458e 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard #wes-server jupyter igv authentication authorization +CANDIG_MODULES=candig-data-portal weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard #wes-server jupyter igv authentication authorization CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -301,3 +301,8 @@ CANCOGEN_METADATA_URL=http://chord-metadata:8008 CANCOGEN_HTSGET_URL=http://htsget-app:3333 CANCOGEN_DRS_URL=http://chord-drs:6000 CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search + + +# candig-data-server (previously mcode) +CANDIG_DATA_PORTAL_VERSION=v0.1.0 +CANDIG_DATA_PORTAL_PORT=2543 \ No newline at end of file diff --git a/lib/candig-data-portal/Dockerfile b/lib/candig-data-portal/Dockerfile new file mode 100644 index 000000000..45bb7c205 --- /dev/null +++ b/lib/candig-data-portal/Dockerfile @@ -0,0 +1,14 @@ +FROM node:14.16.0-alpine + +LABEL Maintainer="CanDIG Project" + +RUN apk add --no-cache git +RUN apk add --no-cache curl + +COPY candig-data-portal /app/candig-data-portal + +WORKDIR /app/candig-data-portal + +RUN npm install + +ENTRYPOINT ["npm", "start"] \ No newline at end of file diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal new file mode 160000 index 000000000..54feabbbc --- /dev/null +++ b/lib/candig-data-portal/candig-data-portal @@ -0,0 +1 @@ +Subproject commit 54feabbbc87ae2f6f702183de731e405ca7dadfd diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml new file mode 100644 index 000000000..4d79cec4d --- /dev/null +++ b/lib/candig-data-portal/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.7' + +services: + candig-data-portal: + build: + context: $PWD/lib/candig-data-portal + args: + alpine_version: "${ALPINE_VERSION}" + image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} + networks: + - ${DOCKER_NET} + ports: + - "${CANDIG_DATA_PORTAL_PORT}:3000" + deploy: + placement: + constraints: + - node.role == worker + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + labels: + - "traefik.enable=true" + - "traefik.docker.lbswarm=true" + - "traefik.http.routers.candig-data-portal.rule=Host(`candig-data-portal.${CANDIG_DOMAIN}`)" + - "traefik.http.services.candig-data-portal.loadbalancer.server.port=${CANDIG_DATA_PORTAL_PORT}" + logging: *default-logging + healthcheck: + test: [ "CMD", "curl", "http://localhost:3000" ] + interval: 30s + timeout: 20s + retries: 3 \ No newline at end of file From e10ad27838436e84372be506cc9942d146a0a3a7 Mon Sep 17 00:00:00 2001 From: Amanjeev Sethi Date: Thu, 9 Dec 2021 19:40:22 -0500 Subject: [PATCH 028/236] CanDIG Data Portal - bugfixes (#108) * feature (candig-data-server): add git submodule for the candig-data-server service * feature (candig-data-server): add candig-data-portal service DIG-650 * feature (candig-data-server): add candig-data-portal service; add to example env; DIG-650 * docs: update README links to template, adds candig-data-portal in the list; DIG-650 * feature (candig-data-server): add health checks DIG-650 * feature (candig-data-portal): fixes after PR #107 DIG-650 DIG-651 * feature (candig-data-portal): fixes after PR #107 CANDIG_MODULES order fix DIG-650 DIG-651 --- .gitmodules | 2 +- etc/env/example.env | 4 ++-- lib/candig-data-portal/Dockerfile | 9 ++++----- lib/candig-data-portal/docker-compose.yml | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.gitmodules b/.gitmodules index 3fc4de4aa..465ab956f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -27,4 +27,4 @@ url = https://github.com/CanDIG/cancogen_dashboard.git [submodule "lib/candig-data-portal/candig-data-portal"] path = lib/candig-data-portal/candig-data-portal - url = https://github.com/CanDIG/candig-data-portal.git \ No newline at end of file + url = https://github.com/CanDIG/candig-data-portal.git diff --git a/etc/env/example.env b/etc/env/example.env index 16ef0458e..50ec8aef1 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=candig-data-portal weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard #wes-server jupyter igv authentication authorization +CANDIG_MODULES=weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard candig-data-portal #wes-server jupyter igv authentication authorization CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -305,4 +305,4 @@ CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search # candig-data-server (previously mcode) CANDIG_DATA_PORTAL_VERSION=v0.1.0 -CANDIG_DATA_PORTAL_PORT=2543 \ No newline at end of file +CANDIG_DATA_PORTAL_PORT=2543 diff --git a/lib/candig-data-portal/Dockerfile b/lib/candig-data-portal/Dockerfile index 45bb7c205..9ddb454f3 100644 --- a/lib/candig-data-portal/Dockerfile +++ b/lib/candig-data-portal/Dockerfile @@ -2,13 +2,12 @@ FROM node:14.16.0-alpine LABEL Maintainer="CanDIG Project" -RUN apk add --no-cache git -RUN apk add --no-cache curl +WORKDIR /app/candig-data-portal -COPY candig-data-portal /app/candig-data-portal +RUN apk add --no-cache git curl -WORKDIR /app/candig-data-portal +COPY candig-data-portal . RUN npm install -ENTRYPOINT ["npm", "start"] \ No newline at end of file +ENTRYPOINT ["npm", "start"] diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index 4d79cec4d..767099564 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -30,4 +30,4 @@ services: test: [ "CMD", "curl", "http://localhost:3000" ] interval: 30s timeout: 20s - retries: 3 \ No newline at end of file + retries: 3 From ac86bc7ec088ea5affcc24d0d6d7747bd3da6b03 Mon Sep 17 00:00:00 2001 From: Jagdeep Sason <85504663+jagdeepsason2021@users.noreply.github.com> Date: Fri, 7 Jan 2022 15:59:28 -0500 Subject: [PATCH 029/236] Pushing htsget app jenkins script (#105) --- Jenkinsfile | 25 +++++++++++++++++++++++++ lib/htsget-server/htsget_app | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 635a02929..5fa4ac061 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,9 @@ pipeline { agent any + triggers { + // Cron job gets executed every day of the working week at 4 p.m + cron('00 20 * * 1-5') + } parameters { // the choice that the automated system uses is whichever value was last chosen in "Build with Parameters" choice choices: ['registry-1.docker.io', 'ghcr.io'], description: 'URL of container registry', name: 'REGISTRY_URL' @@ -24,6 +28,27 @@ pipeline { withCredentials([usernamePassword(credentialsId: params.REGISTRY_URL, passwordVariable: 'TOKEN', usernameVariable: 'USERNAME')]) { sh('. $WORKSPACE/bin/miniconda3/etc/profile.d/conda.sh; conda activate candig; echo ${TOKEN} | docker login ${REGISTRY_URL} -u ${USERNAME} --password-stdin; make docker-push OVERRIDE_REGISTRY=${REGISTRY_URL}') } + stage('Build htsgetapp_server') { + steps { + sh ''' + cd ./lib/htsget-server/htsget_app + ls -la + bash htsget.sh + ''' + } + } + } + post { + // Status message gets sent to the Slack Channel after the job finishes + always { + success { + slackSend channel: 'automation-email', message:"Build deployed successfully" -${currentBuild.currentResult} ${env.JOB_NAME} ${env.BUILD_NUMBER} ${env.BUILD_URL} + } + failure { + slackSend channel: 'automation-email', failOnError:true message:"Build failed" -${currentBuild.currentResult} ${env.JOB_NAME} ${env.BUILD_NUMBER} ${env.BUILD_URL} + } + } + } } } } diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index f4070b5f4..2013fc325 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit f4070b5f413dc4f46319bd3ed0dc1e4301b59e36 +Subproject commit 2013fc325aeb15f89185afac6aae5df148fb6900 From 9687b58e77af63710646ca647bf67b84ba597317 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 24 Jan 2022 22:07:08 -0800 Subject: [PATCH 030/236] update submodule to catch up (#110) --- lib/chord-metadata/chord_metadata_service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 671892569..dc95528cc 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 671892569f403d4dda47880e5619206cfbd5db9f +Subproject commit dc95528ccef53d7d807bbbd346e88a16e1fa4913 From a619e40c88af873dd4a7f47cca1870f6075c79e5 Mon Sep 17 00:00:00 2001 From: daisie_local Date: Wed, 26 Jan 2022 17:31:54 -0800 Subject: [PATCH 031/236] move submodule for katsu --- lib/chord-metadata/chord_metadata_service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index dc95528cc..5c055167c 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit dc95528ccef53d7d807bbbd346e88a16e1fa4913 +Subproject commit 5c055167cf146493010422291904c57c701dbe4e From a4c5298014a22c2f1676183dd3b08672aa7de683 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 26 Jan 2022 17:38:23 -0800 Subject: [PATCH 032/236] move submodule for katsu (#111) --- lib/chord-metadata/chord_metadata_service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index dc95528cc..5c055167c 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit dc95528ccef53d7d807bbbd346e88a16e1fa4913 +Subproject commit 5c055167cf146493010422291904c57c701dbe4e From 608c5220a06e84935c5d750e608b18ca49f25e76 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 3 Feb 2022 17:07:42 -0800 Subject: [PATCH 033/236] update submod for katsu (#113) * move submodule for katsu * update katsu submod to v1.4.1 --- lib/chord-metadata/chord_metadata_service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 5c055167c..6446b21e8 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 5c055167cf146493010422291904c57c701dbe4e +Subproject commit 6446b21e834adbf225b992974d34b136e49b075d From 6c80ea458d1a326d125ac75c1beee0c3cdbbb881 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 3 Feb 2022 17:18:22 -0800 Subject: [PATCH 034/236] bump version for CHORD_METADATA_VERSION to v1.4.1 --- etc/env/example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/env/example.env b/etc/env/example.env index 50ec8aef1..2a7694a19 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -183,7 +183,7 @@ FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 # chord metadata service -CHORD_METADATA_VERSION=v1.3.0 +CHORD_METADATA_VERSION=v1.4.1 CHORD_METADATA_PORT=8008 CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false From 1574a37602eaade98015d4a5ce84f27c9b5c6081 Mon Sep 17 00:00:00 2001 From: Amanjeev Sethi Date: Mon, 7 Feb 2022 22:44:12 -0500 Subject: [PATCH 035/236] CanDIG Data Portal and Katsu API + New Tyk Middlewares (#112) * initial commit of new auth middleware * feature (candig-data-server): add git submodule for the candig-data-server service * feature (candig-data-server): add candig-data-portal service DIG-650 * feature (candig-data-server): add candig-data-portal service; add to example env; DIG-650 * docs: update README links to template, adds candig-data-portal in the list; DIG-650 * feature (candig-data-server): add health checks DIG-650 * feature (candig-data-portal): fixes after PR #107 DIG-650 DIG-651 * feature (candig-data-portal): fixes after PR #107 CANDIG_MODULES order fix DIG-650 DIG-651 * fix (traefik): version value is three digits now * fix (keycloak): script to add client in keycloak must use the name and not base64 of the name * fix (tyk): script needs some time for redis to come up If this fails in the future, add a more robust test DIG-766 * feature (data-portal): add tyk api for data-portal * doc (authmiddleware): comment for new middlware files * remove: not needed config templates from data-portal * add (tyk): new middleware from Jimmy and use them in data-portal and katsu * fix (data-portal): bug in Dockerfile to envsubst missing templates DIG-651 * fix (candig-server): removes front-end capacity from candig-server Adds backendAuthMiddleware because candig-server will only be or should only be used as the api/backend. DIG-651 * fix (chord-metadata): adds image name back in docker-compose.yml DIG-651 * update make target (#114) Co-authored-by: Jimmy Li Co-authored-by: Daisie Huang --- docs/install-docker.md | 2 +- etc/env/example.env | 10 +- lib/candig-data-portal/Dockerfile | 17 +- lib/candig-data-portal/docker-compose.yml | 5 +- lib/keycloak/keycloak_setup.sh | 4 +- lib/tyk/Dockerfile | 3 + .../configuration_templates/api_auth.json.tpl | 90 +++++----- .../api_candig.json.tpl | 13 +- .../api_katsu_chord.json.tpl | 7 +- .../api_mcode_candig_data_portal.json.tpl | 156 ++++++++++++++++++ .../configuration_templates/authMiddleware.js | 6 + .../backendAuthMiddleware.js | 36 ++++ .../frontendAuthMiddleware.js | 70 ++++++++ .../configuration_templates/policies.json.tpl | 8 + lib/tyk/tyk_key_generation.sh | 9 + lib/tyk/tyk_setup.sh | 9 + 16 files changed, 376 insertions(+), 69 deletions(-) create mode 100644 lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl create mode 100644 lib/tyk/configuration_templates/backendAuthMiddleware.js create mode 100644 lib/tyk/configuration_templates/frontendAuthMiddleware.js diff --git a/docs/install-docker.md b/docs/install-docker.md index eee6c5a27..3f7e442b4 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -140,7 +140,7 @@ make images make docker-pull # deploy stack (if using docker-compose environment) -make init-auth +make init-authx make compose # TODO: post deploy auth configuration diff --git a/etc/env/example.env b/etc/env/example.env index 2a7694a19..32a3c618f 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -91,7 +91,7 @@ CONSUL_LAN_PORT=8301 CONSUL_WAN_PORT=8302 # traefik controller -TRAEFIK_VERSION=v2.5 +TRAEFIK_VERSION=2.5.0 # enable swarm operations # options are [true, false] TRAEFIK_SWARM=false @@ -262,6 +262,13 @@ TYK_KATSU_API_SLUG=katsu TYK_KATSU_API_TARGET=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} TYK_KATSU_API_LISTEN_PATH=katsu +## api - mcode - candig-data-portal +TYK_CANDIG_DATA_PORTAL_API_ID=41 +TYK_CANDIG_DATA_PORTAL_API_NAME=CanDIG Data Portal +TYK_CANDIG_DATA_PORTAL_API_SLUG=candig-data-portal +TYK_CANDIG_DATA_PORTAL_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT} +TYK_CANDIG_DATA_PORTAL_API_LISTEN_PATH= + ## Extra APIs can be added here ## api - example #TYK_EXAMPLE_API_ID=666 @@ -306,3 +313,4 @@ CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search # candig-data-server (previously mcode) CANDIG_DATA_PORTAL_VERSION=v0.1.0 CANDIG_DATA_PORTAL_PORT=2543 +CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORT}/data-portal diff --git a/lib/candig-data-portal/Dockerfile b/lib/candig-data-portal/Dockerfile index 9ddb454f3..d95270774 100644 --- a/lib/candig-data-portal/Dockerfile +++ b/lib/candig-data-portal/Dockerfile @@ -1,13 +1,26 @@ -FROM node:14.16.0-alpine +ARG candig_data_portal_url +ARG katsu_api_target_url + +FROM node:14.16.0-alpine as build LABEL Maintainer="CanDIG Project" + +ENV CANDIG_DATA_PORTAL_URL=$candig_data_portal_url +ENV TYK_KATSU_API_TARGET=$katsu_api_target_url + + +RUN apk update +RUN apk add gettext + WORKDIR /app/candig-data-portal +ENV PATH /app/candig-data-portal/node_modules/.bin:$PATH RUN apk add --no-cache git curl COPY candig-data-portal . RUN npm install +RUN npm run build -ENTRYPOINT ["npm", "start"] +ENTRYPOINT ["npm", "start"] \ No newline at end of file diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index 767099564..eb3b10fe2 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -5,8 +5,9 @@ services: build: context: $PWD/lib/candig-data-portal args: - alpine_version: "${ALPINE_VERSION}" - image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} + candig_data_portal_url: "${CANDIG_DATA_PORTAL_URL}" + katsu_api_target_url: "${TYK_KATSU_API_TARGET}" +# image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} networks: - ${DOCKER_NET} ports: diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh index d04efc513..2b5f292f7 100644 --- a/lib/keycloak/keycloak_setup.sh +++ b/lib/keycloak/keycloak_setup.sh @@ -156,8 +156,8 @@ else fi ; echo "Setting client ${KEYCLOAK_CLIENT_ID}" | tee -a $LOGFILE -echo $KEYCLOAK_CLIENT_ID_64 -set_client "${KEYCLOAK_REALM}" "${KEYCLOAK_CLIENT_ID_64}" "${KEYCLOAK_LOGIN_REDIRECT_PATH}" +echo "${KEYCLOAK_CLIENT_ID} and base64 is ${KEYCLOAK_CLIENT_ID_64}" | tee -a $LOGFILE +set_client "${KEYCLOAK_REALM}" "${KEYCLOAK_CLIENT_ID}" "${KEYCLOAK_LOGIN_REDIRECT_PATH}" echo "Getting keycloak secret" | tee -a $LOGFILE KEYCLOAK_SECRET_RESPONSE=$(get_secret ${KEYCLOAK_REALM}) diff --git a/lib/tyk/Dockerfile b/lib/tyk/Dockerfile index 55c7896d8..ce00a5a86 100644 --- a/lib/tyk/Dockerfile +++ b/lib/tyk/Dockerfile @@ -7,6 +7,8 @@ LABEL Maintainer="CanDIG Project" # See tyk_setup.sh for the same. COPY ./tmp/tyk.conf /opt/tyk-gateway/tyk.conf COPY ./tmp/middleware/authMiddleware.js /opt/tyk-gateway/middleware/authMiddleware.js +COPY ./tmp/middleware/backendAuthMiddleware.js /opt/tyk-gateway/middleware/backendAuthMiddleware.js +COPY ./tmp/middleware/frontendAuthMiddleware.js /opt/tyk-gateway/middleware/frontendAuthMiddleware.js COPY ./tmp/middleware/permissionsStoreMiddleware.js /opt/tyk-gateway/middleware/permissionsStoreMiddleware.js COPY ./tmp/middleware/virtualLogin.js /opt/tyk-gateway/middleware/virtualLogin.js COPY ./tmp/middleware/virtualLogout.js /opt/tyk-gateway/middleware/virtualLogout.js @@ -15,6 +17,7 @@ COPY ./tmp/apps/api_candig.json /opt/tyk-gateway/apps/api_candig.json COPY ./tmp/apps/api_auth.json /opt/tyk-gateway/apps/api_auth.json COPY ./tmp/policies/policies.json /opt/tyk-gateway/policies/policies.json COPY ./tmp/apps/api_katsu.json /opt/tyk-gateway/apps/api_katsu.json +COPY ./tmp/apps/api_candig_data_portal.json /opt/tyk-gateway/apps/api_candig_data_portal.json ## Extra APIs can be added here #COPY ./tmp/apps/api_example.json /opt/tyk-gateway/apps/api_example.json diff --git a/lib/tyk/configuration_templates/api_auth.json.tpl b/lib/tyk/configuration_templates/api_auth.json.tpl index 169d456b6..eb5e2be1e 100644 --- a/lib/tyk/configuration_templates/api_auth.json.tpl +++ b/lib/tyk/configuration_templates/api_auth.json.tpl @@ -18,60 +18,60 @@ }, "auth": { - "auth_header_name": "" + "auth_header_name": "" }, "name": "${TYK_AUTH_API_NAME}", "slug": "${TYK_AUTH_API_SLUG}", "proxy": { - "target_url": "${TYK_LOGIN_TARGET_URL}/auth/login", - "strip_listen_path": false, - "listen_path": "/auth/" + "target_url": "${TYK_LOGIN_TARGET_URL}/auth/login", + "strip_listen_path": false, + "listen_path": "/auth/" }, "version_data": { - "not_versioned": true, - "versions": { - "Default": { - "name": "Default", - "use_extended_paths": true, - "extended_paths": { - "virtual": [ + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true, + "extended_paths": { + "virtual": [ { - "response_function_name": "logoutHandler", - "function_source_type": "file", - "function_source_uri": "middleware/virtualLogout.js", - "path": "logout", - "method": "GET", - "use_session": false, - "proxy_on_error": false - }, - { - "response_function_name": "tokenHandler", - "function_source_type": "file", - "function_source_uri": "middleware/virtualToken.js", - "path": "token", - "method": "POST", - "use_session": false, - "proxy_on_error": false - }, - { - "response_function_name": "loginHandler", - "function_source_type": "file", - "function_source_uri": "middleware/virtualLogin.js", - "path": "login", - "method": "GET", - "use_session": false, - "proxy_on_error": false - } - ], - "do_not_track_endpoints": [{ - "path": "token", - "method": "POST" - }] - } - } - } + "response_function_name": "logoutHandler", + "function_source_type": "file", + "function_source_uri": "middleware/virtualLogout.js", + "path": "logout", + "method": "GET", + "use_session": false, + "proxy_on_error": false + }, + { + "response_function_name": "tokenHandler", + "function_source_type": "file", + "function_source_uri": "middleware/virtualToken.js", + "path": "token", + "method": "POST", + "use_session": false, + "proxy_on_error": false + }, + { + "response_function_name": "loginHandler", + "function_source_type": "file", + "function_source_uri": "middleware/virtualLogin.js", + "path": "login", + "method": "GET", + "use_session": false, + "proxy_on_error": false + } + ], + "do_not_track_endpoints": [{ + "path": "token", + "method": "POST" + }] + } + } + } } } diff --git a/lib/tyk/configuration_templates/api_candig.json.tpl b/lib/tyk/configuration_templates/api_candig.json.tpl index a41f62952..40c73e4f7 100644 --- a/lib/tyk/configuration_templates/api_candig.json.tpl +++ b/lib/tyk/configuration_templates/api_candig.json.tpl @@ -55,8 +55,8 @@ "custom_middleware": { "pre": [ { - "name": "authMiddleware", - "path": "/opt/tyk-gateway/middleware/authMiddleware.js", + "name": "backendAuthMiddleware", + "path": "/opt/tyk-gateway/middleware/backendAuthMiddleware.js", "require_session": false } ], @@ -83,15 +83,6 @@ }, "config_data": { - "SESSION_ENDPOINTS": [ - "/candig", - "/candig/gene_search", - "/candig/patients_overview", - "/candig/sample_analysis", - "/candig/custom_visualization", - "/candig/api_info", - "/candig/serverinfo" - ], "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", diff --git a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl index 69e44acb8..79f7d581f 100644 --- a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl +++ b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl @@ -55,8 +55,8 @@ "custom_middleware": { "pre": [ { - "name": "authMiddleware", - "path": "/opt/tyk-gateway/middleware/authMiddleware.js", + "name": "backendAuthMiddleware", + "path": "/opt/tyk-gateway/middleware/backendAuthMiddleware.js", "require_session": false } ], @@ -83,9 +83,6 @@ }, "config_data": { - "SESSION_ENDPOINTS": [ - "/katsu" - ], "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", diff --git a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl new file mode 100644 index 000000000..0832901c4 --- /dev/null +++ b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl @@ -0,0 +1,156 @@ +{ + "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", + "name": "${TYK_CANDIG_DATA_PORTAL_API_NAME}", + "use_openid": true, + "active": true, + "slug": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", + + "enable_signature_checking": false, + + "jwt_issued_at_validation_skew": 0, + "jwt_expires_at_validation_skew": 0, + "upstream_certificates": {}, + "use_keyless": false, + "enable_coprocess_auth": false, + "base_identity_provided_by": "", + + "proxy": { + "target_url": "${TYK_CANDIG_DATA_PORTAL_API_TARGET}", + "strip_listen_path": true, + "disable_strip_slash": false, + "listen_path": "/${TYK_CANDIG_DATA_PORTAL_API_LISTEN_PATH}", + "transport": { + "ssl_insecure_skip_verify": false, + "ssl_ciphers": [], + "ssl_min_version": 0, + "proxy_url": "" + }, + "target_list": [], + "preserve_host_header": false + }, + + "version_data": { + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true + } + }, + "extended_paths": { + "ignored": [ + { + "path": "${KEYCLOAK_LOGIN_REDIRECT_PATH}", + "method_actions": { + "GET": { + "action": "no_action", + "code": 200, + "headers": {} + } + } + } + ] + } + }, + "custom_middleware": { + "pre": [ + { + "name": "frontendAuthMiddleware", + "path": "/opt/tyk-gateway/middleware/frontendAuthMiddleware.js", + "require_session": false + } + ], + "post": [ + { + "name": "permissionsStoreMiddleware", + "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", + "require_session": false + } + ], + "id_extractor": { + "extract_with": "", + "extract_from": "", + "extractor_config": {} + }, + "driver": "", + "auth_check": { + "path": "", + "require_session": false, + "name": "" + }, + "post_key_auth": [], + "response": [] + }, + + "config_data": { + "SESSION_ENDPOINTS": [ + "/data-portal" + ], + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", + "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", + "VAULT_ROLE":"researcher" + }, + "openid_options": { + "segregate_by_client": false, + "providers": [ + { + "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "client_ids": { + "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" + } + } + ] + }, + + + "definition": { + "location": "header", + "key": "x-api-version" + }, + + + "internal": false, + "jwt_skip_kid": false, + "enable_batch_request_support": false, + "response_processors": [], + "use_mutual_tls_auth": false, + "basic_auth": { + "disable_caching": false, + "cache_ttl": 0, + "extract_from_body": false, + "body_user_regexp": "", + "body_password_regexp": "" + }, + "use_standard_auth": false, + "session_lifetime": 0, + "use_oauth2": false, + "jwt_source": "", + "jwt_signing_method": "", + "jwt_not_before_validation_skew": 0, + "jwt_identity_base_field": "", + + "session_provider": { + "name": "", + "storage_engine": "", + "meta": {} + }, + + "auth": { + "use_param": false, + "param_name": "", + "use_cookie": false, + "cookie_name": "", + "auth_header_name": "", + "use_certificate": false, + "validate_signature": false, + "signature": { + "algorithm": "", + "header": "", + "secret": "", + "allowed_clock_skew": 0, + "error_code": 0, + "error_message": "" + } + } +} diff --git a/lib/tyk/configuration_templates/authMiddleware.js b/lib/tyk/configuration_templates/authMiddleware.js index ccba3f510..8ac75fa69 100644 --- a/lib/tyk/configuration_templates/authMiddleware.js +++ b/lib/tyk/configuration_templates/authMiddleware.js @@ -1,4 +1,10 @@ // ---- Authentication middleware ----- +// This is an older version of the auth middleware which is used by services +// that have both API and frontend components. For those to be deployed by +// CanDIG infrastructure, use this middleware in the Tyk API setting +// Else use the specific middlewares -- +// backendAuthMiddleware.js : for backend services +// frontendAuthMiddleware.js: for frontend services, esp React etc. var authMiddleware = new TykJS.TykMiddleware.NewMiddleware({}); diff --git a/lib/tyk/configuration_templates/backendAuthMiddleware.js b/lib/tyk/configuration_templates/backendAuthMiddleware.js new file mode 100644 index 000000000..544038b84 --- /dev/null +++ b/lib/tyk/configuration_templates/backendAuthMiddleware.js @@ -0,0 +1,36 @@ +// ---- Backend Authentication middleware ----- + +var backendAuthMiddleware = new TykJS.TykMiddleware.NewMiddleware({}); + +function getCookie(request, cookie_name) { + if (!("Cookie" in request.Headers)) { + return undefined; + } + var splitCookie = request.Headers["Cookie"][0].split("; "); + var valueCookie = _.find(splitCookie, function(cookie) { + if (cookie.indexOf(cookie_name+"=") > -1) { + return cookie + } + }); + + return valueCookie +} + +backendAuthMiddleware.NewProcessRequest(function(request, session, spec) { + // log("Running Authorization JSVM middleware") + + if (request.Headers["Authorization"] === undefined) { + try { + var tokenCookie = getCookie(request, "session_id") + var idToken = tokenCookie.split("=")[1]; + request.SetHeaders["Authorization"] = "Bearer " + idToken; + } catch(err) { + log(err) + var tokenCookie = undefined + } + } + + return backendAuthMiddleware.ReturnData(request, session.meta_data); +}); + +log("Authorization middleware initialised"); diff --git a/lib/tyk/configuration_templates/frontendAuthMiddleware.js b/lib/tyk/configuration_templates/frontendAuthMiddleware.js new file mode 100644 index 000000000..c83f8d29d --- /dev/null +++ b/lib/tyk/configuration_templates/frontendAuthMiddleware.js @@ -0,0 +1,70 @@ +// ---- Frontend Authentication middleware ----- + +var frontendAuthMiddleware = new TykJS.TykMiddleware.NewMiddleware({}); + +function getCookie(request, cookie_name) { + if (!("Cookie" in request.Headers)) { + return undefined; + } + var splitCookie = request.Headers["Cookie"][0].split("; "); + var valueCookie = _.find(splitCookie, function(cookie) { + if (cookie.indexOf(cookie_name+"=") > -1) { + return cookie + } + }); + + return valueCookie +} + +function isTokenExpired(idToken) { + tokenPayload = idToken.split(".")[1] + padding = tokenPayload.length % 4 + + if (padding != 0) { + _.times(4-padding, function() { + tokenPayload += "=" + }) + } + + decodedPayload = JSON.parse(b64dec(tokenPayload)) + tokenExpires = decodedPayload["exp"] + sysTime = (new Date).getTime()/1000 | 0; + + return sysTime>tokenExpires +} + +frontendAuthMiddleware.NewProcessRequest(function(request, session, spec) { + // log("Running Authorization JSVM middleware") + + if (request.Headers["Authorization"] === undefined) { + try { + var tokenCookie = getCookie(request, "session_id") + } catch(err) { + log(err) + var tokenCookie = undefined + } + + if (tokenCookie != undefined) { + var idToken = tokenCookie.split("=")[1]; + + if (isTokenExpired(idToken)) { + request.ReturnOverrides.ResponseCode = 302; + request.ReturnOverrides.ResponseHeaders = { + "Location": spec.config_data.TYK_SERVER + "/auth/login?app_url=" + request.URL + }; + } else { + request.SetHeaders["Authorization"] = "Bearer " + idToken; + } + + } else { + request.ReturnOverrides.ResponseCode = 302 + request.ReturnOverrides.ResponseHeaders = { + "Location": spec.config_data.TYK_SERVER + "/auth/login?app_url=" + request.URL + } + } + } + + return frontendAuthMiddleware.ReturnData(request, session.meta_data); +}); + +log("Authorization middleware initialised"); diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 6706c4317..b74eb729c 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -16,6 +16,14 @@ "versions": [ "Default" ] + }, + "${TYK_CANDIG_DATA_PORTAL_API_ID}": { + "allowed_urls": [], + "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", + "api_name": "${TYK_CANDIG_DATA_PORTAL_API_NAME}", + "versions": [ + "Default" + ] } }, "active": true, diff --git a/lib/tyk/tyk_key_generation.sh b/lib/tyk/tyk_key_generation.sh index 5deca62ed..e37e8dd5f 100644 --- a/lib/tyk/tyk_key_generation.sh +++ b/lib/tyk/tyk_key_generation.sh @@ -7,6 +7,10 @@ LOGFILE=$PWD/tmp/progress.txt echo "Starting Tyk key setup, post launch" | tee -a $LOGFILE +echo "Lets wait for Redis and Tyk to get up" | tee -a $LOGFILE +# if this fails in the future add a more robust test +sleep 5; + TYK_SECRET_KEY_VAL=$(cat $PWD/tmp/secrets/tyk-secret-key) export TYK_SECRET_KEY=$TYK_SECRET_KEY_VAL @@ -34,6 +38,11 @@ generate_key() { "api_id": "'"${TYK_KATSU_API_ID}"'", "api_name": "'"${TYK_KATSU_API_NAME}"'", "Versions": ["Default"] + }, + "'"${TYK_CANDIG_DATA_PORTAL_API_ID}"'": { + "api_id": "'"${TYK_CANDIG_DATA_PORTAL_API_ID}"'", + "api_name": "'"${TYK_CANDIG_DATA_PORTAL_API_NAME}"'", + "Versions": ["Default"] } } }' diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh index b1e665df2..86e4f9478 100755 --- a/lib/tyk/tyk_setup.sh +++ b/lib/tyk/tyk_setup.sh @@ -39,6 +39,12 @@ envsubst < ${PWD}/lib/tyk/configuration_templates/tyk.conf.tpl > ${CONFIG_DIR}/t echo "Working on authMiddleware.js" | tee -a $LOGFILE envsubst < ${PWD}/lib/tyk/configuration_templates/authMiddleware.js > ${CONFIG_DIR}/middleware/authMiddleware.js +echo "Working on backendAuthMiddleware.js" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/backendAuthMiddleware.js > ${CONFIG_DIR}/middleware/backendAuthMiddleware.js + +echo "Working on frontendAuthMiddleware.js" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/frontendAuthMiddleware.js > ${CONFIG_DIR}/middleware/frontendAuthMiddleware.js + echo "Working on api_auth.json" | tee -a $LOGFILE envsubst < ${PWD}/lib/tyk/configuration_templates/api_auth.json.tpl > ${CONFIG_DIR}/apps/api_auth.json @@ -68,6 +74,9 @@ cp ${PWD}/lib/tyk/configuration_templates/permissionsStoreMiddleware.js ${CONFIG echo "Working on api_katsu_chord.json" envsubst < ${PWD}/lib/tyk/configuration_templates/api_katsu_chord.json.tpl > ${CONFIG_DIR}/apps/api_katsu.json +echo "Working on api_candig_data_portal.json" +envsubst < ${PWD}/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl > ${CONFIG_DIR}/apps/api_candig_data_portal.json + # Extra APIs can be added here #echo "Working on api_example.json" #envsubst < ${PWD}/lib/tyk/configuration_templates/api_example.json.tpl > ${CONFIG_DIR}/apps/api_example.json From 2290301e38d1edc36b71a96a599d9305eadd22f9 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 7 Feb 2022 19:49:59 -0800 Subject: [PATCH 036/236] Add a note about updating hosts --- docs/install-docker.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/install-docker.md b/docs/install-docker.md index 3f7e442b4..411d79dfb 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -149,6 +149,15 @@ docker login make docker-push ``` +## Update hosts + +Get your local IP address and edit your /etc/hosts file to add: + +```bash + docker.localhost + auth.docker.localhost +``` + ## Deploy CanDIGv2 Services (Swarm) > Note: swarm deployment requires minimum 2 nodes connected (1 manager, 1 worker) From 04ad4ea0fd16340e133dbe07f020ee4be79b1630 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 14 Feb 2022 13:01:06 -0800 Subject: [PATCH 037/236] Fix names of secrets files in compose/docker-compose (#115) * move submodule for katsu * fix names of secrets files --- lib/compose/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index e9d633db7..1f1d95c7e 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -43,9 +43,9 @@ secrets: aws-credentials: file: $PWD/tmp/secrets/aws-credentials federation-peers: - file: $PWD/lib/federation-service/federation_service/configs/federation-peers.json + file: $PWD/lib/federation-service/federation_service/configs/peers.json federation-services: - file: $PWD/lib/federation-service/federation_service/configs/federation-services.json + file: $PWD/lib/federation-service/federation_service/configs/services.json metadata-app-secret: file: $PWD/tmp/secrets/metadata-app-secret metadata-db-user: From 32700ce5a8e60d688d50fef0ba9b767f93f2f25c Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 14 Feb 2022 18:03:59 -0800 Subject: [PATCH 038/236] Update htsget submodule (#116) * move submodule for katsu * update htsget submodule --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 2013fc325..2136b35e0 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 2013fc325aeb15f89185afac6aae5df148fb6900 +Subproject commit 2136b35e0807fc5230931bc62f6d78a6c3a959e2 From 6f3f9cbe0f8df1b804a9851df861931c26eecb29 Mon Sep 17 00:00:00 2001 From: AliRZ-02 Date: Tue, 8 Mar 2022 09:28:11 -0500 Subject: [PATCH 039/236] DIG-772: GraphQL Integration with CanDIGv2 stack - Part 2 (#117) * DIG-772: Initial Commit for GraphQL integration with CanDIGv2 stack * Renamed Docker Compose File & Updated Branch of GraphQL Submodule * Added extra config variables * Fixed Configuration for GraphQL Interface * Modified Authx Makefile & Updated Submodule * Removed unneeded vault addition to Makefile * Changed Formatting * Changes to logging stack and updates to GQL-i * Modified Logging Config * Fluentd Logging Changes for GraphQL-interface * Logging Changes due to Formatting * Submodule Updates * Changes to Fluentd Logging and Submodule Updates * Fixed Fluentd Logging Regex & Submodule Updates * Fluentd Configuration Changes * Config Changes * Submodule changes * Submodule Updates and Config Changes --- .gitmodules | 3 + etc/env/example.env | 20 ++- lib/cancogen-dashboard/cancogen_dashboard | 2 +- lib/graphql/GraphQL-interface | 1 + lib/graphql/docker-compose.yml | 35 ++++ lib/logging/fluentd/Dockerfile | 4 + lib/logging/fluentd/fluent.conf | 80 ++++++--- lib/tyk/Dockerfile | 1 + .../api_graphql.json.tpl | 153 ++++++++++++++++++ .../configuration_templates/policies.json.tpl | 8 + lib/tyk/tyk_key_generation.sh | 5 + lib/tyk/tyk_setup.sh | 3 + 12 files changed, 290 insertions(+), 25 deletions(-) create mode 160000 lib/graphql/GraphQL-interface create mode 100644 lib/graphql/docker-compose.yml create mode 100644 lib/tyk/configuration_templates/api_graphql.json.tpl diff --git a/.gitmodules b/.gitmodules index 465ab956f..de099d598 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "lib/candig-data-portal/candig-data-portal"] path = lib/candig-data-portal/candig-data-portal url = https://github.com/CanDIG/candig-data-portal.git +[submodule "lib/graphql/GraphQL-interface"] + path = lib/graphql/GraphQL-interface + url = https://github.com/CanDIG/GraphQL-interface.git diff --git a/etc/env/example.env b/etc/env/example.env index 32a3c618f..9f4a44a7d 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard candig-data-portal #wes-server jupyter igv authentication authorization +CANDIG_MODULES= weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard candig-data-portal graphql #wes-server jupyter igv authentication authorization CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -269,6 +269,13 @@ TYK_CANDIG_DATA_PORTAL_API_SLUG=candig-data-portal TYK_CANDIG_DATA_PORTAL_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT} TYK_CANDIG_DATA_PORTAL_API_LISTEN_PATH= +## api - graphql +TYK_GRAPHQL_API_ID=51 +TYK_GRAPHQL_API_NAME=GraphQL-Interface +TYK_GRAPHQL_API_SLUG=graphql-interface +TYK_GRAPHQL_API_TARGET=http://${CANDIG_DOMAIN}:${GRAPHQL_INTERFACE_PORT} +TYK_GRAPHQL_API_LISTEN_PATH=graphql + ## Extra APIs can be added here ## api - example #TYK_EXAMPLE_API_ID=666 @@ -314,3 +321,14 @@ CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search CANDIG_DATA_PORTAL_VERSION=v0.1.0 CANDIG_DATA_PORTAL_PORT=2543 CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORT}/data-portal + +# graphql-interface +GRAPHQL_PYTHON_VERSION=3.8 +GRAPHQL_INTERFACE_VERSION=v1.0.0 +GRAPHQL_INTERFACE_PORT=7999 + +GRAPHQL_KATSU_API=http://${TYK_LOGIN_TARGET_URL}/${TYK_KATSU_API_LISTEN_PATH}/api +GRAPHQL_CANDIG_SERVER=http://${TYK_LOGIN_TARGET_URL}/${TYK_CANDIG_API_LISTEN_PATH} +GRAPHQL_BEACON_ID=com.candig.graphql +GRAPHQL_KATSU_TOKEN_KEY=authorization +GRAPHQL_CANDIG_TOKEN_KEY=authorization diff --git a/lib/cancogen-dashboard/cancogen_dashboard b/lib/cancogen-dashboard/cancogen_dashboard index 1efb52759..f5d70b17c 160000 --- a/lib/cancogen-dashboard/cancogen_dashboard +++ b/lib/cancogen-dashboard/cancogen_dashboard @@ -1 +1 @@ -Subproject commit 1efb52759c4a6bde2920766f74452ece8431d5d7 +Subproject commit f5d70b17cdf92d38dc795b5309ab5433d083449e diff --git a/lib/graphql/GraphQL-interface b/lib/graphql/GraphQL-interface new file mode 160000 index 000000000..75fab0b73 --- /dev/null +++ b/lib/graphql/GraphQL-interface @@ -0,0 +1 @@ +Subproject commit 75fab0b73b69173c97dfaccc6f8645d989ca373a diff --git a/lib/graphql/docker-compose.yml b/lib/graphql/docker-compose.yml new file mode 100644 index 000000000..2d15b96c1 --- /dev/null +++ b/lib/graphql/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.7' + +services: + graphql: + build: + context: $PWD/lib/graphql/GraphQL-interface + args: + GRAPHQL_PYTHON_VERSION: "${GRAPHQL_PYTHON_VERSION}" + image: ${DOCKER_REGISTRY}/graphql:${GRAPHQL_INTERFACE_VERSION:-latest} + networks: + - ${DOCKER_NET} + ports: + - "${GRAPHQL_INTERFACE_PORT}:7999" + deploy: + placement: + constraints: + - node.role == worker + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + labels: + - "traefik.enable=true" + - "traefik.docker.lbswarm=true" + - "traefik.http.routers.graphql.rule=Host(`graphql.${CANDIG_DOMAIN}`)" + - "traefik.http.routers.graphql.entrypoints=${TRAEFIK_ENTRYPOINT}" + - "traefik.http.services.graphql.loadbalancer.server.port=${GRAPHQL_INTERFACE_PORT}" + logging: *default-logging + environment: + - GRAPHQL_KATSU_API=${GRAPHQL_KATSU_API} + - GRAPHQL_CANDIG_SERVER=${GRAPHQL_CANDIG_SERVER} + - GRAPHQL_BEACON_ID=${GRAPHQL_BEACON_ID} + - GRAPHQL_KATSU_TOKEN_KEY=${GRAPHQL_KATSU_TOKEN_KEY} + - GRAPHQL_CANDIG_TOKEN_KEY=${GRAPHQL_CANDIG_TOKEN_KEY} diff --git a/lib/logging/fluentd/Dockerfile b/lib/logging/fluentd/Dockerfile index 2355d6772..98fd4b6f7 100644 --- a/lib/logging/fluentd/Dockerfile +++ b/lib/logging/fluentd/Dockerfile @@ -6,6 +6,10 @@ LABEL Maintainer="CanDIG Project" USER root +RUN gem uninstall elasticsearch && gem install elasticsearch -v 7.17.0 + +RUN gem install fluent-plugin-concat + RUN apk add --no-cache --update --virtual .build-deps sudo build-base ruby-dev \ && sudo gem install fluent-plugin-elasticsearch strptime \ && sudo gem sources --clear-all \ diff --git a/lib/logging/fluentd/fluent.conf b/lib/logging/fluentd/fluent.conf index ff0e91787..f0c3727bf 100644 --- a/lib/logging/fluentd/fluent.conf +++ b/lib/logging/fluentd/fluent.conf @@ -11,29 +11,63 @@ + + \ No newline at end of file + diff --git a/lib/tyk/Dockerfile b/lib/tyk/Dockerfile index ce00a5a86..b48646515 100644 --- a/lib/tyk/Dockerfile +++ b/lib/tyk/Dockerfile @@ -18,6 +18,7 @@ COPY ./tmp/apps/api_auth.json /opt/tyk-gateway/apps/api_auth.json COPY ./tmp/policies/policies.json /opt/tyk-gateway/policies/policies.json COPY ./tmp/apps/api_katsu.json /opt/tyk-gateway/apps/api_katsu.json COPY ./tmp/apps/api_candig_data_portal.json /opt/tyk-gateway/apps/api_candig_data_portal.json +COPY ./tmp/apps/api_graphql.json /opt/tyk-gateway/apps/api_graphql.json ## Extra APIs can be added here #COPY ./tmp/apps/api_example.json /opt/tyk-gateway/apps/api_example.json diff --git a/lib/tyk/configuration_templates/api_graphql.json.tpl b/lib/tyk/configuration_templates/api_graphql.json.tpl new file mode 100644 index 000000000..631292bc0 --- /dev/null +++ b/lib/tyk/configuration_templates/api_graphql.json.tpl @@ -0,0 +1,153 @@ +{ + "api_id": "${TYK_GRAPHQL_API_ID}", + "name": "${TYK_GRAPHQL_API_NAME}", + "use_openid": true, + "active": true, + "slug": "${TYK_GRAPHQL_API_SLUG}", + + "enable_signature_checking": false, + + "jwt_issued_at_validation_skew": 0, + "jwt_expires_at_validation_skew": 0, + "upstream_certificates": {}, + "use_keyless": false, + "enable_coprocess_auth": false, + "base_identity_provided_by": "", + + "proxy": { + "target_url": "${TYK_GRAPHQL_API_TARGET}", + "strip_listen_path": true, + "disable_strip_slash": false, + "listen_path": "/${TYK_GRAPHQL_API_LISTEN_PATH}", + "transport": { + "ssl_insecure_skip_verify": false, + "ssl_ciphers": [], + "ssl_min_version": 0, + "proxy_url": "" + }, + "target_list": [], + "preserve_host_header": false + }, + + "version_data": { + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true + } + }, + "extended_paths": { + "ignored": [ + { + "path": "${KEYCLOAK_LOGIN_REDIRECT_PATH}", + "method_actions": { + "GET": { + "action": "no_action", + "code": 200, + "headers": {} + } + } + } + ] + } + }, + "custom_middleware": { + "pre": [ + { + "name": "frontendAuthMiddleware", + "path": "/opt/tyk-gateway/middleware/frontendAuthMiddleware.js", + "require_session": false + } + ], + "post": [ + { + "name": "permissionsStoreMiddleware", + "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", + "require_session": false + } + ], + "id_extractor": { + "extract_with": "", + "extract_from": "", + "extractor_config": {} + }, + "driver": "", + "auth_check": { + "path": "", + "require_session": false, + "name": "" + }, + "post_key_auth": [], + "response": [] + }, + + "config_data": { + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", + "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", + "VAULT_ROLE":"researcher" + }, + "openid_options": { + "segregate_by_client": false, + "providers": [ + { + "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "client_ids": { + "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" + } + } + ] + }, + + + "definition": { + "location": "header", + "key": "x-api-version" + }, + + + "internal": false, + "jwt_skip_kid": false, + "enable_batch_request_support": false, + "response_processors": [], + "use_mutual_tls_auth": false, + "basic_auth": { + "disable_caching": false, + "cache_ttl": 0, + "extract_from_body": false, + "body_user_regexp": "", + "body_password_regexp": "" + }, + "use_standard_auth": false, + "session_lifetime": 0, + "use_oauth2": false, + "jwt_source": "", + "jwt_signing_method": "", + "jwt_not_before_validation_skew": 0, + "jwt_identity_base_field": "", + + "session_provider": { + "name": "", + "storage_engine": "", + "meta": {} + }, + + "auth": { + "use_param": false, + "param_name": "", + "use_cookie": false, + "cookie_name": "", + "auth_header_name": "", + "use_certificate": false, + "validate_signature": false, + "signature": { + "algorithm": "", + "header": "", + "secret": "", + "allowed_clock_skew": 0, + "error_code": 0, + "error_message": "" + } + } +} diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index b74eb729c..5b23e3244 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -24,6 +24,14 @@ "versions": [ "Default" ] + }, + "${TYK_GRAPHQL_API_ID}": { + "allowed_urls": [], + "api_id": "${TYK_GRAPHQL_API_ID}", + "api_name": "${TYK_GRAPHQL_API_NAME}", + "versions": [ + "Default" + ] } }, "active": true, diff --git a/lib/tyk/tyk_key_generation.sh b/lib/tyk/tyk_key_generation.sh index e37e8dd5f..dbd607080 100644 --- a/lib/tyk/tyk_key_generation.sh +++ b/lib/tyk/tyk_key_generation.sh @@ -43,6 +43,11 @@ generate_key() { "api_id": "'"${TYK_CANDIG_DATA_PORTAL_API_ID}"'", "api_name": "'"${TYK_CANDIG_DATA_PORTAL_API_NAME}"'", "Versions": ["Default"] + }, + "'"${TYK_GRAPHQL_API_ID}"'": { + "api_id": "'"${TYK_GRAPHQL_API_ID}"'", + "api_name": "'"${TYK_GRAPHQL_API_NAME}"'", + "Versions": ["Default"] } } }' diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh index 86e4f9478..c3a2a96f0 100755 --- a/lib/tyk/tyk_setup.sh +++ b/lib/tyk/tyk_setup.sh @@ -77,6 +77,9 @@ envsubst < ${PWD}/lib/tyk/configuration_templates/api_katsu_chord.json.tpl > ${C echo "Working on api_candig_data_portal.json" envsubst < ${PWD}/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl > ${CONFIG_DIR}/apps/api_candig_data_portal.json +echo "Working on api_graphql.json" +envsubst < ${PWD}/lib/tyk/configuration_templates/api_graphql.json.tpl > ${CONFIG_DIR}/apps/api_graphql.json + # Extra APIs can be added here #echo "Working on api_example.json" #envsubst < ${PWD}/lib/tyk/configuration_templates/api_example.json.tpl > ${CONFIG_DIR}/apps/api_example.json From e95628fbaf6254e47d5e6c0dbdbadc9bd81aab97 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 16 Mar 2022 12:14:30 -0700 Subject: [PATCH 040/236] Integrating OPA into the stack (#119) * build vault and opa * tweaks to catch up with current infrastructure * variables and opa setup tweaks * Opa doesn't need self certs * Opa doesn't need self certs * Opa doesn't need self certs * vault setup tweaks * making submodule for opa * update katsu * add second keycloak user on setup * rename rego_dev_playground to candig-opa * set proper secrets for client-secret for opa * update keycloak_setup to add client-scopes and mappers * having opa in compose prevents multi-service compose * remove unnecessary ssl-cert * best to compose before init-auth * don't build traefik * update to new integrated candig_opa * add cleanup tweaks for clean-authx * env vars for vault_setup * fetch keys and restart opa after build * set test user 1 to trusted_researcher * use env var in a few more places * opa submodule tweak * remove old auth stuff from candig-server * move a bunch of modules to not be default --- .gitmodules | 3 + Makefile | 12 +- Makefile.authx | 44 ++++++- docs/install-docker.md | 2 +- etc/env/example.env | 17 ++- lib/candig-server/authorization/Dockerfile | 4 - .../candig-server.policy.rego.tpl | 53 -------- lib/candig-server/docker-compose.yml | 36 ------ lib/chord-metadata/chord_metadata_service | 2 +- lib/chord-metadata/docker-compose.yml | 5 + lib/compose/docker-compose.yml | 8 ++ lib/keycloak/keycloak_setup.sh | 119 +++++++++++++++--- lib/opa/docker-compose.yml | 72 +++++++++++ lib/opa/opa | 1 + lib/opa/opa_setup.sh | 14 +++ lib/tyk/tyk_setup.sh | 4 +- .../vault-config.json.tpl | 2 +- lib/vault/docker-compose.yml | 4 +- lib/vault/vault_setup.sh | 42 +++---- 19 files changed, 295 insertions(+), 149 deletions(-) delete mode 100644 lib/candig-server/authorization/Dockerfile delete mode 100644 lib/candig-server/authorization/configuration_templates/candig-server.policy.rego.tpl create mode 100644 lib/opa/docker-compose.yml create mode 160000 lib/opa/opa create mode 100644 lib/opa/opa_setup.sh diff --git a/.gitmodules b/.gitmodules index de099d598..3b0e987d4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,6 +28,9 @@ [submodule "lib/candig-data-portal/candig-data-portal"] path = lib/candig-data-portal/candig-data-portal url = https://github.com/CanDIG/candig-data-portal.git +[submodule "lib/opa/opa"] + path = lib/opa/opa + url = https://github.com/CanDIG/candig-opa.git [submodule "lib/graphql/GraphQL-interface"] path = lib/graphql/GraphQL-interface url = https://github.com/CanDIG/GraphQL-interface.git diff --git a/Makefile b/Makefile index c06d44750..742ae948d 100644 --- a/Makefile +++ b/Makefile @@ -395,7 +395,7 @@ compose-%: echo " started compose-$*" >> $(LOGFILE) cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ $(DIR)/lib/$*/docker-compose.yml \ - | docker-compose -f - up -d $(SERVICE) + | docker-compose -f - up -d echo " finished compose-$*" >> $(LOGFILE) @@ -443,7 +443,7 @@ docker-push: #<<< .PHONY: docker-secrets -docker-secrets: minio-secrets +docker-secrets: mkdir minio-secrets @echo admin > $(DIR)/tmp/secrets/portainer-user $(MAKE) secret-portainer-secret $(MAKE) secret-metadata-app-secret @@ -454,9 +454,12 @@ docker-secrets: minio-secrets @echo admin > $(DIR)/tmp/secrets/keycloak-admin-user $(MAKE) secret-keycloak-admin-password - @echo user > $(DIR)/tmp/secrets/keycloak-test-user + @echo user1 > $(DIR)/tmp/secrets/keycloak-test-user $(MAKE) secret-keycloak-test-user-password + @echo user2 > $(DIR)/tmp/secrets/keycloak-test-user2 + $(MAKE) secret-keycloak-test-user2-password + $(MAKE) secret-tyk-secret-key $(MAKE) secret-tyk-node-secret-key $(MAKE) secret-tyk-analytics-admin-key @@ -484,6 +487,7 @@ docker-volumes: docker volume create tyk-data docker volume create tyk-redis-data docker volume create vault-data + docker volume create opa-data #>>> @@ -526,7 +530,7 @@ init-conda: #<<< .PHONY: init-docker -init-docker: docker-networks docker-volumes ssl-cert docker-secrets +init-docker: docker-networks docker-volumes docker-secrets #>>> diff --git a/Makefile.authx b/Makefile.authx index 22cad4fd7..74aded367 100644 --- a/Makefile.authx +++ b/Makefile.authx @@ -7,11 +7,40 @@ clean-keycloak: cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ $(DIR)/lib/keycloak/docker-compose.yml | docker-compose -f - down + # - remove intermittent docker images + @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'keycloak' | xargs -I{} docker rmi --force {} + + docker volume rm keycloak-data + +#>>> +# close vault services +# make clean-vault + +#<<< +clean-vault: cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/tyk/docker-compose.yml | docker-compose -f - down + $(DIR)/lib/vault/docker-compose.yml | docker-compose -f - down # - remove intermittent docker images - @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'keycloak' | xargs -I{} docker rmi --force {} + @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'vault' | xargs -I{} docker rmi --force {} + + docker volume rm vault-data + + +#>>> +# close opa services +# make clean-opa + +#<<< +clean-opa: + cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ + $(DIR)/lib/opa/docker-compose.yml | docker-compose -f - down + + # - remove intermittent docker images + @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'openpolicyagent/opa' | xargs -I{} docker rmi --force {} + @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'opa-runner' | xargs -I{} docker rmi --force {} + + docker volume rm opa-data #>>> @@ -38,7 +67,7 @@ clean-tyk: # make clean-authx #<<< -clean-authx: clean-keycloak clean-tyk +clean-authx: clean-keycloak clean-tyk clean-vault clean-opa #>>> @@ -63,6 +92,15 @@ init-authx: mkdir echo ; \ $(MAKE) compose-tyk; \ source ${PWD}/lib/tyk/tyk_key_generation.sh; \ + echo "Setting up Vault"; \ + export SERVICE=vault; \ + $(MAKE) build-vault; \ + source ${PWD}/lib/vault/vault_setup.sh; \ + echo "Setting up Opa"; \ + export SERVICE=opa; \ + $(MAKE) build-opa; \ + $(MAKE) compose-opa; \ + source ${PWD}/lib/opa/opa_setup.sh; \ echo ; diff --git a/docs/install-docker.md b/docs/install-docker.md index 411d79dfb..c4164ffaa 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -140,8 +140,8 @@ make images make docker-pull # deploy stack (if using docker-compose environment) -make init-authx make compose +make init-authx # TODO: post deploy auth configuration # push updated images to $DOCKER_REGISTRY (optional) diff --git a/etc/env/example.env b/etc/env/example.env index 9f4a44a7d..a7a009749 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES= weavescope portainer consul traefik logging monitoring minio drs-server htsget-server chord-metadata datasets cnv-service rnaget candig-server federation-service cancogen-dashboard candig-data-portal graphql #wes-server jupyter igv authentication authorization +CANDIG_MODULES=weavescope minio drs-server htsget-server chord-metadata candig-server federation-service candig-data-portal #wes-server jupyter igv authentication authorization traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -188,6 +188,14 @@ CHORD_METADATA_PORT=8008 CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false CHORD_METADATA_DEBUG=false +CHORD_METADATA_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} + +# candig-specific katsu +INSIDE_CANDIG=true +CANDIG_AUTHORIZATION=OPA +CANDIG_AUTHZ_SERVICE_PORT=8182 +CACHE_TIME=0 + # cnv service CNV_SERVICE_HOST=0.0.0.0 @@ -228,6 +236,7 @@ KEYCLOAK_PRIVATE_PROTO=http KEYCLOAK_PUBLIC_URL=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_PORT} KEYCLOAK_PUBLIC_URL_PROD=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN} KEYCLOAK_PRIVATE_URL=${KEYCLOAK_PRIVATE_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_CONTAINER_PORT} +KEYCLOAK_REALM_URL=${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM} KEYCLOAK_GENERATE_TEST_USER=0 @@ -264,7 +273,7 @@ TYK_KATSU_API_LISTEN_PATH=katsu ## api - mcode - candig-data-portal TYK_CANDIG_DATA_PORTAL_API_ID=41 -TYK_CANDIG_DATA_PORTAL_API_NAME=CanDIG Data Portal +TYK_CANDIG_DATA_PORTAL_API_NAME="CanDIG Data Portal" TYK_CANDIG_DATA_PORTAL_API_SLUG=candig-data-portal TYK_CANDIG_DATA_PORTAL_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT} TYK_CANDIG_DATA_PORTAL_API_LISTEN_PATH= @@ -302,8 +311,8 @@ OPA_PORT=8181 OPA_LOG_LEVEL=debug #TODO: consolidate opa public and private domains OPA_URL=http://opa:${OPA_PORT} - -CANDIG_AUTHZ_SERVICE_PORT=8182 +CANDIG_OPA_SECRET=my-secret-root-token +CANDIG_OPA_SECRET_SERVICE=my-secret-service-token # cancogen_dashboard diff --git a/lib/candig-server/authorization/Dockerfile b/lib/candig-server/authorization/Dockerfile deleted file mode 100644 index a20a07ae1..000000000 --- a/lib/candig-server/authorization/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -ARG BASE_IMAGE -FROM ${BASE_IMAGE} - -COPY ./tmp/policy.rego /policy.rego \ No newline at end of file diff --git a/lib/candig-server/authorization/configuration_templates/candig-server.policy.rego.tpl b/lib/candig-server/authorization/configuration_templates/candig-server.policy.rego.tpl deleted file mode 100644 index 07791bb7b..000000000 --- a/lib/candig-server/authorization/configuration_templates/candig-server.policy.rego.tpl +++ /dev/null @@ -1,53 +0,0 @@ -package permissions - -import input - -now := time.now_ns()/1000000000 - -default allowed = false - -default iss = "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}" -default aud = "${KEYCLOAK_CLIENT_ID}" - -default full_authn_pk=`-----BEGIN PUBLIC KEY----- -${KEYCLOAK_PUBLIC_KEY} ------END PUBLIC KEY-----` - -#default full_authz_pk=`-----BEGIN PUBLIC KEY----- -#${VAULT_PUBLIC_KEY} -#-----END PUBLIC KEY-----` - -##default authz_jwks=`${VAULT_JWKS}` - -allowed = true { - # retrieve authentication token parts - [authN_token_header, authN_token_payload, authN_token_signature] := io.jwt.decode(input.kcToken) - - # retrieve authorization token parts - [authZ_token_header, authZ_token_payload, authZ_token_signature] := io.jwt.decode(input.vaultToken) - - # retrieve rotated authZ jwks from the arbiter - rotated_authz_jwks := input.authZjwks - - # Verify authentication token signature - authN_token_is_valid := io.jwt.verify_rs256(input.kcToken, full_authn_pk) - - # Verify authentication token signature - # (disabled until vault key rotation accommodated for) - authZ_token_is_valid := io.jwt.verify_rs256(input.vaultToken, rotated_authz_jwks) - - - all([ - # Authentication - authN_token_is_valid == true, - authN_token_payload.aud == aud, - authN_token_payload.iss == iss, - authN_token_payload.iat < now, - - # Authorization - authZ_token_is_valid == true, - ##authZ_token_payload.aud == aud, - ##authZ_token_payload.iss == iss, - authZ_token_payload.iat < now, - ]) -} diff --git a/lib/candig-server/docker-compose.yml b/lib/candig-server/docker-compose.yml index f4f1b93e1..97e8ffe24 100644 --- a/lib/candig-server/docker-compose.yml +++ b/lib/candig-server/docker-compose.yml @@ -29,39 +29,3 @@ services: - "traefik.http.services.candig-server.loadbalancer.server.port=${CANDIG_SERVER_PORT}" logging: *default-logging command: ["--host", "0.0.0.0", "--port", "3000"] - - candig-server-opa: - build: - context: ${PWD}/lib/candig-server/authorization - args: - - BASE_IMAGE=openpolicyagent/opa:${OPA_VERSION} - image: ${DOCKER_REGISTRY}/candig-server-opa:${CANDIG_SERVER_VERSION} - networks: - - ${DOCKER_NET} - ports: - - "${CANDIG_AUTHZ_SERVICE_PORT}:8181" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.candig-server-opa.rule=Host(`candig-server-opa.${CANDIG_DOMAIN}`)" - - "traefik.http.services.candig-server-opa.loadbalancer.server.port=${CANDIG_SERVER_PORT}" - logging: *default-logging - command: - - "run" - - "--server" - - "--log-level=${OPA_LOG_LEVEL}" - - "/policy.rego" - healthcheck: - test: ["CMD", "curl", "-f", "http://0.0.0.0:${CANDIG_AUTHZ_SERVICE_PORT}/health"] - interval: 30s - timeout: 20s - retries: 3 diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 6446b21e8..6d58fd722 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 6446b21e834adbf225b992974d34b136e49b075d +Subproject commit 6d58fd7225c1c91d8cf428135c4df757cff3003d diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 5097eb921..d8c12d183 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -36,6 +36,11 @@ services: - POSTGRES_HOST=metadata-db - POSTGRES_USER=admin - POSTGRES_PASSWORD_FILE=/run/secrets/metadata_db_secret + - INSIDE_CANDIG=${INSIDE_CANDIG} + - CANDIG_OPA_URL=${OPA_URL} + - CANDIG_AUTHORIZATION=${CANDIG_AUTHORIZATION} + - CACHE_TIME=${CACHE_TIME} + - CANDIG_OPA_SECRET=${CANDIG_OPA_SECRET} secrets: - source: metadata-settings target: /app/chord_metadata_service/metadata/settings.py diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index 1f1d95c7e..181619709 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -32,6 +32,8 @@ volumes: external: true keycloak-data: external: true + opa-data: + external: true tyk-data: external: true tyk-redis-data: @@ -72,3 +74,9 @@ secrets: file: $PWD/tmp/secrets/keycloak-admin-user keycloak-admin-password: file: $PWD/tmp/secrets/keycloak-admin-password + keycloak-test-user: + file: $PWD/tmp/secrets/keycloak-test-user + keycloak-test-user-password: + file: $PWD/tmp/secrets/keycloak-test-user-password + keycloak-client-local-candig-secret: + file: $PWD/tmp/secrets/keycloak-client-${KEYCLOAK_CLIENT_ID}-secret diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh index 2b5f292f7..5f3f01e9f 100644 --- a/lib/keycloak/keycloak_setup.sh +++ b/lib/keycloak/keycloak_setup.sh @@ -21,12 +21,42 @@ add_user() { # CANDIG_AUTH_DOMAIN is the name of the keycloak server inside the compose network local username=$1 local password=$2 + local attribute=$3 - echo " Adding user ${username}" | tee -a $LOGFILE - docker exec ${CANDIG_AUTH_DOMAIN} /opt/jboss/keycloak/bin/add-user-keycloak.sh -u ${username} -p ${password} -r ${KEYCLOAK_REALM} + local JSON=' { + "username": "'${username}'", + "enabled": true, + "attributes": { + "'${attribute}'": [ + "true" + ] + }, + "access": { + "manageGroupMembership": true, + "view": true, + "mapRoles": true, + "impersonate": true, + "manage": true + } + }' - echo " Restarting the keycloak container" | tee -a $LOGFILE - docker restart ${CANDIG_AUTH_DOMAIN} + user_id=`curl --stderr - \ + -i -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X POST -H "Content-Type: application/json" -d "${JSON}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/users" -k | grep "Location:" \ + | sed -E s/.*users.\([a-z0-9-]+\).*/\\\1/` + + echo "Created user ${user_id}" | tee -a $LOGFILE + + local password_json=' { + "type": "rawPassword", + "value": "'${password}'" + }' + echo "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/users/${user_id}" + curl \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X PUT -H "Content-Type: application/json" -d "${password_json}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/users/${user_id}/reset-password" -k } get_token() { @@ -48,7 +78,7 @@ set_realm() { local realm=$1 local JSON='{ - "realm": "candig", + "realm": "'${realm}'", "enabled": true }' @@ -79,9 +109,54 @@ set_client() { local client=$2 local redirect=$3 + # add client scope with protocol mappers + scope_json='{ + "name": "'${realm}'", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "name": "'${client}'-audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "'${client}'", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "trusted_researcher", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "trusted_researcher", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "trusted_researcher", + "jsonType.label": "boolean" + } + } + ] + }' + + new_scope=`curl --stderr - \ + -i -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X POST -H "Content-Type: application/json" -d "${scope_json}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/client-scopes" -k | grep "Location:" \ + | sed -E s/.*client-scopes.\([a-z0-9-]+\).*/\\\\1/` + + echo "Created client scope ${new_scope}" | tee -a $LOGFILE + # Will add / to listen only if it is present - local JSON='{ + local client_json='{ "clientId": "'"${client}"'", "enabled": true, "protocol": "openid-connect", @@ -102,14 +177,24 @@ set_client() { "saml.server.signature": "false", "saml.server.signature.keyinfo.ext": "false", "saml_force_name_id_format": "false" - } + }, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "'${realm}'", + "email" + ] }' - curl \ - -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ - -X POST -H "Content-Type: application/json" -d "${JSON}" \ - "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/clients" -k + new_client=`curl --stderr - \ + -i -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X POST -H "Content-Type: application/json" -d "${client_json}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/clients" -k | grep "Location:" \ + | sed -E s/.*clients.\([a-z0-9-]+\).*/\\\\1/` # TODO: security issue fix this, -k flag above ignores cert, even if the url is https + + echo "Created client ${new_client}" | tee -a $LOGFILE } get_secret() { @@ -139,9 +224,9 @@ KEYCLOAK_TOKEN=$(get_token) echo "Creating Realm ${KEYCLOAK_REALM}" | tee -a $LOGFILE set_realm ${KEYCLOAK_REALM} -echo "Setting client in base64" | tee -a $LOGFILE +echo "Setting client ${KEYCLOAK_CLIENT_ID} in base64" | tee -a $LOGFILE export KEYCLOAK_CLIENT_ID_64=$(echo -n ${KEYCLOAK_CLIENT_ID} | base64) -echo $KEYCLOAK_CLIENT_ID_64 > tmp/secrets/keycloak-client-local-candig-id-64 +echo $KEYCLOAK_CLIENT_ID_64 > tmp/secrets/keycloak-client-$KEYCLOAK_CLIENT_ID-id-64 echo "Remove ports on prod" | tee -a $LOGFILE if [[ ${KEYCLOAK_PUBLIC_URL} == *":443"* ]]; then @@ -156,13 +241,12 @@ else fi ; echo "Setting client ${KEYCLOAK_CLIENT_ID}" | tee -a $LOGFILE -echo "${KEYCLOAK_CLIENT_ID} and base64 is ${KEYCLOAK_CLIENT_ID_64}" | tee -a $LOGFILE set_client "${KEYCLOAK_REALM}" "${KEYCLOAK_CLIENT_ID}" "${KEYCLOAK_LOGIN_REDIRECT_PATH}" echo "Getting keycloak secret" | tee -a $LOGFILE KEYCLOAK_SECRET_RESPONSE=$(get_secret ${KEYCLOAK_REALM}) export KEYCLOAK_SECRET=$KEYCLOAK_SECRET_RESPONSE -echo $KEYCLOAK_SECRET > tmp/secrets/keycloak-client-local-candig-secret | tee -a $LOGFILE +echo $KEYCLOAK_SECRET > tmp/secrets/keycloak-client-$KEYCLOAK_CLIENT_ID-secret | tee -a $LOGFILE echo "Getting keycloak public key" | tee -a $LOGFILE KEYCLOAK_PUBLIC_KEY_RESPONSE=$(get_public_key ${KEYCLOAK_REALM}) @@ -172,9 +256,12 @@ echo $KEYCLOAK_PUBLIC_KEY > tmp/secrets/keycloak-public-key | tee -a $LOGFILE if [[ ${KEYCLOAK_GENERATE_TEST_USER} == 1 ]]; then echo "Adding test user" | tee -a $LOGFILE - add_user "$(cat tmp/secrets/keycloak-test-user)" "$(cat tmp/secrets/keycloak-test-user-password)" + add_user "$(cat tmp/secrets/keycloak-test-user)" "$(cat tmp/secrets/keycloak-test-user-password)" "trusted_researcher" + add_user "$(cat tmp/secrets/keycloak-test-user2)" "$(cat tmp/secrets/keycloak-test-user2-password)" "stranger" fi +#set_trusted_researcher "$(cat tmp/secrets/keycloak-test-user)" + echo "Waiting for keycloak to restart" | tee -a $LOGFILE while ! docker logs --tail 5 ${CANDIG_AUTH_DOMAIN} | grep "Admin console listening on http://127.0.0.1:9990"; do sleep 1; done echo "Keycloak setup done!" | tee -a $LOGFILE diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml new file mode 100644 index 000000000..83bb4a2f0 --- /dev/null +++ b/lib/opa/docker-compose.yml @@ -0,0 +1,72 @@ +version: '3.7' +services: + opa-runner: + build: + context: $PWD/lib/opa/opa + args: + venv_python: "${VENV_PYTHON}" + alpine_version: "${ALPINE_VERSION}" + katsu_url: "${CHORD_METADATA_PUBLIC_URL}" + idp: "${KEYCLOAK_REALM_URL}" + client_id: "${KEYCLOAK_CLIENT_ID}" + networks: + - ${DOCKER_NET} + deploy: + placement: + constraints: + - node.role == worker + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + logging: *default-logging + volumes: + - opa-data:/app + secrets: + - source: keycloak-client-local-candig-secret + target: idp_client_secret + environment: + IDP_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} + CLIENT_SECRET_ROOT: ${CANDIG_OPA_SECRET} + CLIENT_SECRET_SERVICE: ${CANDIG_OPA_SECRET_SERVICE} + OPA_URL: ${OPA_URL} + opa: + image: openpolicyagent/opa:latest + ports: + - "${OPA_PORT}:8181" + volumes: + - opa-data:/app + environment: + CLIENT_SECRET_ROOT: ${CANDIG_OPA_SECRET} + CLIENT_SECRET_SERVICE: ${CANDIG_OPA_SECRET_SERVICE} + IDP: ${KEYCLOAK_REALM_URL} + command: + - "run" + - "--server" + - "--addr" + - "${OPA_URL}" + - "--log-level=debug" + - "--authentication=token" + - "--authorization=basic" + - "app/permissions_engine/authz.rego" + - "app/permissions_engine/permissions.rego" + - "app/permissions_engine/idp.rego" + - "app/data.json" + networks: + - bridge-net + deploy: + placement: + constraints: + - node.role == manager + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + logging: *default-logging + healthcheck: + test: [ "CMD", "curl", "-f", "${OPA_URL}" ] + interval: 30s + timeout: 20s + retries: 3 diff --git a/lib/opa/opa b/lib/opa/opa new file mode 160000 index 000000000..7c4a603e4 --- /dev/null +++ b/lib/opa/opa @@ -0,0 +1 @@ +Subproject commit 7c4a603e49d9bb4cbc6304934435612e6401646b diff --git a/lib/opa/opa_setup.sh b/lib/opa/opa_setup.sh new file mode 100644 index 000000000..6abbdb308 --- /dev/null +++ b/lib/opa/opa_setup.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +LOGFILE=$PWD/tmp/progress.txt + + +opa_runner=$(docker ps | grep "opa-runner" | awk '{print $1}') + +docker exec $opa_runner python3 app/permissions_engine/fetch_keys.py + +opa=$(docker ps | grep "candigv2_opa_1" | awk '{print $1}') + +docker restart $opa diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh index c3a2a96f0..b1ca57b89 100755 --- a/lib/tyk/tyk_setup.sh +++ b/lib/tyk/tyk_setup.sh @@ -13,10 +13,10 @@ LOGFILE=$PWD/tmp/progress.txt # see Makefile.authx for other details. export CONFIG_DIR="$PWD/lib/tyk/tmp" -KEYCLOAK_SECRET_VAL=$(cat $PWD/tmp/secrets/keycloak-client-local-candig-secret) +KEYCLOAK_SECRET_VAL=$(cat $PWD/tmp/secrets/keycloak-client-${KEYCLOAK_CLIENT_ID}-secret) export KEYCLOAK_SECRET=$KEYCLOAK_SECRET_VAL -KEYCLOAK_CLIENT_ID_64_VAL=$(cat $PWD/tmp/secrets/keycloak-client-local-candig-id-64) +KEYCLOAK_CLIENT_ID_64_VAL=$(cat $PWD/tmp/secrets/keycloak-client-${KEYCLOAK_CLIENT_ID}-id-64) export KEYCLOAK_CLIENT_ID_64=$KEYCLOAK_CLIENT_ID_64_VAL TYK_SECRET_KEY_VAL=$(cat $PWD/tmp/secrets/tyk-secret-key) diff --git a/lib/vault/configuration_templates/vault-config.json.tpl b/lib/vault/configuration_templates/vault-config.json.tpl index 273112405..75fc55398 100644 --- a/lib/vault/configuration_templates/vault-config.json.tpl +++ b/lib/vault/configuration_templates/vault-config.json.tpl @@ -11,4 +11,4 @@ } }, "ui": ${VAULT_UI} -} \ No newline at end of file +} diff --git a/lib/vault/docker-compose.yml b/lib/vault/docker-compose.yml index c1c63285e..fc8d4b707 100644 --- a/lib/vault/docker-compose.yml +++ b/lib/vault/docker-compose.yml @@ -5,7 +5,7 @@ services: build: context: ${PWD}/lib/vault args: - - VAULT_VERSION=1.5.0 + - VAULT_VERSION=1.9.3 - venv_python=${VENV_PYTHON} image: ${DOCKER_REGISTRY}/vault:${VAULT_VERSION:-latest} ports: @@ -38,4 +38,4 @@ services: test: [ "CMD", "curl", "-f", "http://0.0.0.0:${VAULT_SERVICE_PORT}/ui/" ] interval: 30s timeout: 20s - retries: 3 \ No newline at end of file + retries: 3 diff --git a/lib/vault/vault_setup.sh b/lib/vault/vault_setup.sh index 7c60cb2e5..984d461e8 100755 --- a/lib/vault/vault_setup.sh +++ b/lib/vault/vault_setup.sh @@ -1,5 +1,6 @@ -#! /usr/bin/env bash -set -e +#!/usr/bin/env bash + +set -Eexo pipefail # This script will set up a full vault environment on your local CanDIGv2 cluster @@ -12,14 +13,14 @@ set -e # https://stackoverflow.com/questions/35703317/docker-exec-write-text-to-file-in-container # https://www.vaultproject.io/api-docs/secret/identity/entity#batch-delete-entities -mkdir -p ${PWD}/lib/authorization/vault/tmp +mkdir -p ${PWD}/lib/vault/tmp # vault-config.json echo "Working on vault-config.json .." -envsubst < ${PWD}/lib/authorization/vault/configuration_templates/vault-config.json.tpl > ${PWD}/lib/authorization/vault/tmp/vault-config.json +envsubst < ${PWD}/lib/vault/configuration_templates/vault-config.json.tpl > ${PWD}/lib/vault/tmp/vault-config.json # boot container -export SERVICE=vault && make compose-authorization +make compose-vault # -- todo: run only if not already initialized -- # --- temp @@ -28,6 +29,7 @@ sleep 3 # --- # gather keys and login token +echo ">> gathering keys" vault=$(docker ps | grep vault | awk '{print $1}') stuff=$(docker exec $vault sh -c "vault operator init") # | head -7 | rev | cut -d " " -f1 | rev) echo "found stuff as ${stuff}" @@ -47,13 +49,13 @@ echo "found key5: ${key_5}" echo "found root: ${key_root}" # save keys -touch ${PWD}/lib/authorization/vault/tmp/keys.txt -echo -e "key1: ${key_1}" >> ${PWD}/lib/authorization/vault/tmp/keys.txt -echo -e "key2: ${key_2}" >> ${PWD}/lib/authorization/vault/tmp/keys.txt -echo -e "key3: ${key_3}" >> ${PWD}/lib/authorization/vault/tmp/keys.txt -echo -e "key4: ${key_4}" >> ${PWD}/lib/authorization/vault/tmp/keys.txt -echo -e "key5: ${key_5}" >> ${PWD}/lib/authorization/vault/tmp/keys.txt -echo -e "root: ${key_root}" >> ${PWD}/lib/authorization/vault/tmp/keys.txt +touch ${PWD}/lib/vault/tmp/keys.txt +echo -e "key1: ${key_1}" >> ${PWD}/lib/vault/tmp/keys.txt +echo -e "key2: ${key_2}" >> ${PWD}/lib/vault/tmp/keys.txt +echo -e "key3: ${key_3}" >> ${PWD}/lib/vault/tmp/keys.txt +echo -e "key4: ${key_4}" >> ${PWD}/lib/vault/tmp/keys.txt +echo -e "key5: ${key_5}" >> ${PWD}/lib/vault/tmp/keys.txt +echo -e "root: ${key_root}" >> ${PWD}/lib/vault/tmp/keys.txt echo ">> attempting to automatically unseal vault:" @@ -91,16 +93,14 @@ docker exec $vault sh -c "vault write auth/jwt/role/researcher user_claim=prefer # configure jwt echo echo ">> configuring jwt stuff" - -docker exec $vault sh -c "vault write auth/jwt/config oidc_discovery_url=\"${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/candig\" bound_issuer=\"${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/candig\" default_role=\"researcher\"" +docker exec $vault sh -c "vault write auth/jwt/config oidc_discovery_url=\"${KEYCLOAK_PUBLIC_URL}/auth/realms/candig\" bound_issuer=\"${KEYCLOAK_PUBLIC_URL}/auth/realms/candig\" default_role=\"researcher\"" # create users echo +KEYCLOAK_TEST_USER="$(cat tmp/secrets/keycloak-test-user)" echo ">> creating user $KEYCLOAK_TEST_USER" - -export TEMPLATE_USER=$(echo $KEYCLOAK_TEST_USER) export TEMPLATE_DATASET_PERMISSIONS=4 -TEST_USER_PERMISSIONS_DATASTRUCTURE=$(envsubst < ${PWD}/lib/authorization/vault/configuration_templates/vault-entity-entitlements.json.tpl) +TEST_USER_PERMISSIONS_DATASTRUCTURE=$(envsubst < ${PWD}/lib/vault/configuration_templates/vault-entity-entitlements.json.tpl) test_user_output=$(docker exec $vault sh -c "echo '${TEST_USER_PERMISSIONS_DATASTRUCTURE}' > ${KEYCLOAK_TEST_USER}.json; vault write identity/entity @${KEYCLOAK_TEST_USER}.json; rm ${KEYCLOAK_TEST_USER}.json;") @@ -108,12 +108,10 @@ ENTITY_ID=$(echo "${test_user_output}" | grep id | awk '{print $2}') echo ">>> found entity id : ${ENTITY_ID}" -echo +KEYCLOAK_TEST_USER_TWO="$(cat tmp/secrets/keycloak-test-user2)" echo ">> creating user $KEYCLOAK_TEST_USER_TWO" - -export TEMPLATE_USER=$(echo $KEYCLOAK_TEST_USER_TWO) export TEMPLATE_DATASET_PERMISSIONS=1 -TEST_USER_TWO_PERMISSIONS_DATASTRUCTURE=$(envsubst < ${PWD}/lib/authorization/vault/configuration_templates/vault-entity-entitlements.json.tpl) +TEST_USER_TWO_PERMISSIONS_DATASTRUCTURE=$(envsubst < ${PWD}/lib/vault/configuration_templates/vault-entity-entitlements.json.tpl) test_user_output_two=$(docker exec $vault sh -c "echo '${TEST_USER_TWO_PERMISSIONS_DATASTRUCTURE}' > ${KEYCLOAK_TEST_USER_TWO}.json; vault write identity/entity @${KEYCLOAK_TEST_USER_TWO}.json; rm ${KEYCLOAK_TEST_USER_TWO}.json;") @@ -156,7 +154,7 @@ echo ">> matching key and inserting custom info into the jwt" # json escaped or base64 escaped string and the braces have to be spaced apart # because templating code requres {{}} which when followed by another brace # messes up Vault and it complains that there is a mismatch in balance of braces -VAULT_IDENTITY_ROLE_TEMPLATE=$(envsubst < ${PWD}/lib/authorization/vault/configuration_templates/vault-datastructure.json.tpl) +VAULT_IDENTITY_ROLE_TEMPLATE=$(envsubst < ${PWD}/lib/vault/configuration_templates/vault-datastructure.json.tpl) docker exec $vault sh -c "echo '${VAULT_IDENTITY_ROLE_TEMPLATE}' > researcher.json; vault write identity/oidc/role/researcher @researcher.json; rm researcher.json;" echo From b30948d4f1f4718e0a684993cda3bb1dc47717b5 Mon Sep 17 00:00:00 2001 From: daisie_local Date: Wed, 23 Mar 2022 15:47:03 -0700 Subject: [PATCH 041/236] corresponding move for https://github.com/CanDIG/candig-opa/pull/1701 --- lib/opa/docker-compose.yml | 1 + lib/opa/opa | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index 83bb4a2f0..5320b0793 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -53,6 +53,7 @@ services: - "app/permissions_engine/permissions.rego" - "app/permissions_engine/idp.rego" - "app/data.json" + - "app/permissions_engine/access.json" networks: - bridge-net deploy: diff --git a/lib/opa/opa b/lib/opa/opa index 7c4a603e4..91e9aaf9d 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 7c4a603e49d9bb4cbc6304934435612e6401646b +Subproject commit 91e9aaf9d2896cfab1729542f43cd9a8fddf5387 From cfafba3c36716b8b6470107f80661d2601a01f35 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 23 Mar 2022 15:49:38 -0700 Subject: [PATCH 042/236] Move Opa datasets permissions to separate file (#120) * corresponding move for https://github.com/CanDIG/candig-opa/pull/1701 * remove SERVICE lines --- Makefile.authx | 5 ----- lib/opa/docker-compose.yml | 1 + lib/opa/opa | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Makefile.authx b/Makefile.authx index 74aded367..dd9bd6b3b 100644 --- a/Makefile.authx +++ b/Makefile.authx @@ -84,20 +84,16 @@ init-authx: mkdir # move to a better solution so Aman made a decision to keep this hack as-is. $(MAKE) docker-volumes echo "Setting up Keycloak"; \ - export SERVICE=keycloak; \ source ${PWD}/lib/keycloak/keycloak_setup.sh; \ echo "Setting up Tyk"; \ - export SERVICE=tyk; \ ${PWD}/lib/tyk/tyk_setup.sh; \ echo ; \ $(MAKE) compose-tyk; \ source ${PWD}/lib/tyk/tyk_key_generation.sh; \ echo "Setting up Vault"; \ - export SERVICE=vault; \ $(MAKE) build-vault; \ source ${PWD}/lib/vault/vault_setup.sh; \ echo "Setting up Opa"; \ - export SERVICE=opa; \ $(MAKE) build-opa; \ $(MAKE) compose-opa; \ source ${PWD}/lib/opa/opa_setup.sh; \ @@ -118,7 +114,6 @@ redeploy-tyk: mkdir # in its own shell. So to pass the environment from previous commands, one has # to do this backslash dance. This could have been resolved but we decided to # move to a better solution so Aman made a decision to keep this hack as-is. - export SERVICE=tyk; \ source ${PWD}/lib/tyk/tyk_setup.sh; \ echo ; \ $(MAKE) compose-tyk; \ diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index 83bb4a2f0..5320b0793 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -53,6 +53,7 @@ services: - "app/permissions_engine/permissions.rego" - "app/permissions_engine/idp.rego" - "app/data.json" + - "app/permissions_engine/access.json" networks: - bridge-net deploy: diff --git a/lib/opa/opa b/lib/opa/opa index 7c4a603e4..91e9aaf9d 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 7c4a603e49d9bb4cbc6304934435612e6401646b +Subproject commit 91e9aaf9d2896cfab1729542f43cd9a8fddf5387 From a1993c8302500a0cd0682d5d8334b43b0d3bb815 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 28 Mar 2022 13:02:29 -0700 Subject: [PATCH 043/236] pass env vars in docker-compose --- lib/htsget-server/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index 360b44078..fa503fc70 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -7,6 +7,8 @@ services: args: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" + opa_secret: "${CANDIG_OPA_SECRET}" + opa_url: "${OPA_URL}" image: ${DOCKER_REGISTRY}/htsget-app:${HTSGET_APP_VERSION:-latest} networks: - ${DOCKER_NET} From 5b57310ec20ddc812fbc9430035cdfc10069a28d Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 28 Mar 2022 16:08:33 -0700 Subject: [PATCH 044/236] pass CANDIG_AUTHORIZATION in to Dockerfile --- lib/htsget-server/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index fa503fc70..e29237d26 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -9,6 +9,7 @@ services: alpine_version: "${ALPINE_VERSION}" opa_secret: "${CANDIG_OPA_SECRET}" opa_url: "${OPA_URL}" + candig_auth: "${CANDIG_AUTHORIZATION}" image: ${DOCKER_REGISTRY}/htsget-app:${HTSGET_APP_VERSION:-latest} networks: - ${DOCKER_NET} From 41e0c4e7187046d8a13562b9c1f4a85c5b16e725 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 29 Mar 2022 23:28:20 -0700 Subject: [PATCH 045/236] Update candig-server deployment to use Opa (#122) * corresponding move for https://github.com/CanDIG/candig-opa/pull/1701 * Move Opa datasets permissions to separate file (#120) * corresponding move for https://github.com/CanDIG/candig-opa/pull/1701 * remove SERVICE lines * use config file * tweaks * update dockerfile for candig-server * Update opa * bump candig-server-version to 1.5.0 --- etc/env/example.env | 2 +- lib/candig-server/Dockerfile | 10 +++++++--- lib/candig-server/config.py | 10 ++++++++++ lib/candig-server/docker-compose.yml | 7 ++++++- lib/opa/opa | 2 +- 5 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 lib/candig-server/config.py diff --git a/etc/env/example.env b/etc/env/example.env index a7a009749..17eee94e8 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -202,7 +202,7 @@ CNV_SERVICE_HOST=0.0.0.0 CNV_SERVICE_PORT=8870 # candig server -CANDIG_SERVER_VERSION=1.4.0 +CANDIG_SERVER_VERSION=1.5.0 CANDIG_SERVER_HOST=0.0.0.0 CANDIG_SERVER_PORT=3001 CANDIG_INGEST_VERSION=1.5.0 diff --git a/lib/candig-server/Dockerfile b/lib/candig-server/Dockerfile index 06de69176..5e08ac685 100644 --- a/lib/candig-server/Dockerfile +++ b/lib/candig-server/Dockerfile @@ -7,16 +7,20 @@ ARG candig_ingest LABEL Maintainer="CanDIG Project" -RUN mkdir -p /app/candig-server +COPY candig-server /app/candig-server WORKDIR /app/candig-server +COPY config.py /app/candig-server + RUN apt-get update && \ - apt-get install -y build-essential zlib1g-dev wget && \ + apt-get install -y build-essential zlib1g-dev wget git && \ apt-get autoclean && \ apt-get autoremove -y -RUN pip install --no-cache-dir candig-server==${candig_version} candig-ingest==${candig_ingest} +RUN pip install --no-cache-dir candig-ingest==${candig_ingest} +RUN pip install -r requirements.txt +RUN pip install /app/candig-server # Uncomment below lines if you want to ingest some mock data RUN mkdir candig-example-data && \ diff --git a/lib/candig-server/config.py b/lib/candig-server/config.py new file mode 100644 index 000000000..0706961a1 --- /dev/null +++ b/lib/candig-server/config.py @@ -0,0 +1,10 @@ +import os + + +OPA_SERVER = os.environ['OPA_SERVER']+"/v1/data/permissions/datasets" +OPA_SERVER_TOKEN = os.environ['OPA_SERVER_TOKEN'] + +TYK_ENABLED = True +TYK_SERVER = os.environ['TYK_SERVER'] +TYK_LISTEN_PATH = os.environ['TYK_LISTEN_PATH'] +ACCESS_LIST = "access_list.txt" diff --git a/lib/candig-server/docker-compose.yml b/lib/candig-server/docker-compose.yml index 97e8ffe24..a282a0426 100644 --- a/lib/candig-server/docker-compose.yml +++ b/lib/candig-server/docker-compose.yml @@ -28,4 +28,9 @@ services: - "traefik.http.routers.candig-server.rule=Host(`candig-server.${CANDIG_DOMAIN}`)" - "traefik.http.services.candig-server.loadbalancer.server.port=${CANDIG_SERVER_PORT}" logging: *default-logging - command: ["--host", "0.0.0.0", "--port", "3000"] + environment: + - OPA_SERVER=${OPA_URL} + - OPA_SERVER_TOKEN=${CANDIG_OPA_SECRET} + - TYK_SERVER=${TYK_CANDIG_API_TARGET} + - TYK_LISTEN_PATH=${TYK_CANDIG_API_LISTEN_PATH} + command: ["--host", "0.0.0.0", "--port", "3000", "--config-file", "/app/candig-server/config.py"] diff --git a/lib/opa/opa b/lib/opa/opa index 91e9aaf9d..fb216d237 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 91e9aaf9d2896cfab1729542f43cd9a8fddf5387 +Subproject commit fb216d2372d38c6b81bf096d8dbac2dd0ae5a83b From c6c00efd593ab6b4631d9c407f8522357565e254 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 29 Mar 2022 23:35:41 -0700 Subject: [PATCH 046/236] HTSGET uses Opa to authorize user access to datasets (#121) * corresponding move for https://github.com/CanDIG/candig-opa/pull/1701 * pass env vars in docker-compose * pass CANDIG_AUTHORIZATION in to Dockerfile * this pr needs the htsget changes --- lib/htsget-server/docker-compose.yml | 3 +++ lib/htsget-server/htsget_app | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index 360b44078..e29237d26 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -7,6 +7,9 @@ services: args: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" + opa_secret: "${CANDIG_OPA_SECRET}" + opa_url: "${OPA_URL}" + candig_auth: "${CANDIG_AUTHORIZATION}" image: ${DOCKER_REGISTRY}/htsget-app:${HTSGET_APP_VERSION:-latest} networks: - ${DOCKER_NET} diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 2136b35e0..ce6586e43 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 2136b35e0807fc5230931bc62f6d78a6c3a959e2 +Subproject commit ce6586e438fa668a0fcb26c7fbadd5f74ec1a0d5 From 29f29ed39a62f0f2bb33a7323193ad25ae86b3da Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Mon, 4 Apr 2022 14:52:04 -0400 Subject: [PATCH 047/236] fixes for keycloak container port, candig-server build disable (#123) Co-authored-by: Shaikh Rashid --- etc/env/example.env | 6 +++--- lib/candig-server/docker-compose.yml | 12 ++++++------ lib/keycloak/docker-compose.yml | 4 ++-- lib/opa/docker-compose.yml | 2 +- .../configuration_templates/api_katsu_chord.json.tpl | 2 +- lib/vault/vault_setup.sh | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 17eee94e8..1782a1ca4 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope minio drs-server htsget-server chord-metadata candig-server federation-service candig-data-portal #wes-server jupyter igv authentication authorization traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard +CANDIG_MODULES=weavescope minio drs-server htsget-server chord-metadata candig-server federation-service candig-data-portal #wes-server jupyter igv authentication authorization traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -238,7 +238,7 @@ KEYCLOAK_PUBLIC_URL_PROD=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN} KEYCLOAK_PRIVATE_URL=${KEYCLOAK_PRIVATE_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_CONTAINER_PORT} KEYCLOAK_REALM_URL=${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM} -KEYCLOAK_GENERATE_TEST_USER=0 +KEYCLOAK_GENERATE_TEST_USER=1 # tyk service TYK_VERSION=v3.2 @@ -310,7 +310,7 @@ OPA_VERSION=latest OPA_PORT=8181 OPA_LOG_LEVEL=debug #TODO: consolidate opa public and private domains -OPA_URL=http://opa:${OPA_PORT} +OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} CANDIG_OPA_SECRET=my-secret-root-token CANDIG_OPA_SECRET_SERVICE=my-secret-service-token diff --git a/lib/candig-server/docker-compose.yml b/lib/candig-server/docker-compose.yml index a282a0426..6e18feb70 100644 --- a/lib/candig-server/docker-compose.yml +++ b/lib/candig-server/docker-compose.yml @@ -2,12 +2,12 @@ version: '3.7' services: candig-server: - build: - context: $PWD/lib/candig-server - args: - venv_python: '3.6' - candig_version: ${CANDIG_SERVER_VERSION} - candig_ingest: ${CANDIG_INGEST_VERSION} + #build: + #context: $PWD/lib/candig-server + #args: + #venv_python: '3.6' + #candig_version: ${CANDIG_SERVER_VERSION} + #candig_ingest: ${CANDIG_INGEST_VERSION} image: ${DOCKER_REGISTRY}/candig-server:${CANDIG_SERVER_VERSION} networks: - ${DOCKER_NET} diff --git a/lib/keycloak/docker-compose.yml b/lib/keycloak/docker-compose.yml index ec22aa714..2ba6d83ea 100644 --- a/lib/keycloak/docker-compose.yml +++ b/lib/keycloak/docker-compose.yml @@ -10,7 +10,7 @@ services: #TODO: define image: tag command: [ "-b", "${KEYCLOAK_HOST}", "-Dkeycloak.migration.strategy=IGNORE_EXISTING" ] ports: - - "${KEYCLOAK_CONTAINER_PORT}:8080" + - "${KEYCLOAK_PORT}:${KEYCLOAK_CONTAINER_PORT}" networks: - ${DOCKER_NET} volumes: @@ -42,4 +42,4 @@ services: test: [ "CMD", "curl", "-f", "http://0.0.0.0:${KEYCLOAK_CONTAINER_PORT}" ] interval: 30s timeout: 20s - retries: 3 \ No newline at end of file + retries: 3 diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index 5320b0793..3009a3efd 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -7,7 +7,7 @@ services: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" katsu_url: "${CHORD_METADATA_PUBLIC_URL}" - idp: "${KEYCLOAK_REALM_URL}" + idp: "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" client_id: "${KEYCLOAK_CLIENT_ID}" networks: - ${DOCKER_NET} diff --git a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl index 79f7d581f..c37834aba 100644 --- a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl +++ b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl @@ -92,7 +92,7 @@ "segregate_by_client": false, "providers": [ { - "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" } diff --git a/lib/vault/vault_setup.sh b/lib/vault/vault_setup.sh index 984d461e8..e9ad92fd8 100755 --- a/lib/vault/vault_setup.sh +++ b/lib/vault/vault_setup.sh @@ -93,7 +93,7 @@ docker exec $vault sh -c "vault write auth/jwt/role/researcher user_claim=prefer # configure jwt echo echo ">> configuring jwt stuff" -docker exec $vault sh -c "vault write auth/jwt/config oidc_discovery_url=\"${KEYCLOAK_PUBLIC_URL}/auth/realms/candig\" bound_issuer=\"${KEYCLOAK_PUBLIC_URL}/auth/realms/candig\" default_role=\"researcher\"" +docker exec $vault sh -c "vault write auth/jwt/config oidc_discovery_url=\"${KEYCLOAK_PRIVATE_URL}/auth/realms/candig\" bound_issuer=\"${KEYCLOAK_PRIVATE_URL}/auth/realms/candig\" default_role=\"researcher\"" # create users echo From f7d1a378ae5e7b43d11c91c0a5f09510ac12db3b Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 5 Apr 2022 10:25:29 -0700 Subject: [PATCH 048/236] changes to match opa tweaks (#124) * corresponding move for https://github.com/CanDIG/candig-opa/pull/1701 * pass env vars in docker-compose * pass CANDIG_AUTHORIZATION in to Dockerfile * don't specify the server address * load paths.json * update submodule * fix opa_url * oops, didn't mean to comment this out * double-quotes causing a parsing error in tyk --- lib/candig-server/docker-compose.yml | 2 +- lib/opa/docker-compose.yml | 3 +-- lib/opa/opa | 2 +- .../api_mcode_candig_data_portal.json.tpl | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/candig-server/docker-compose.yml b/lib/candig-server/docker-compose.yml index 6e18feb70..3a64edeb6 100644 --- a/lib/candig-server/docker-compose.yml +++ b/lib/candig-server/docker-compose.yml @@ -29,7 +29,7 @@ services: - "traefik.http.services.candig-server.loadbalancer.server.port=${CANDIG_SERVER_PORT}" logging: *default-logging environment: - - OPA_SERVER=${OPA_URL} + - OPA_SERVER=http://opa:${OPA_PORT} - OPA_SERVER_TOKEN=${CANDIG_OPA_SECRET} - TYK_SERVER=${TYK_CANDIG_API_TARGET} - TYK_LISTEN_PATH=${TYK_CANDIG_API_LISTEN_PATH} diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index 3009a3efd..c04c1b9b5 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -44,8 +44,6 @@ services: command: - "run" - "--server" - - "--addr" - - "${OPA_URL}" - "--log-level=debug" - "--authentication=token" - "--authorization=basic" @@ -54,6 +52,7 @@ services: - "app/permissions_engine/idp.rego" - "app/data.json" - "app/permissions_engine/access.json" + - "app/permissions_engine/paths.json" networks: - bridge-net deploy: diff --git a/lib/opa/opa b/lib/opa/opa index fb216d237..5de578be9 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit fb216d2372d38c6b81bf096d8dbac2dd0ae5a83b +Subproject commit 5de578be93034670c57b4c69012fbb28c729d064 diff --git a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl index 0832901c4..2e10feec1 100644 --- a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl +++ b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", - "name": "${TYK_CANDIG_DATA_PORTAL_API_NAME}", + "name": ${TYK_CANDIG_DATA_PORTAL_API_NAME}, "use_openid": true, "active": true, "slug": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", From b172de59f560112f6c97d1acbd85e4d446e5df0d Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 5 Apr 2022 10:34:51 -0700 Subject: [PATCH 049/236] remove quotes from policies.json.tpl (#125) --- lib/tyk/configuration_templates/policies.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 5b23e3244..435cd602c 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -20,7 +20,7 @@ "${TYK_CANDIG_DATA_PORTAL_API_ID}": { "allowed_urls": [], "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", - "api_name": "${TYK_CANDIG_DATA_PORTAL_API_NAME}", + "api_name": ${TYK_CANDIG_DATA_PORTAL_API_NAME}, "versions": [ "Default" ] From c06b2543b13db56c25433164f234f6b2ebb515e6 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 5 Apr 2022 11:38:05 -0700 Subject: [PATCH 050/236] clean up the way we start opa --- lib/opa/docker-compose.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index c04c1b9b5..cc883a020 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -47,12 +47,8 @@ services: - "--log-level=debug" - "--authentication=token" - "--authorization=basic" - - "app/permissions_engine/authz.rego" - - "app/permissions_engine/permissions.rego" - - "app/permissions_engine/idp.rego" - "app/data.json" - - "app/permissions_engine/access.json" - - "app/permissions_engine/paths.json" + - "app/permissions_engine/" networks: - bridge-net deploy: From 5cab0c5d7e3319aef3d03a331e9c1bf5d416993b Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 5 Apr 2022 11:39:11 -0700 Subject: [PATCH 051/236] clean up opa startup call (#126) * corresponding move for https://github.com/CanDIG/candig-opa/pull/1701 * pass env vars in docker-compose * pass CANDIG_AUTHORIZATION in to Dockerfile * clean up the way we start opa --- lib/opa/docker-compose.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index c04c1b9b5..cc883a020 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -47,12 +47,8 @@ services: - "--log-level=debug" - "--authentication=token" - "--authorization=basic" - - "app/permissions_engine/authz.rego" - - "app/permissions_engine/permissions.rego" - - "app/permissions_engine/idp.rego" - "app/data.json" - - "app/permissions_engine/access.json" - - "app/permissions_engine/paths.json" + - "app/permissions_engine/" networks: - bridge-net deploy: From ca0915a16ca00c539d23a8a063213d141ab0971a Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 7 Apr 2022 11:39:31 -0700 Subject: [PATCH 052/236] template fixes --- lib/tyk/configuration_templates/api_candig.json.tpl | 2 +- lib/tyk/configuration_templates/api_graphql.json.tpl | 2 +- .../api_mcode_candig_data_portal.json.tpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/tyk/configuration_templates/api_candig.json.tpl b/lib/tyk/configuration_templates/api_candig.json.tpl index 40c73e4f7..19abb7ef9 100644 --- a/lib/tyk/configuration_templates/api_candig.json.tpl +++ b/lib/tyk/configuration_templates/api_candig.json.tpl @@ -92,7 +92,7 @@ "segregate_by_client": false, "providers": [ { - "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" } diff --git a/lib/tyk/configuration_templates/api_graphql.json.tpl b/lib/tyk/configuration_templates/api_graphql.json.tpl index 631292bc0..68b8ba06d 100644 --- a/lib/tyk/configuration_templates/api_graphql.json.tpl +++ b/lib/tyk/configuration_templates/api_graphql.json.tpl @@ -92,7 +92,7 @@ "segregate_by_client": false, "providers": [ { - "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" } diff --git a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl index 2e10feec1..a6611744c 100644 --- a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl +++ b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl @@ -95,7 +95,7 @@ "segregate_by_client": false, "providers": [ { - "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" } From a980ec0a9e7731d16eb623b269564dc4def05f4b Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 8 Apr 2022 18:02:57 +0000 Subject: [PATCH 053/236] candig-data-portal docker fixes --- lib/candig-data-portal/Dockerfile | 2 -- lib/candig-data-portal/docker-compose.yml | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/candig-data-portal/Dockerfile b/lib/candig-data-portal/Dockerfile index d95270774..4ea0ec918 100644 --- a/lib/candig-data-portal/Dockerfile +++ b/lib/candig-data-portal/Dockerfile @@ -1,4 +1,3 @@ -ARG candig_data_portal_url ARG katsu_api_target_url FROM node:14.16.0-alpine as build @@ -6,7 +5,6 @@ FROM node:14.16.0-alpine as build LABEL Maintainer="CanDIG Project" -ENV CANDIG_DATA_PORTAL_URL=$candig_data_portal_url ENV TYK_KATSU_API_TARGET=$katsu_api_target_url diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index eb3b10fe2..bf97ab575 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -27,6 +27,12 @@ services: - "traefik.http.routers.candig-data-portal.rule=Host(`candig-data-portal.${CANDIG_DOMAIN}`)" - "traefik.http.services.candig-data-portal.loadbalancer.server.port=${CANDIG_DATA_PORTAL_PORT}" logging: *default-logging + environment: + - REACT_APP_KATSU_API_SERVER=${TYK_KATSU_API_TARGET} + - REACT_APP_CANDIG_SERVER=${TYK_CANDIG_API_TARGET} + #- REACT_APP_FEDERATION_API_SERVER=http://${CANDIG_DOMAIN}:${FEDERATION_PORT} + #- REACT_APP_BASE_NAME='/' + - REACT_APP_SITE_LOCATION=UHN healthcheck: test: [ "CMD", "curl", "http://localhost:3000" ] interval: 30s From 71247a5c03dc985e86c955be3b27fb003e7fea5d Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 8 Apr 2022 13:01:46 -0700 Subject: [PATCH 054/236] another docker fix --- lib/candig-data-portal/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index bf97ab575..1cff44f6f 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -5,7 +5,6 @@ services: build: context: $PWD/lib/candig-data-portal args: - candig_data_portal_url: "${CANDIG_DATA_PORTAL_URL}" katsu_api_target_url: "${TYK_KATSU_API_TARGET}" # image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} networks: From 7f4b5d7ae991d10905ad8a9a103417dad75f147c Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 8 Apr 2022 13:14:43 -0700 Subject: [PATCH 055/236] opa_runner uses internal ip addresses --- lib/opa/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index cc883a020..988dd4d6b 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -7,7 +7,7 @@ services: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" katsu_url: "${CHORD_METADATA_PUBLIC_URL}" - idp: "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" + idp: "${KEYCLOAK_PRIVATE_URL}/auth/realms/${KEYCLOAK_REALM}" client_id: "${KEYCLOAK_CLIENT_ID}" networks: - ${DOCKER_NET} From 761e44ee0d4faac67164838a05c94be2b8ce4202 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 8 Apr 2022 13:15:08 -0700 Subject: [PATCH 056/236] straighten out uses of internal and external urls --- etc/env/example.env | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 1782a1ca4..f8f7b3aaf 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -189,6 +189,7 @@ CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false CHORD_METADATA_DEBUG=false CHORD_METADATA_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} +CHORD_METADATA_PRIVATE_URL=http://chord-metadata:8000 # candig-specific katsu INSIDE_CANDIG=true @@ -207,6 +208,7 @@ CANDIG_SERVER_HOST=0.0.0.0 CANDIG_SERVER_PORT=3001 CANDIG_INGEST_VERSION=1.5.0 CANDIG_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} +CANDIG_PRIVATE_URL=http://candig-server:3000 # rnaget service RNAGET_VERSION=v0.9.5 @@ -261,28 +263,28 @@ TYK_AUTH_API_SLUG=authentication TYK_CANDIG_API_ID=21 TYK_CANDIG_API_NAME=CanDIG TYK_CANDIG_API_SLUG=candig -TYK_CANDIG_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} +TYK_CANDIG_API_TARGET=${CANDIG_PRIVATE_URL} TYK_CANDIG_API_LISTEN_PATH=candig ## api - katsu - chord-metadata TYK_KATSU_API_ID=31 TYK_KATSU_API_NAME=Katsu TYK_KATSU_API_SLUG=katsu -TYK_KATSU_API_TARGET=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} +TYK_KATSU_API_TARGET=${CHORD_METADATA_PRIVATE_URL} TYK_KATSU_API_LISTEN_PATH=katsu ## api - mcode - candig-data-portal TYK_CANDIG_DATA_PORTAL_API_ID=41 TYK_CANDIG_DATA_PORTAL_API_NAME="CanDIG Data Portal" TYK_CANDIG_DATA_PORTAL_API_SLUG=candig-data-portal -TYK_CANDIG_DATA_PORTAL_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT} +TYK_CANDIG_DATA_PORTAL_API_TARGET=${CANDIG_DATA_PORTAL_PRIVATE_URL} TYK_CANDIG_DATA_PORTAL_API_LISTEN_PATH= ## api - graphql TYK_GRAPHQL_API_ID=51 TYK_GRAPHQL_API_NAME=GraphQL-Interface TYK_GRAPHQL_API_SLUG=graphql-interface -TYK_GRAPHQL_API_TARGET=http://${CANDIG_DOMAIN}:${GRAPHQL_INTERFACE_PORT} +TYK_GRAPHQL_API_TARGET=${GRAPHQL_PRIVATE_URL} TYK_GRAPHQL_API_LISTEN_PATH=graphql ## Extra APIs can be added here @@ -301,7 +303,7 @@ VAULT_SERVICE_PORT=8200 VAULT_SERVICE_HOST=0.0.0.0 #TODO: consolidate vault public and private domains VAULT_SERVICE_PUBLIC_URL=http://${VAULT_SERVICE_HOST}:${VAULT_SERVICE_PORT} -VAULT_SERVICE_URL=http://candigv2_vault_1:${VAULT_SERVICE_PORT} +VAULT_SERVICE_URL=http://candigv2_vault_1:8200 ## VAULT_JWKS= ###(generated in setup.sh) @@ -309,10 +311,10 @@ VAULT_SERVICE_URL=http://candigv2_vault_1:${VAULT_SERVICE_PORT} OPA_VERSION=latest OPA_PORT=8181 OPA_LOG_LEVEL=debug -#TODO: consolidate opa public and private domains -OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} CANDIG_OPA_SECRET=my-secret-root-token CANDIG_OPA_SECRET_SERVICE=my-secret-service-token +OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} +OPA_PRIVATE_URL=http://opa:8181 # cancogen_dashboard @@ -330,11 +332,13 @@ CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search CANDIG_DATA_PORTAL_VERSION=v0.1.0 CANDIG_DATA_PORTAL_PORT=2543 CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORT}/data-portal +CANDIG_DATA_PORTAL_PRIVATE_URL=http://candig-data-portal:3000 # graphql-interface GRAPHQL_PYTHON_VERSION=3.8 GRAPHQL_INTERFACE_VERSION=v1.0.0 GRAPHQL_INTERFACE_PORT=7999 +GRAPHQL_PRIVATE_URL=http://graphql:7999 GRAPHQL_KATSU_API=http://${TYK_LOGIN_TARGET_URL}/${TYK_KATSU_API_LISTEN_PATH}/api GRAPHQL_CANDIG_SERVER=http://${TYK_LOGIN_TARGET_URL}/${TYK_CANDIG_API_LISTEN_PATH} From b624eccafd6088da3185c4eeeb9952628ad80991 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 8 Apr 2022 13:18:27 -0700 Subject: [PATCH 057/236] portal port --- etc/env/example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/env/example.env b/etc/env/example.env index f8f7b3aaf..f3ffbe169 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -331,7 +331,7 @@ CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search # candig-data-server (previously mcode) CANDIG_DATA_PORTAL_VERSION=v0.1.0 CANDIG_DATA_PORTAL_PORT=2543 -CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORT}/data-portal +CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT}/data-portal CANDIG_DATA_PORTAL_PRIVATE_URL=http://candig-data-portal:3000 # graphql-interface From 0e4a8395aae9f2b725eb1d1a08e6e4f7ad440c1f Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 8 Apr 2022 13:33:35 -0700 Subject: [PATCH 058/236] Update opa --- lib/opa/opa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/opa/opa b/lib/opa/opa index 5de578be9..03e786c37 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 5de578be93034670c57b4c69012fbb28c729d064 +Subproject commit 03e786c377b60ffec268c93e26ffec25525e45f2 From 8d1835ed5b55233c7d8a826747f2318fe20ea9aa Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Sat, 9 Apr 2022 01:39:40 +0000 Subject: [PATCH 059/236] pass in IDP to env in opa-runner --- lib/opa/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index 988dd4d6b..6a72216b5 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -31,6 +31,7 @@ services: CLIENT_SECRET_ROOT: ${CANDIG_OPA_SECRET} CLIENT_SECRET_SERVICE: ${CANDIG_OPA_SECRET_SERVICE} OPA_URL: ${OPA_URL} + IDP: "${KEYCLOAK_PRIVATE_URL}/auth/realms/${KEYCLOAK_REALM}" opa: image: openpolicyagent/opa:latest ports: From 2939716fd235115b739050066e6a121fcd162dd4 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Sat, 9 Apr 2022 01:49:40 +0000 Subject: [PATCH 060/236] move script exec --- lib/opa/docker-compose.yml | 2 ++ lib/opa/opa_setup.sh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index 6a72216b5..ca7471cc7 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -32,6 +32,8 @@ services: CLIENT_SECRET_SERVICE: ${CANDIG_OPA_SECRET_SERVICE} OPA_URL: ${OPA_URL} IDP: "${KEYCLOAK_PRIVATE_URL}/auth/realms/${KEYCLOAK_REALM}" + KATSU_URL: "${CHORD_METADATA_PUBLIC_URL}" + opa: image: openpolicyagent/opa:latest ports: diff --git a/lib/opa/opa_setup.sh b/lib/opa/opa_setup.sh index 6abbdb308..8fde8bc20 100644 --- a/lib/opa/opa_setup.sh +++ b/lib/opa/opa_setup.sh @@ -9,6 +9,8 @@ opa_runner=$(docker ps | grep "opa-runner" | awk '{print $1}') docker exec $opa_runner python3 app/permissions_engine/fetch_keys.py +docker exec $opa_runner python3 app/tests/create_katsu_test_datasets.py + opa=$(docker ps | grep "candigv2_opa_1" | awk '{print $1}') docker restart $opa From 6b5503693c022c1b6d0036f3e815e47dc904eb14 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 10:26:50 -0700 Subject: [PATCH 061/236] short internal container name for vault --- etc/env/example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/env/example.env b/etc/env/example.env index f3ffbe169..cb1b87eb5 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -303,7 +303,7 @@ VAULT_SERVICE_PORT=8200 VAULT_SERVICE_HOST=0.0.0.0 #TODO: consolidate vault public and private domains VAULT_SERVICE_PUBLIC_URL=http://${VAULT_SERVICE_HOST}:${VAULT_SERVICE_PORT} -VAULT_SERVICE_URL=http://candigv2_vault_1:8200 +VAULT_SERVICE_URL=http://vault:8200 ## VAULT_JWKS= ###(generated in setup.sh) From 881d5a42b338883ab310e797eede31dc59ff05da Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 10:27:26 -0700 Subject: [PATCH 062/236] Consolidate to just API_SLUG instead of separate API_NAME --- etc/env/example.env | 6 ------ lib/tyk/configuration_templates/api_auth.json.tpl | 2 +- lib/tyk/configuration_templates/api_candig.json.tpl | 2 +- lib/tyk/configuration_templates/api_graphql.json.tpl | 2 +- lib/tyk/configuration_templates/api_katsu_chord.json.tpl | 2 +- .../api_mcode_candig_data_portal.json.tpl | 2 +- lib/tyk/configuration_templates/key_request.json.tpl | 2 +- lib/tyk/configuration_templates/policies.json.tpl | 8 ++++---- lib/tyk/tyk_key_generation.sh | 8 ++++---- 9 files changed, 14 insertions(+), 20 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index cb1b87eb5..3e705453d 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -256,33 +256,28 @@ TYK_POLICY_ID=candig_policy ## api - authentication TYK_AUTH_API_ID=11 -TYK_AUTH_API_NAME=Authentication TYK_AUTH_API_SLUG=authentication ## api - candig-server (v1) TYK_CANDIG_API_ID=21 -TYK_CANDIG_API_NAME=CanDIG TYK_CANDIG_API_SLUG=candig TYK_CANDIG_API_TARGET=${CANDIG_PRIVATE_URL} TYK_CANDIG_API_LISTEN_PATH=candig ## api - katsu - chord-metadata TYK_KATSU_API_ID=31 -TYK_KATSU_API_NAME=Katsu TYK_KATSU_API_SLUG=katsu TYK_KATSU_API_TARGET=${CHORD_METADATA_PRIVATE_URL} TYK_KATSU_API_LISTEN_PATH=katsu ## api - mcode - candig-data-portal TYK_CANDIG_DATA_PORTAL_API_ID=41 -TYK_CANDIG_DATA_PORTAL_API_NAME="CanDIG Data Portal" TYK_CANDIG_DATA_PORTAL_API_SLUG=candig-data-portal TYK_CANDIG_DATA_PORTAL_API_TARGET=${CANDIG_DATA_PORTAL_PRIVATE_URL} TYK_CANDIG_DATA_PORTAL_API_LISTEN_PATH= ## api - graphql TYK_GRAPHQL_API_ID=51 -TYK_GRAPHQL_API_NAME=GraphQL-Interface TYK_GRAPHQL_API_SLUG=graphql-interface TYK_GRAPHQL_API_TARGET=${GRAPHQL_PRIVATE_URL} TYK_GRAPHQL_API_LISTEN_PATH=graphql @@ -290,7 +285,6 @@ TYK_GRAPHQL_API_LISTEN_PATH=graphql ## Extra APIs can be added here ## api - example #TYK_EXAMPLE_API_ID=666 -#TYK_EXAMPLE_API_NAME=Example #TYK_EXAMPLE_API_SLUG=example #TYK_EXAMPLE_API_TARGET=http://example.org #TYK_EXAMPLE_API_LISTEN_PATH=example diff --git a/lib/tyk/configuration_templates/api_auth.json.tpl b/lib/tyk/configuration_templates/api_auth.json.tpl index eb5e2be1e..f512f4941 100644 --- a/lib/tyk/configuration_templates/api_auth.json.tpl +++ b/lib/tyk/configuration_templates/api_auth.json.tpl @@ -21,7 +21,7 @@ "auth_header_name": "" }, - "name": "${TYK_AUTH_API_NAME}", + "name": "${TYK_AUTH_API_SLUG}", "slug": "${TYK_AUTH_API_SLUG}", "proxy": { diff --git a/lib/tyk/configuration_templates/api_candig.json.tpl b/lib/tyk/configuration_templates/api_candig.json.tpl index 19abb7ef9..370ebf70a 100644 --- a/lib/tyk/configuration_templates/api_candig.json.tpl +++ b/lib/tyk/configuration_templates/api_candig.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_CANDIG_API_ID}", - "name": "${TYK_CANDIG_API_NAME}", + "name": "${TYK_CANDIG_API_SLUG}", "use_openid": true, "active": true, "slug": "${TYK_CANDIG_API_SLUG}", diff --git a/lib/tyk/configuration_templates/api_graphql.json.tpl b/lib/tyk/configuration_templates/api_graphql.json.tpl index 68b8ba06d..8bfdad2a7 100644 --- a/lib/tyk/configuration_templates/api_graphql.json.tpl +++ b/lib/tyk/configuration_templates/api_graphql.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_GRAPHQL_API_ID}", - "name": "${TYK_GRAPHQL_API_NAME}", + "name": "${TYK_GRAPHQL_API_SLUG}", "use_openid": true, "active": true, "slug": "${TYK_GRAPHQL_API_SLUG}", diff --git a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl index c37834aba..8d49e8aee 100644 --- a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl +++ b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_KATSU_API_ID}", - "name": "${TYK_KATSU_API_NAME}", + "name": "${TYK_KATSU_API_SLUG}", "use_openid": true, "active": true, "slug": "${TYK_KATSU_API_SLUG}", diff --git a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl index a6611744c..7fbea7a09 100644 --- a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl +++ b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", - "name": ${TYK_CANDIG_DATA_PORTAL_API_NAME}, + "name": ${TYK_CANDIG_DATA_PORTAL_API_SLUG}, "use_openid": true, "active": true, "slug": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", diff --git a/lib/tyk/configuration_templates/key_request.json.tpl b/lib/tyk/configuration_templates/key_request.json.tpl index d83a8d943..74a1f3841 100644 --- a/lib/tyk/configuration_templates/key_request.json.tpl +++ b/lib/tyk/configuration_templates/key_request.json.tpl @@ -11,7 +11,7 @@ "access_rights": { "${TYK_CANDIG_API_ID}": { "api_id": "${TYK_CANDIG_API_ID}", - "api_name": "${TYK_CANDIG_API_NAME}", + "api_name": "${TYK_CANDIG_API_SLUG}", "Versions": ["Default"] } }, diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 435cd602c..70f33d1da 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -4,7 +4,7 @@ "${TYK_CANDIG_API_ID}": { "allowed_urls": [], "api_id": "${TYK_CANDIG_API_ID}", - "api_name": "${TYK_CANDIG_API_NAME}", + "api_name": "${TYK_CANDIG_API_SLUG}", "versions": [ "Default" ] @@ -12,7 +12,7 @@ "${TYK_KATSU_API_ID}": { "allowed_urls": [], "api_id": "${TYK_KATSU_API_ID}", - "api_name": "${TYK_KATSU_API_NAME}", + "api_name": "${TYK_KATSU_API_SLUG}", "versions": [ "Default" ] @@ -20,7 +20,7 @@ "${TYK_CANDIG_DATA_PORTAL_API_ID}": { "allowed_urls": [], "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", - "api_name": ${TYK_CANDIG_DATA_PORTAL_API_NAME}, + "api_name": ${TYK_CANDIG_DATA_PORTAL_API_SLUG}, "versions": [ "Default" ] @@ -28,7 +28,7 @@ "${TYK_GRAPHQL_API_ID}": { "allowed_urls": [], "api_id": "${TYK_GRAPHQL_API_ID}", - "api_name": "${TYK_GRAPHQL_API_NAME}", + "api_name": "${TYK_GRAPHQL_API_SLUG}", "versions": [ "Default" ] diff --git a/lib/tyk/tyk_key_generation.sh b/lib/tyk/tyk_key_generation.sh index dbd607080..2c8dc711a 100644 --- a/lib/tyk/tyk_key_generation.sh +++ b/lib/tyk/tyk_key_generation.sh @@ -31,22 +31,22 @@ generate_key() { "access_rights": { "'"${TYK_CANDIG_API_ID}"'": { "api_id": "'"${TYK_CANDIG_API_ID}"'", - "api_name": "'"${TYK_CANDIG_API_NAME}"'", + "api_name": "'"${TYK_CANDIG_API_SLUG}"'", "Versions": ["Default"] }, "'"${TYK_KATSU_API_ID}"'": { "api_id": "'"${TYK_KATSU_API_ID}"'", - "api_name": "'"${TYK_KATSU_API_NAME}"'", + "api_name": "'"${TYK_KATSU_API_SLUG}"'", "Versions": ["Default"] }, "'"${TYK_CANDIG_DATA_PORTAL_API_ID}"'": { "api_id": "'"${TYK_CANDIG_DATA_PORTAL_API_ID}"'", - "api_name": "'"${TYK_CANDIG_DATA_PORTAL_API_NAME}"'", + "api_name": "'"${TYK_CANDIG_DATA_PORTAL_API_SLUG}"'", "Versions": ["Default"] }, "'"${TYK_GRAPHQL_API_ID}"'": { "api_id": "'"${TYK_GRAPHQL_API_ID}"'", - "api_name": "'"${TYK_GRAPHQL_API_NAME}"'", + "api_name": "'"${TYK_GRAPHQL_API_SLUG}"'", "Versions": ["Default"] } } From f10a3c2b5af5caf7fadfe2331686cbc28c66415a Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 10:36:23 -0700 Subject: [PATCH 063/236] move to a variable set in .env --- etc/env/example.env | 1 + lib/candig-data-portal/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/etc/env/example.env b/etc/env/example.env index 3e705453d..b760bb1b5 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -8,6 +8,7 @@ CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] CANDIG_DOMAIN=docker.localhost CANDIG_AUTH_DOMAIN=auth.docker.localhost +CANDIG_SITE_LOCATION=UHN # miniconda venv # options are [linux, darwin] diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index 1cff44f6f..52e96513d 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -31,7 +31,7 @@ services: - REACT_APP_CANDIG_SERVER=${TYK_CANDIG_API_TARGET} #- REACT_APP_FEDERATION_API_SERVER=http://${CANDIG_DOMAIN}:${FEDERATION_PORT} #- REACT_APP_BASE_NAME='/' - - REACT_APP_SITE_LOCATION=UHN + - REACT_APP_SITE_LOCATION=${CANDIG_SITE_LOCATION} healthcheck: test: [ "CMD", "curl", "http://localhost:3000" ] interval: 30s From 591c1e93f597a4af9b3154a8e93849669fde2592 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 10:47:16 -0700 Subject: [PATCH 064/236] Update htsget_app --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index ce6586e43..f980e31c8 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit ce6586e438fa668a0fcb26c7fbadd5f74ec1a0d5 +Subproject commit f980e31c8a7c79a26af3a38c815ff40e34b2967b From 142c5d73d3adda6dfb52d856b08ad44edc06fdd7 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 10:49:01 -0700 Subject: [PATCH 065/236] Update opa --- lib/opa/opa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/opa/opa b/lib/opa/opa index 03e786c37..6b2cf3046 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 03e786c377b60ffec268c93e26ffec25525e45f2 +Subproject commit 6b2cf3046b99e30cfde68deec46eaab4678775f0 From 26127c8344b9e1a13e38eb1d24723d21212e6468 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 10:49:29 -0700 Subject: [PATCH 066/236] Update candig-data-portal --- lib/candig-data-portal/candig-data-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal index 54feabbbc..d284d86a3 160000 --- a/lib/candig-data-portal/candig-data-portal +++ b/lib/candig-data-portal/candig-data-portal @@ -1 +1 @@ -Subproject commit 54feabbbc87ae2f6f702183de731e405ca7dadfd +Subproject commit d284d86a33daa37273d1c90daee29c458bfc991e From 1d195d1a269435d6210158456bbabefc407bbb39 Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu Date: Mon, 11 Apr 2022 14:22:16 -0400 Subject: [PATCH 067/236] Building with Vagrant on VirtualBox no longer works (#127) The base image in use was too old (debian buster), and docker-compose was not installed --- Vagrantfile | 4 ++-- provision.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index bfb25b094..668382157 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -18,7 +18,7 @@ Vagrant.configure('2') do |config| config.vm.provider 'virtualbox' do |vb, override| override.vm.synced_folder '.', '/home/vagrant/candig', type: 'virtualbox' - override.vm.box = 'debian/contrib-buster64' + override.vm.box = 'generic/ubuntu2010' override.vm.hostname = 'candig.local' override.disksize.size = '50GB' # override.vm.network "forwarded_port", guest: 80, host: 80 @@ -35,7 +35,7 @@ Vagrant.configure('2') do |config| config.vm.provider :libvirt do |libvirt, override| override.vm.synced_folder '.', '/home/vagrant/candig', type: 'nfs' - override.vm.box = 'debian-sandbox/contrib-buster64' + override.vm.box = 'generic/ubuntu2010' override.vm.hostname = 'candig.local' override.disksize.size = '50GB' libvirt.memory = 4096 diff --git a/provision.sh b/provision.sh index 11a1da3f6..8819f7b26 100644 --- a/provision.sh +++ b/provision.sh @@ -62,10 +62,10 @@ dist=$(lsb_release -is) codename=$(lsb_release -cs) curl -fsSL https://download.docker.com/linux/${dist,,}/gpg | sudo apt-key add - -sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/${dist,,} $(lsb_release -cs) stable" +sudo add-apt-repository -y "deb [arch=amd64] https://download.docker.com/linux/${dist,,} $(lsb_release -cs) stable" sudo apt-get update -sudo apt-get install -y docker-ce docker-ce-cli containerd.io +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose echo " finished installing docker components" | tee -a $LOGFILE sudo apt-get autoclean From 89ab09e02f5b522b424e131aa3e487d533a49985 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 11:48:07 -0700 Subject: [PATCH 068/236] update opa --- lib/opa/opa | 2 +- lib/opa/opa_setup.sh | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/opa/opa b/lib/opa/opa index 6b2cf3046..aafe9cd2d 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 6b2cf3046b99e30cfde68deec46eaab4678775f0 +Subproject commit aafe9cd2dae4f48010ee24367fec6827244e5698 diff --git a/lib/opa/opa_setup.sh b/lib/opa/opa_setup.sh index 8fde8bc20..a91bbd768 100644 --- a/lib/opa/opa_setup.sh +++ b/lib/opa/opa_setup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -Eeuo pipefail +set -Euo pipefail LOGFILE=$PWD/tmp/progress.txt @@ -8,9 +8,6 @@ LOGFILE=$PWD/tmp/progress.txt opa_runner=$(docker ps | grep "opa-runner" | awk '{print $1}') docker exec $opa_runner python3 app/permissions_engine/fetch_keys.py +docker restart candigv2_opa_1 docker exec $opa_runner python3 app/tests/create_katsu_test_datasets.py - -opa=$(docker ps | grep "candigv2_opa_1" | awk '{print $1}') - -docker restart $opa From ab148d4b7b7ead2a05394142e8d6a6c0dd234f8b Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 11:48:15 -0700 Subject: [PATCH 069/236] add quotes back --- .../api_mcode_candig_data_portal.json.tpl | 2 +- lib/tyk/configuration_templates/policies.json.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl index 7fbea7a09..c2ab9d2b0 100644 --- a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl +++ b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", - "name": ${TYK_CANDIG_DATA_PORTAL_API_SLUG}, + "name": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", "use_openid": true, "active": true, "slug": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 70f33d1da..185969197 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -20,7 +20,7 @@ "${TYK_CANDIG_DATA_PORTAL_API_ID}": { "allowed_urls": [], "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", - "api_name": ${TYK_CANDIG_DATA_PORTAL_API_SLUG}, + "api_name": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", "versions": [ "Default" ] From a48a020579d331d33380b776e879877b877a2d0e Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 11:50:03 -0700 Subject: [PATCH 070/236] Miscellaneous changes to server deployment settings (#128) * corresponding move for https://github.com/CanDIG/candig-opa/pull/1701 * pass env vars in docker-compose * pass CANDIG_AUTHORIZATION in to Dockerfile * clean up the way we start opa * template fixes * candig-data-portal docker fixes * another docker fix * opa_runner uses internal ip addresses * straighten out uses of internal and external urls * portal port * Update opa * pass in IDP to env in opa-runner * move script exec * short internal container name for vault * Consolidate to just API_SLUG instead of separate API_NAME * move to a variable set in .env * Update htsget_app * Update opa * Update candig-data-portal * update opa * add quotes back --- etc/env/example.env | 27 +++++++++---------- lib/candig-data-portal/Dockerfile | 2 -- lib/candig-data-portal/candig-data-portal | 2 +- lib/candig-data-portal/docker-compose.yml | 7 ++++- lib/htsget-server/htsget_app | 2 +- lib/opa/docker-compose.yml | 5 +++- lib/opa/opa | 2 +- lib/opa/opa_setup.sh | 7 +++-- .../configuration_templates/api_auth.json.tpl | 2 +- .../api_candig.json.tpl | 4 +-- .../api_graphql.json.tpl | 4 +-- .../api_katsu_chord.json.tpl | 2 +- .../api_mcode_candig_data_portal.json.tpl | 4 +-- .../key_request.json.tpl | 2 +- .../configuration_templates/policies.json.tpl | 8 +++--- lib/tyk/tyk_key_generation.sh | 8 +++--- 16 files changed, 46 insertions(+), 42 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 1782a1ca4..b760bb1b5 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -8,6 +8,7 @@ CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] CANDIG_DOMAIN=docker.localhost CANDIG_AUTH_DOMAIN=auth.docker.localhost +CANDIG_SITE_LOCATION=UHN # miniconda venv # options are [linux, darwin] @@ -189,6 +190,7 @@ CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false CHORD_METADATA_DEBUG=false CHORD_METADATA_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} +CHORD_METADATA_PRIVATE_URL=http://chord-metadata:8000 # candig-specific katsu INSIDE_CANDIG=true @@ -207,6 +209,7 @@ CANDIG_SERVER_HOST=0.0.0.0 CANDIG_SERVER_PORT=3001 CANDIG_INGEST_VERSION=1.5.0 CANDIG_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} +CANDIG_PRIVATE_URL=http://candig-server:3000 # rnaget service RNAGET_VERSION=v0.9.5 @@ -254,41 +257,35 @@ TYK_POLICY_ID=candig_policy ## api - authentication TYK_AUTH_API_ID=11 -TYK_AUTH_API_NAME=Authentication TYK_AUTH_API_SLUG=authentication ## api - candig-server (v1) TYK_CANDIG_API_ID=21 -TYK_CANDIG_API_NAME=CanDIG TYK_CANDIG_API_SLUG=candig -TYK_CANDIG_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} +TYK_CANDIG_API_TARGET=${CANDIG_PRIVATE_URL} TYK_CANDIG_API_LISTEN_PATH=candig ## api - katsu - chord-metadata TYK_KATSU_API_ID=31 -TYK_KATSU_API_NAME=Katsu TYK_KATSU_API_SLUG=katsu -TYK_KATSU_API_TARGET=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} +TYK_KATSU_API_TARGET=${CHORD_METADATA_PRIVATE_URL} TYK_KATSU_API_LISTEN_PATH=katsu ## api - mcode - candig-data-portal TYK_CANDIG_DATA_PORTAL_API_ID=41 -TYK_CANDIG_DATA_PORTAL_API_NAME="CanDIG Data Portal" TYK_CANDIG_DATA_PORTAL_API_SLUG=candig-data-portal -TYK_CANDIG_DATA_PORTAL_API_TARGET=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT} +TYK_CANDIG_DATA_PORTAL_API_TARGET=${CANDIG_DATA_PORTAL_PRIVATE_URL} TYK_CANDIG_DATA_PORTAL_API_LISTEN_PATH= ## api - graphql TYK_GRAPHQL_API_ID=51 -TYK_GRAPHQL_API_NAME=GraphQL-Interface TYK_GRAPHQL_API_SLUG=graphql-interface -TYK_GRAPHQL_API_TARGET=http://${CANDIG_DOMAIN}:${GRAPHQL_INTERFACE_PORT} +TYK_GRAPHQL_API_TARGET=${GRAPHQL_PRIVATE_URL} TYK_GRAPHQL_API_LISTEN_PATH=graphql ## Extra APIs can be added here ## api - example #TYK_EXAMPLE_API_ID=666 -#TYK_EXAMPLE_API_NAME=Example #TYK_EXAMPLE_API_SLUG=example #TYK_EXAMPLE_API_TARGET=http://example.org #TYK_EXAMPLE_API_LISTEN_PATH=example @@ -301,7 +298,7 @@ VAULT_SERVICE_PORT=8200 VAULT_SERVICE_HOST=0.0.0.0 #TODO: consolidate vault public and private domains VAULT_SERVICE_PUBLIC_URL=http://${VAULT_SERVICE_HOST}:${VAULT_SERVICE_PORT} -VAULT_SERVICE_URL=http://candigv2_vault_1:${VAULT_SERVICE_PORT} +VAULT_SERVICE_URL=http://vault:8200 ## VAULT_JWKS= ###(generated in setup.sh) @@ -309,10 +306,10 @@ VAULT_SERVICE_URL=http://candigv2_vault_1:${VAULT_SERVICE_PORT} OPA_VERSION=latest OPA_PORT=8181 OPA_LOG_LEVEL=debug -#TODO: consolidate opa public and private domains -OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} CANDIG_OPA_SECRET=my-secret-root-token CANDIG_OPA_SECRET_SERVICE=my-secret-service-token +OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} +OPA_PRIVATE_URL=http://opa:8181 # cancogen_dashboard @@ -329,12 +326,14 @@ CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search # candig-data-server (previously mcode) CANDIG_DATA_PORTAL_VERSION=v0.1.0 CANDIG_DATA_PORTAL_PORT=2543 -CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORT}/data-portal +CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT}/data-portal +CANDIG_DATA_PORTAL_PRIVATE_URL=http://candig-data-portal:3000 # graphql-interface GRAPHQL_PYTHON_VERSION=3.8 GRAPHQL_INTERFACE_VERSION=v1.0.0 GRAPHQL_INTERFACE_PORT=7999 +GRAPHQL_PRIVATE_URL=http://graphql:7999 GRAPHQL_KATSU_API=http://${TYK_LOGIN_TARGET_URL}/${TYK_KATSU_API_LISTEN_PATH}/api GRAPHQL_CANDIG_SERVER=http://${TYK_LOGIN_TARGET_URL}/${TYK_CANDIG_API_LISTEN_PATH} diff --git a/lib/candig-data-portal/Dockerfile b/lib/candig-data-portal/Dockerfile index d95270774..4ea0ec918 100644 --- a/lib/candig-data-portal/Dockerfile +++ b/lib/candig-data-portal/Dockerfile @@ -1,4 +1,3 @@ -ARG candig_data_portal_url ARG katsu_api_target_url FROM node:14.16.0-alpine as build @@ -6,7 +5,6 @@ FROM node:14.16.0-alpine as build LABEL Maintainer="CanDIG Project" -ENV CANDIG_DATA_PORTAL_URL=$candig_data_portal_url ENV TYK_KATSU_API_TARGET=$katsu_api_target_url diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal index 54feabbbc..d284d86a3 160000 --- a/lib/candig-data-portal/candig-data-portal +++ b/lib/candig-data-portal/candig-data-portal @@ -1 +1 @@ -Subproject commit 54feabbbc87ae2f6f702183de731e405ca7dadfd +Subproject commit d284d86a33daa37273d1c90daee29c458bfc991e diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index eb3b10fe2..52e96513d 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -5,7 +5,6 @@ services: build: context: $PWD/lib/candig-data-portal args: - candig_data_portal_url: "${CANDIG_DATA_PORTAL_URL}" katsu_api_target_url: "${TYK_KATSU_API_TARGET}" # image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} networks: @@ -27,6 +26,12 @@ services: - "traefik.http.routers.candig-data-portal.rule=Host(`candig-data-portal.${CANDIG_DOMAIN}`)" - "traefik.http.services.candig-data-portal.loadbalancer.server.port=${CANDIG_DATA_PORTAL_PORT}" logging: *default-logging + environment: + - REACT_APP_KATSU_API_SERVER=${TYK_KATSU_API_TARGET} + - REACT_APP_CANDIG_SERVER=${TYK_CANDIG_API_TARGET} + #- REACT_APP_FEDERATION_API_SERVER=http://${CANDIG_DOMAIN}:${FEDERATION_PORT} + #- REACT_APP_BASE_NAME='/' + - REACT_APP_SITE_LOCATION=${CANDIG_SITE_LOCATION} healthcheck: test: [ "CMD", "curl", "http://localhost:3000" ] interval: 30s diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index ce6586e43..f980e31c8 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit ce6586e438fa668a0fcb26c7fbadd5f74ec1a0d5 +Subproject commit f980e31c8a7c79a26af3a38c815ff40e34b2967b diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index cc883a020..ca7471cc7 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -7,7 +7,7 @@ services: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" katsu_url: "${CHORD_METADATA_PUBLIC_URL}" - idp: "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" + idp: "${KEYCLOAK_PRIVATE_URL}/auth/realms/${KEYCLOAK_REALM}" client_id: "${KEYCLOAK_CLIENT_ID}" networks: - ${DOCKER_NET} @@ -31,6 +31,9 @@ services: CLIENT_SECRET_ROOT: ${CANDIG_OPA_SECRET} CLIENT_SECRET_SERVICE: ${CANDIG_OPA_SECRET_SERVICE} OPA_URL: ${OPA_URL} + IDP: "${KEYCLOAK_PRIVATE_URL}/auth/realms/${KEYCLOAK_REALM}" + KATSU_URL: "${CHORD_METADATA_PUBLIC_URL}" + opa: image: openpolicyagent/opa:latest ports: diff --git a/lib/opa/opa b/lib/opa/opa index 5de578be9..aafe9cd2d 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 5de578be93034670c57b4c69012fbb28c729d064 +Subproject commit aafe9cd2dae4f48010ee24367fec6827244e5698 diff --git a/lib/opa/opa_setup.sh b/lib/opa/opa_setup.sh index 6abbdb308..a91bbd768 100644 --- a/lib/opa/opa_setup.sh +++ b/lib/opa/opa_setup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -Eeuo pipefail +set -Euo pipefail LOGFILE=$PWD/tmp/progress.txt @@ -8,7 +8,6 @@ LOGFILE=$PWD/tmp/progress.txt opa_runner=$(docker ps | grep "opa-runner" | awk '{print $1}') docker exec $opa_runner python3 app/permissions_engine/fetch_keys.py +docker restart candigv2_opa_1 -opa=$(docker ps | grep "candigv2_opa_1" | awk '{print $1}') - -docker restart $opa +docker exec $opa_runner python3 app/tests/create_katsu_test_datasets.py diff --git a/lib/tyk/configuration_templates/api_auth.json.tpl b/lib/tyk/configuration_templates/api_auth.json.tpl index eb5e2be1e..f512f4941 100644 --- a/lib/tyk/configuration_templates/api_auth.json.tpl +++ b/lib/tyk/configuration_templates/api_auth.json.tpl @@ -21,7 +21,7 @@ "auth_header_name": "" }, - "name": "${TYK_AUTH_API_NAME}", + "name": "${TYK_AUTH_API_SLUG}", "slug": "${TYK_AUTH_API_SLUG}", "proxy": { diff --git a/lib/tyk/configuration_templates/api_candig.json.tpl b/lib/tyk/configuration_templates/api_candig.json.tpl index 40c73e4f7..370ebf70a 100644 --- a/lib/tyk/configuration_templates/api_candig.json.tpl +++ b/lib/tyk/configuration_templates/api_candig.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_CANDIG_API_ID}", - "name": "${TYK_CANDIG_API_NAME}", + "name": "${TYK_CANDIG_API_SLUG}", "use_openid": true, "active": true, "slug": "${TYK_CANDIG_API_SLUG}", @@ -92,7 +92,7 @@ "segregate_by_client": false, "providers": [ { - "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" } diff --git a/lib/tyk/configuration_templates/api_graphql.json.tpl b/lib/tyk/configuration_templates/api_graphql.json.tpl index 631292bc0..8bfdad2a7 100644 --- a/lib/tyk/configuration_templates/api_graphql.json.tpl +++ b/lib/tyk/configuration_templates/api_graphql.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_GRAPHQL_API_ID}", - "name": "${TYK_GRAPHQL_API_NAME}", + "name": "${TYK_GRAPHQL_API_SLUG}", "use_openid": true, "active": true, "slug": "${TYK_GRAPHQL_API_SLUG}", @@ -92,7 +92,7 @@ "segregate_by_client": false, "providers": [ { - "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" } diff --git a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl index c37834aba..8d49e8aee 100644 --- a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl +++ b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_KATSU_API_ID}", - "name": "${TYK_KATSU_API_NAME}", + "name": "${TYK_KATSU_API_SLUG}", "use_openid": true, "active": true, "slug": "${TYK_KATSU_API_SLUG}", diff --git a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl index 2e10feec1..c2ab9d2b0 100644 --- a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl +++ b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl @@ -1,6 +1,6 @@ { "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", - "name": ${TYK_CANDIG_DATA_PORTAL_API_NAME}, + "name": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", "use_openid": true, "active": true, "slug": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", @@ -95,7 +95,7 @@ "segregate_by_client": false, "providers": [ { - "issuer": "${KEYCLOAK_PUBLIC_URL_PROD}/auth/realms/${KEYCLOAK_REALM}", + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", "client_ids": { "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" } diff --git a/lib/tyk/configuration_templates/key_request.json.tpl b/lib/tyk/configuration_templates/key_request.json.tpl index d83a8d943..74a1f3841 100644 --- a/lib/tyk/configuration_templates/key_request.json.tpl +++ b/lib/tyk/configuration_templates/key_request.json.tpl @@ -11,7 +11,7 @@ "access_rights": { "${TYK_CANDIG_API_ID}": { "api_id": "${TYK_CANDIG_API_ID}", - "api_name": "${TYK_CANDIG_API_NAME}", + "api_name": "${TYK_CANDIG_API_SLUG}", "Versions": ["Default"] } }, diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 435cd602c..185969197 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -4,7 +4,7 @@ "${TYK_CANDIG_API_ID}": { "allowed_urls": [], "api_id": "${TYK_CANDIG_API_ID}", - "api_name": "${TYK_CANDIG_API_NAME}", + "api_name": "${TYK_CANDIG_API_SLUG}", "versions": [ "Default" ] @@ -12,7 +12,7 @@ "${TYK_KATSU_API_ID}": { "allowed_urls": [], "api_id": "${TYK_KATSU_API_ID}", - "api_name": "${TYK_KATSU_API_NAME}", + "api_name": "${TYK_KATSU_API_SLUG}", "versions": [ "Default" ] @@ -20,7 +20,7 @@ "${TYK_CANDIG_DATA_PORTAL_API_ID}": { "allowed_urls": [], "api_id": "${TYK_CANDIG_DATA_PORTAL_API_ID}", - "api_name": ${TYK_CANDIG_DATA_PORTAL_API_NAME}, + "api_name": "${TYK_CANDIG_DATA_PORTAL_API_SLUG}", "versions": [ "Default" ] @@ -28,7 +28,7 @@ "${TYK_GRAPHQL_API_ID}": { "allowed_urls": [], "api_id": "${TYK_GRAPHQL_API_ID}", - "api_name": "${TYK_GRAPHQL_API_NAME}", + "api_name": "${TYK_GRAPHQL_API_SLUG}", "versions": [ "Default" ] diff --git a/lib/tyk/tyk_key_generation.sh b/lib/tyk/tyk_key_generation.sh index dbd607080..2c8dc711a 100644 --- a/lib/tyk/tyk_key_generation.sh +++ b/lib/tyk/tyk_key_generation.sh @@ -31,22 +31,22 @@ generate_key() { "access_rights": { "'"${TYK_CANDIG_API_ID}"'": { "api_id": "'"${TYK_CANDIG_API_ID}"'", - "api_name": "'"${TYK_CANDIG_API_NAME}"'", + "api_name": "'"${TYK_CANDIG_API_SLUG}"'", "Versions": ["Default"] }, "'"${TYK_KATSU_API_ID}"'": { "api_id": "'"${TYK_KATSU_API_ID}"'", - "api_name": "'"${TYK_KATSU_API_NAME}"'", + "api_name": "'"${TYK_KATSU_API_SLUG}"'", "Versions": ["Default"] }, "'"${TYK_CANDIG_DATA_PORTAL_API_ID}"'": { "api_id": "'"${TYK_CANDIG_DATA_PORTAL_API_ID}"'", - "api_name": "'"${TYK_CANDIG_DATA_PORTAL_API_NAME}"'", + "api_name": "'"${TYK_CANDIG_DATA_PORTAL_API_SLUG}"'", "Versions": ["Default"] }, "'"${TYK_GRAPHQL_API_ID}"'": { "api_id": "'"${TYK_GRAPHQL_API_ID}"'", - "api_name": "'"${TYK_GRAPHQL_API_NAME}"'", + "api_name": "'"${TYK_GRAPHQL_API_SLUG}"'", "Versions": ["Default"] } } From d3dddfa4a42961acd5d2d79fb3a6e892a8dd8be5 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 13:04:30 -0700 Subject: [PATCH 071/236] Update htsget_app --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index f980e31c8..1b06823bc 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit f980e31c8a7c79a26af3a38c815ff40e34b2967b +Subproject commit 1b06823bcfe231f880690a92e393e84427bbd994 From 497231ff0d450443112df9fe01c968ebebb49b1e Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 11 Apr 2022 14:24:58 -0700 Subject: [PATCH 072/236] Update htsget_app --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 1b06823bc..b5c44f53b 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 1b06823bcfe231f880690a92e393e84427bbd994 +Subproject commit b5c44f53ba6e01956180474c667bd902df78df1c From 4000472833c7e35a5ec043a9a8704c88ac84ea88 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 12 Apr 2022 10:07:42 -0700 Subject: [PATCH 073/236] tiny setting tweaks (#130) * update urls for portal * don't bother with test datasets in opa --- etc/env/example.env | 4 ++-- lib/candig-data-portal/docker-compose.yml | 4 ++-- lib/opa/opa_setup.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index b760bb1b5..79197cd62 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -189,7 +189,7 @@ CHORD_METADATA_PORT=8008 CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false CHORD_METADATA_DEBUG=false -CHORD_METADATA_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} +CHORD_METADATA_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_KATSU_API_LISTEN_PATH} CHORD_METADATA_PRIVATE_URL=http://chord-metadata:8000 # candig-specific katsu @@ -208,7 +208,7 @@ CANDIG_SERVER_VERSION=1.5.0 CANDIG_SERVER_HOST=0.0.0.0 CANDIG_SERVER_PORT=3001 CANDIG_INGEST_VERSION=1.5.0 -CANDIG_PUBLIC_URL=http://${CANDIG_DOMAIN}:${CANDIG_SERVER_PORT} +CANDIG_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_CANDIG_API_LISTEN_PATH} CANDIG_PRIVATE_URL=http://candig-server:3000 # rnaget service diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index 52e96513d..903e26d1b 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -27,8 +27,8 @@ services: - "traefik.http.services.candig-data-portal.loadbalancer.server.port=${CANDIG_DATA_PORTAL_PORT}" logging: *default-logging environment: - - REACT_APP_KATSU_API_SERVER=${TYK_KATSU_API_TARGET} - - REACT_APP_CANDIG_SERVER=${TYK_CANDIG_API_TARGET} + - REACT_APP_KATSU_API_SERVER=${CHORD_METADATA_PUBLIC_URL} + - REACT_APP_CANDIG_SERVER=${CANDIG_PUBLIC_URL} #- REACT_APP_FEDERATION_API_SERVER=http://${CANDIG_DOMAIN}:${FEDERATION_PORT} #- REACT_APP_BASE_NAME='/' - REACT_APP_SITE_LOCATION=${CANDIG_SITE_LOCATION} diff --git a/lib/opa/opa_setup.sh b/lib/opa/opa_setup.sh index a91bbd768..64c49b015 100644 --- a/lib/opa/opa_setup.sh +++ b/lib/opa/opa_setup.sh @@ -10,4 +10,4 @@ opa_runner=$(docker ps | grep "opa-runner" | awk '{print $1}') docker exec $opa_runner python3 app/permissions_engine/fetch_keys.py docker restart candigv2_opa_1 -docker exec $opa_runner python3 app/tests/create_katsu_test_datasets.py +# docker exec $opa_runner python3 app/tests/create_katsu_test_datasets.py From 59722705c460e3efde2f57b6e838d401bb0ee3e4 Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu Date: Thu, 21 Apr 2022 14:10:32 -0400 Subject: [PATCH 074/236] Build the auth containers as well on VirtualBox (#136) --- Vagrantfile | 1 + setup_auth.sh | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100755 setup_auth.sh diff --git a/Vagrantfile b/Vagrantfile index 668382157..b6e660bfe 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -31,6 +31,7 @@ Vagrant.configure('2') do |config| override.vm.provision 'shell', privileged: false, path: "provision.sh", args: ["/home/vagrant/candig"] override.vm.provision :reload override.vm.provision 'shell', privileged: false, path: "setup_containers.sh", args: ["/home/vagrant/candig"] + override.vm.provision 'shell', privileged: false, path: "setup_auth.sh", args: ["/home/vagrant/candig"] end config.vm.provider :libvirt do |libvirt, override| diff --git a/setup_auth.sh b/setup_auth.sh new file mode 100755 index 000000000..15e3408ec --- /dev/null +++ b/setup_auth.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +LOGFILE=$PWD/candig/tmp/progress.txt + +grep -q "finished provision.sh" $LOGFILE +if [ $? -ne 0 ]; then + echo "ERROR! Provisioning failed, do not set up containers" | tee -a $LOGFILE + exit 1 +fi +echo "started setup_containers.sh" | tee -a $LOGFILE +cd $1 +source $PWD/bin/miniconda3/etc/profile.d/conda.sh +conda activate candig +export PATH="$PWD/bin:$PATH" +eval $(docker-machine env manager) + +echo " started make init-authx " | tee -a $LOGFILE +make init-authx +if [ $? -ne 0 ]; then + echo "ERROR! make init-authx failed" | tee -a $LOGFILE + exit 1 +fi + +echo "finished setup_auth.sh" | tee -a $LOGFILE From 7b4fd368dc705921ecbcfd756206c7f92db522cf Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu Date: Thu, 21 Apr 2022 14:22:06 -0400 Subject: [PATCH 075/236] Updated list of module names in the example (#133) --- etc/env/example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/env/example.env b/etc/env/example.env index 79197cd62..a9d0b08a4 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope minio drs-server htsget-server chord-metadata candig-server federation-service candig-data-portal #wes-server jupyter igv authentication authorization traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard +CANDIG_MODULES=weavescope minio drs-server htsget-server chord-metadata candig-server federation-service candig-data-portal #wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] From 524f077071fa32de9db243d9bac61ee8c659570f Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 21 Apr 2022 18:06:51 -0700 Subject: [PATCH 076/236] More tweaks (#131) * update urls for portal * don't bother with test datasets in opa * Add an ingest URL for bypassing tyk * inside katsu, the opa url should be the internal one * Update chord_metadata_service * add ssl-cert as a dependency for init-docker * grab container name from docker ps --- Makefile | 4 ++-- etc/env/example.env | 1 + lib/chord-metadata/chord_metadata_service | 2 +- lib/chord-metadata/docker-compose.yml | 2 +- lib/opa/opa_setup.sh | 5 +++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 742ae948d..41a331795 100644 --- a/Makefile +++ b/Makefile @@ -530,7 +530,7 @@ init-conda: #<<< .PHONY: init-docker -init-docker: docker-networks docker-volumes docker-secrets +init-docker: ssl-cert docker-networks docker-volumes docker-secrets #>>> @@ -539,7 +539,7 @@ init-docker: docker-networks docker-volumes docker-secrets #<<< .PHONY: init-kubernetes -init-kubernetes:ssl-cert docker-secrets docker-pull +init-kubernetes: ssl-cert docker-secrets docker-pull $(DIR)/bin/kubectl create namespace $(DOCKER_NAMESPACE) diff --git a/etc/env/example.env b/etc/env/example.env index a9d0b08a4..93075911a 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -190,6 +190,7 @@ CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false CHORD_METADATA_DEBUG=false CHORD_METADATA_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_KATSU_API_LISTEN_PATH} +CHORD_METADATA_INGEST_URL=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} CHORD_METADATA_PRIVATE_URL=http://chord-metadata:8000 # candig-specific katsu diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 6d58fd722..819158313 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 6d58fd7225c1c91d8cf428135c4df757cff3003d +Subproject commit 8191583135d669289eb90ae8d1f8ef6100f27684 diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index d8c12d183..2998c9255 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -37,7 +37,7 @@ services: - POSTGRES_USER=admin - POSTGRES_PASSWORD_FILE=/run/secrets/metadata_db_secret - INSIDE_CANDIG=${INSIDE_CANDIG} - - CANDIG_OPA_URL=${OPA_URL} + - CANDIG_OPA_URL=${OPA_PRIVATE_URL} - CANDIG_AUTHORIZATION=${CANDIG_AUTHORIZATION} - CACHE_TIME=${CACHE_TIME} - CANDIG_OPA_SECRET=${CANDIG_OPA_SECRET} diff --git a/lib/opa/opa_setup.sh b/lib/opa/opa_setup.sh index 64c49b015..4dfe798e4 100644 --- a/lib/opa/opa_setup.sh +++ b/lib/opa/opa_setup.sh @@ -5,9 +5,10 @@ set -Euo pipefail LOGFILE=$PWD/tmp/progress.txt -opa_runner=$(docker ps | grep "opa-runner" | awk '{print $1}') +opa_runner=$(docker ps --format "{{.Names}}" | grep "opa-runner" | awk '{print $1}') +opa_container=$(docker ps --format "{{.Names}}" | grep "opa_" | awk '{print $1}') docker exec $opa_runner python3 app/permissions_engine/fetch_keys.py -docker restart candigv2_opa_1 +docker restart $opa_container # docker exec $opa_runner python3 app/tests/create_katsu_test_datasets.py From 9a360f343a0e2feccb69564c01ddfeca834c7c71 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 2 May 2022 23:45:16 -0700 Subject: [PATCH 077/236] DIG-828: HTSget should be behind Tyk (#138) * add htsget to tyk policy/api * Add htsget to tyk * add htsget stuff to .env * Update example.env * Revert "Update example.env" This reverts commit ae612ec7a3e6edee8092d98c17a2405087848dd5. --- etc/env/example.env | 7 + lib/tyk/Dockerfile | 1 + .../api_htsget.json.tpl | 153 ++++++++++++++++++ .../configuration_templates/policies.json.tpl | 8 + lib/tyk/tyk_key_generation.sh | 5 + lib/tyk/tyk_setup.sh | 3 + 6 files changed, 177 insertions(+) create mode 100644 lib/tyk/configuration_templates/api_htsget.json.tpl diff --git a/etc/env/example.env b/etc/env/example.env index 93075911a..8c01c6d51 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -127,6 +127,7 @@ CHORD_DRS_PORT=6000 # htsget-app HTSGET_APP_VERSION=v0.1.5 +HTSGET_PRIVATE_URL=http://htsget-app:3000 HTSGET_APP_PORT=3333 # wes server @@ -284,6 +285,12 @@ TYK_GRAPHQL_API_SLUG=graphql-interface TYK_GRAPHQL_API_TARGET=${GRAPHQL_PRIVATE_URL} TYK_GRAPHQL_API_LISTEN_PATH=graphql +## api - htsget +TYK_HTSGET_API_ID=61 +TYK_HTSGET_API_SLUG=htsget +TYK_HTSGET_API_TARGET=${HTSGET_PRIVATE_URL} +TYK_HTSGET_API_LISTEN_PATH=genomics + ## Extra APIs can be added here ## api - example #TYK_EXAMPLE_API_ID=666 diff --git a/lib/tyk/Dockerfile b/lib/tyk/Dockerfile index b48646515..c2e4976ff 100644 --- a/lib/tyk/Dockerfile +++ b/lib/tyk/Dockerfile @@ -19,6 +19,7 @@ COPY ./tmp/policies/policies.json /opt/tyk-gateway/policies/policies.json COPY ./tmp/apps/api_katsu.json /opt/tyk-gateway/apps/api_katsu.json COPY ./tmp/apps/api_candig_data_portal.json /opt/tyk-gateway/apps/api_candig_data_portal.json COPY ./tmp/apps/api_graphql.json /opt/tyk-gateway/apps/api_graphql.json +COPY ./tmp/apps/api_htsget.json /opt/tyk-gateway/apps/api_htsget.json ## Extra APIs can be added here #COPY ./tmp/apps/api_example.json /opt/tyk-gateway/apps/api_example.json diff --git a/lib/tyk/configuration_templates/api_htsget.json.tpl b/lib/tyk/configuration_templates/api_htsget.json.tpl new file mode 100644 index 000000000..386d794c7 --- /dev/null +++ b/lib/tyk/configuration_templates/api_htsget.json.tpl @@ -0,0 +1,153 @@ +{ + "api_id": "${TYK_HTSGET_API_ID}", + "name": "${TYK_HTSGET_API_SLUG}", + "use_openid": true, + "active": true, + "slug": "${TYK_HTSGET_API_SLUG}", + + "enable_signature_checking": false, + + "jwt_issued_at_validation_skew": 0, + "jwt_expires_at_validation_skew": 0, + "upstream_certificates": {}, + "use_keyless": false, + "enable_coprocess_auth": false, + "base_identity_provided_by": "", + + "proxy": { + "target_url": "${TYK_HTSGET_API_TARGET}", + "strip_listen_path": true, + "disable_strip_slash": false, + "listen_path": "/${TYK_HTSGET_API_LISTEN_PATH}", + "transport": { + "ssl_insecure_skip_verify": false, + "ssl_ciphers": [], + "ssl_min_version": 0, + "proxy_url": "" + }, + "target_list": [], + "preserve_host_header": false + }, + + "version_data": { + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true + } + }, + "extended_paths": { + "ignored": [ + { + "path": "${KEYCLOAK_LOGIN_REDIRECT_PATH}", + "method_actions": { + "GET": { + "action": "no_action", + "code": 200, + "headers": {} + } + } + } + ] + } + }, + "custom_middleware": { + "pre": [ + { + "name": "backendAuthMiddleware", + "path": "/opt/tyk-gateway/middleware/backendAuthMiddleware.js", + "require_session": false + } + ], + "post": [ + { + "name": "permissionsStoreMiddleware", + "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", + "require_session": false + } + ], + "id_extractor": { + "extract_with": "", + "extract_from": "", + "extractor_config": {} + }, + "driver": "", + "auth_check": { + "path": "", + "require_session": false, + "name": "" + }, + "post_key_auth": [], + "response": [] + }, + + "config_data": { + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", + "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", + "VAULT_ROLE":"researcher" + }, + "openid_options": { + "segregate_by_client": false, + "providers": [ + { + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", + "client_ids": { + "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" + } + } + ] + }, + + + "definition": { + "location": "header", + "key": "x-api-version" + }, + + + "internal": false, + "jwt_skip_kid": false, + "enable_batch_request_support": false, + "response_processors": [], + "use_mutual_tls_auth": false, + "basic_auth": { + "disable_caching": false, + "cache_ttl": 0, + "extract_from_body": false, + "body_user_regexp": "", + "body_password_regexp": "" + }, + "use_standard_auth": false, + "session_lifetime": 0, + "use_oauth2": false, + "jwt_source": "", + "jwt_signing_method": "", + "jwt_not_before_validation_skew": 0, + "jwt_identity_base_field": "", + + "session_provider": { + "name": "", + "storage_engine": "", + "meta": {} + }, + + "auth": { + "use_param": false, + "param_name": "", + "use_cookie": false, + "cookie_name": "", + "auth_header_name": "", + "use_certificate": false, + "validate_signature": false, + "signature": { + "algorithm": "", + "header": "", + "secret": "", + "allowed_clock_skew": 0, + "error_code": 0, + "error_message": "" + } + } +} diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 185969197..8db93ea8d 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -32,6 +32,14 @@ "versions": [ "Default" ] + }, + "${TYK_HTSGET_API_ID}": { + "allowed_urls": [], + "api_id": "${TYK_HTSGET_API_ID}", + "api_name": "${TYK_HTSGET_API_SLUG}", + "versions": [ + "Default" + ] } }, "active": true, diff --git a/lib/tyk/tyk_key_generation.sh b/lib/tyk/tyk_key_generation.sh index 2c8dc711a..63e8974d0 100644 --- a/lib/tyk/tyk_key_generation.sh +++ b/lib/tyk/tyk_key_generation.sh @@ -48,6 +48,11 @@ generate_key() { "api_id": "'"${TYK_GRAPHQL_API_ID}"'", "api_name": "'"${TYK_GRAPHQL_API_SLUG}"'", "Versions": ["Default"] + }, + "'"${TYK_HTSGET_API_ID}"'": { + "api_id": "'"${TYK_HTSGET_API_ID}"'", + "api_name": "'"${TYK_HTSGET_API_SLUG}"'", + "Versions": ["Default"] } } }' diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh index b1ca57b89..22b86fd25 100755 --- a/lib/tyk/tyk_setup.sh +++ b/lib/tyk/tyk_setup.sh @@ -80,6 +80,9 @@ envsubst < ${PWD}/lib/tyk/configuration_templates/api_mcode_candig_data_portal.j echo "Working on api_graphql.json" envsubst < ${PWD}/lib/tyk/configuration_templates/api_graphql.json.tpl > ${CONFIG_DIR}/apps/api_graphql.json +echo "Working on api_htsget.json" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/api_htsget.json.tpl > ${CONFIG_DIR}/apps/api_htsget.json + # Extra APIs can be added here #echo "Working on api_example.json" #envsubst < ${PWD}/lib/tyk/configuration_templates/api_example.json.tpl > ${CONFIG_DIR}/apps/api_example.json From f702256c4dda58f368fd2cfea2d22fe4fb419e36 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 6 May 2022 12:29:09 -0700 Subject: [PATCH 078/236] Assign site-admin credentials to user2 (#139) * create credential for site_admin * assign site_admin to user2 --- etc/env/example.env | 1 + lib/keycloak/keycloak_setup.sh | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/etc/env/example.env b/etc/env/example.env index 8c01c6d51..f2ea97fb8 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -318,6 +318,7 @@ CANDIG_OPA_SECRET=my-secret-root-token CANDIG_OPA_SECRET_SERVICE=my-secret-service-token OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} OPA_PRIVATE_URL=http://opa:8181 +OPA_SITE_ADMIN_KEY=site_admin # cancogen_dashboard diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh index 5f3f01e9f..f4f65edb8 100644 --- a/lib/keycloak/keycloak_setup.sh +++ b/lib/keycloak/keycloak_setup.sh @@ -142,6 +142,20 @@ set_client() { "claim.name": "trusted_researcher", "jsonType.label": "boolean" } + }, + { + "name": "'${OPA_SITE_ADMIN_KEY}'", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "'${OPA_SITE_ADMIN_KEY}'", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "'${OPA_SITE_ADMIN_KEY}'", + "jsonType.label": "boolean" + } } ] }' @@ -257,7 +271,7 @@ echo $KEYCLOAK_PUBLIC_KEY > tmp/secrets/keycloak-public-key | tee -a $LOGFILE if [[ ${KEYCLOAK_GENERATE_TEST_USER} == 1 ]]; then echo "Adding test user" | tee -a $LOGFILE add_user "$(cat tmp/secrets/keycloak-test-user)" "$(cat tmp/secrets/keycloak-test-user-password)" "trusted_researcher" - add_user "$(cat tmp/secrets/keycloak-test-user2)" "$(cat tmp/secrets/keycloak-test-user2-password)" "stranger" + add_user "$(cat tmp/secrets/keycloak-test-user2)" "$(cat tmp/secrets/keycloak-test-user2-password)" ${OPA_SITE_ADMIN_KEY} fi #set_trusted_researcher "$(cat tmp/secrets/keycloak-test-user)" From f7b3e71b2130ddd12342487b5b639479a0d14e26 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 12 May 2022 21:17:30 -0700 Subject: [PATCH 079/236] need to pass opa_site_admin_key to opa's Dockerfile --- lib/opa/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index ca7471cc7..22e758a13 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -9,6 +9,7 @@ services: katsu_url: "${CHORD_METADATA_PUBLIC_URL}" idp: "${KEYCLOAK_PRIVATE_URL}/auth/realms/${KEYCLOAK_REALM}" client_id: "${KEYCLOAK_CLIENT_ID}" + opa_site_admin_key: "${OPA_SITE_ADMIN_KEY}" networks: - ${DOCKER_NET} deploy: From 34944a584fc001dd86164f98bc457e43de2e9bfe Mon Sep 17 00:00:00 2001 From: Laiba Zaman <77404125+Zamm178@users.noreply.github.com> Date: Fri, 13 May 2022 15:27:48 -0400 Subject: [PATCH 080/236] DIG-663 & DIG-763 - Adding Vault Helper Tool to Candigv2 & Documentation for Running VHT (#137) * added VHT as git submodule * modified gitmodules * deleted submodule * moved submodule * adding docs file for testing guide * Remove Vault Helper tool from git submodules Co-authored-by: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> --- docs/run-vault-helper-tool.md | 172 ++++++++++++++++++++++++++++++++++ etc/env/example.env | 4 + lib/vault/Vault-Helper-Tool | 1 + 3 files changed, 177 insertions(+) create mode 100644 docs/run-vault-helper-tool.md create mode 160000 lib/vault/Vault-Helper-Tool diff --git a/docs/run-vault-helper-tool.md b/docs/run-vault-helper-tool.md new file mode 100644 index 000000000..097935f3e --- /dev/null +++ b/docs/run-vault-helper-tool.md @@ -0,0 +1,172 @@ +# User Guide on How to Test the Vault Helper Tool + +## How to run +Ensure that you have followed the commands in `install-docker.md` to initialize Candigv2 with docker, then run +``` +make compose +make init-authx +``` +After this, look at the `keys.txt` file in the vault folder to find the root access token. This token will need to be updated in the `token.txt` file in the root directory. +Then either run +``` +go install -v https://github.com/CanDIG/Vault-Helper-Tool +``` +or run the following commands from the root of the Candigv2 repo to copy over the binary file. (This is necessary since the paths in the settings are configured to work for Candigv2). +``` +cd lib/vault/Vault-Helper-Tool/cli \ +go build \ +cd - \ +cp lib/vault/Vault-Helper-Tool/cli/cli . +``` +From the root of the `Candigv2` repository, run +``` +./cli {command} {optional-arguments} +``` +## How to use Tool + +- To call `write`, use: +``` +./cli write {json file} +``` +or after running the cli as +``` +write {json file} +``` + +- To call `read`, use: + +``` +./cli read {user's name} +``` +or after running the cli as +``` +read {user's name} +``` + +- To call `delete`, use: +``` +./cli delete {user's name} +``` +or after running the cli as +``` +delete {user's name} +``` + +- To call `list`, use: + +``` +./cli list +``` +or after running the cli as +``` +list +``` + +- To call `updateRole`, use: +``` +$ ./cli updateRole {path-to-json-for-role} {role} +``` +or after running the cli as +``` +updateRole {path-to-json-for-role} {role} +``` + +- To call `help`, use: + +``` +./cli -h +``` +or after running the cli as +``` +./cli +``` + +## Examples for Proper usage +- Write: +``` +$ ./cli write Vault-Helper-Tool/example.json +Secret written successfully. +``` +- Read: +``` +$ ./cli read entity_1cd0efa6 +Connecting to Vault using token in token.txt +{"dataset123":"4","dataset321":"4"} +``` +- Delete: +``` +$ ./cli delete entity_1cd0efa6 +User deleted successfully. +``` +- List: +``` +$ ./cli list +Connecting to Vault using token in token.txt +entity_1cd0efa6 +{"dataset123":"4","dataset321":"4"} +------------------------- +entity_c65b1f1a +{"dataset123":"1","dataset321":"1"} +------------------------- +``` +- updateRole +``` +$ ./cli updateRole researcher Vault-Helper-Tool/role.json +Connecting to Vault using token in token.txt +Role updated successfully. +``` +## How to Trigger Errors + +### Incorrect number of arguments +``` +$ ./cli write +Connecting to Vault using token in token.txt +2022/04/12 05:49:59 middleware errored: validation failed: file name not provided +``` + +``` +$ ./cli read +Connecting to Vault using token in token.txt +2022/04/12 05:49:32 middleware errored: validation failed: no arguments provided, missing user's name +``` + +``` +./cli delete +Connecting to Vault using token in token.txt +2022/04/12 05:50:35 middleware errored: validation failed: no arguments provided, missing user's name +``` +``` +./cli updateRole +Connecting to Vault using token in token.txt +2022/04/12 05:50:35 middleware errored: validation failed: no arguments provided, missing filename +``` + +### Wrong file name +``` +$ ./cli write non-file.json +Connecting to Vault using token in token.txt +2022/04/12 05:53:07 middleware errored: handling failed: could not open file. open non-file.json: no such file or directory + +``` + +### User/Role does not exist in vault + +``` +$ ./cli read non-user +Connecting to Vault using token in token.txt +2022/04/12 05:52:36 middleware errored: handling failed: non-user does not exist in Vault. +``` + +``` +$ ./cli delete non-user +Connecting to Vault using token in token.txt +2022/04/14 10:43:15 middleware errored: handling failed: non-user does not exist in Vault. + +``` + +``` +$ ./cli ur Vault-Helper-Tool/non-role.json researcher +Connecting to Vault using token in token.txt +2022/04/19 08:20:39 middleware errored: handling failed: could not open file. open Vault-Helper-Tool/non-role.json: no such file or directory + +``` \ No newline at end of file diff --git a/etc/env/example.env b/etc/env/example.env index f2ea97fb8..0847dc2d7 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -349,3 +349,7 @@ GRAPHQL_CANDIG_SERVER=http://${TYK_LOGIN_TARGET_URL}/${TYK_CANDIG_API_LISTEN_PAT GRAPHQL_BEACON_ID=com.candig.graphql GRAPHQL_KATSU_TOKEN_KEY=authorization GRAPHQL_CANDIG_TOKEN_KEY=authorization + +# vault helper tool +TOKEN_PATH = ${PWD}/Vault-Helper-Tool/token.txt +PROGRESS_FILE = ${PWD}/tmp/progress.txt \ No newline at end of file diff --git a/lib/vault/Vault-Helper-Tool b/lib/vault/Vault-Helper-Tool new file mode 160000 index 000000000..79962d8a3 --- /dev/null +++ b/lib/vault/Vault-Helper-Tool @@ -0,0 +1 @@ +Subproject commit 79962d8a34709171f0aefbaf234637be97890346 From c080c5123fe73ddc36209e2ac5c9711403b54af4 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 13 May 2022 12:38:46 -0700 Subject: [PATCH 081/236] remove directory for VHT --- lib/vault/Vault-Helper-Tool | 1 - 1 file changed, 1 deletion(-) delete mode 160000 lib/vault/Vault-Helper-Tool diff --git a/lib/vault/Vault-Helper-Tool b/lib/vault/Vault-Helper-Tool deleted file mode 160000 index 79962d8a3..000000000 --- a/lib/vault/Vault-Helper-Tool +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 79962d8a34709171f0aefbaf234637be97890346 From c1af31688874da92b77da6a8f9caafc46283aff9 Mon Sep 17 00:00:00 2001 From: Shaikh Rashid Date: Tue, 7 Jun 2022 20:05:25 -0400 Subject: [PATCH 082/236] =?UTF-8?q?bump=20candig-data-portalversion=20?= =?UTF-8?q?=E2=AC=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/env/example.env | 4 ++-- lib/candig-data-portal/Dockerfile | 9 +++------ lib/candig-data-portal/candig-data-portal | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 0847dc2d7..e5a65891f 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -333,7 +333,7 @@ CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search # candig-data-server (previously mcode) -CANDIG_DATA_PORTAL_VERSION=v0.1.0 +CANDIG_DATA_PORTAL_VERSION=v0.1.1 CANDIG_DATA_PORTAL_PORT=2543 CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT}/data-portal CANDIG_DATA_PORTAL_PRIVATE_URL=http://candig-data-portal:3000 @@ -352,4 +352,4 @@ GRAPHQL_CANDIG_TOKEN_KEY=authorization # vault helper tool TOKEN_PATH = ${PWD}/Vault-Helper-Tool/token.txt -PROGRESS_FILE = ${PWD}/tmp/progress.txt \ No newline at end of file +PROGRESS_FILE = ${PWD}/tmp/progress.txt diff --git a/lib/candig-data-portal/Dockerfile b/lib/candig-data-portal/Dockerfile index 4ea0ec918..36bb8875a 100644 --- a/lib/candig-data-portal/Dockerfile +++ b/lib/candig-data-portal/Dockerfile @@ -4,21 +4,18 @@ FROM node:14.16.0-alpine as build LABEL Maintainer="CanDIG Project" - ENV TYK_KATSU_API_TARGET=$katsu_api_target_url - -RUN apk update -RUN apk add gettext +RUN apk update && apk add gettext WORKDIR /app/candig-data-portal ENV PATH /app/candig-data-portal/node_modules/.bin:$PATH -RUN apk add --no-cache git curl +RUN apk add --no-cache git curl vim COPY candig-data-portal . RUN npm install RUN npm run build -ENTRYPOINT ["npm", "start"] \ No newline at end of file +ENTRYPOINT ["npm", "start"] diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal index d284d86a3..abd76927b 160000 --- a/lib/candig-data-portal/candig-data-portal +++ b/lib/candig-data-portal/candig-data-portal @@ -1 +1 @@ -Subproject commit d284d86a33daa37273d1c90daee29c458bfc991e +Subproject commit abd76927be5c198a0cc0c3169a2bed2477b50bf9 From 85f99b921bfea92c86137f4e620309f118ad2904 Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Sat, 11 Jun 2022 03:54:30 -0400 Subject: [PATCH 083/236] =?UTF-8?q?Changes=20required=20in=20AuthX=20stack?= =?UTF-8?q?,=20bug=20fixes=20and=20tweaks=20=F0=9F=90=9B=F0=9F=9A=80=20(#1?= =?UTF-8?q?43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changes required in AuthX stack, bug fixes and tweaks 🐛🚀 * update keycloak url in opa docker-compose * bump candig-data-portalversion Co-authored-by: Shaikh Rashid Co-authored-by: Debian --- Makefile.authx | 12 ++++++++++++ etc/env/example.env | 1 + lib/candig-data-portal/docker-compose.yml | 2 +- lib/keycloak/docker-compose.yml | 10 +++++----- lib/keycloak/keycloak_setup.sh | 9 +++++---- lib/opa/docker-compose.yml | 4 ++-- lib/tyk/configuration_templates/api_auth.json.tpl | 1 + lib/tyk/configuration_templates/virtualLogin.js | 4 ++-- lib/tyk/configuration_templates/virtualToken.js | 2 +- lib/vault/docker-compose.yml | 1 + lib/vault/vault_setup.sh | 3 ++- 11 files changed, 33 insertions(+), 16 deletions(-) diff --git a/Makefile.authx b/Makefile.authx index dd9bd6b3b..535e31551 100644 --- a/Makefile.authx +++ b/Makefile.authx @@ -119,3 +119,15 @@ redeploy-tyk: mkdir $(MAKE) compose-tyk; \ source ${PWD}/lib/tyk/tyk_key_generation.sh; \ echo ; + + +#>>> +# run setup script for service (if available) +# SERVICE=keycloak +# make setup-$SERVICE + +#<<< +setup-%: + echo "Setting up $*" + source ${PWD}/lib/$*/$*_setup.sh + diff --git a/etc/env/example.env b/etc/env/example.env index e5a65891f..d953f6ba5 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -238,6 +238,7 @@ KEYCLOAK_CONTAINER_PORT=8080 KEYCLOAK_HOST=0.0.0.0 KEYCLOAK_PUBLIC_PROTO=http KEYCLOAK_PRIVATE_PROTO=http +KEYCLOAK_ENABLE_PROXY=false KEYCLOAK_PUBLIC_URL=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_PORT} KEYCLOAK_PUBLIC_URL_PROD=${KEYCLOAK_PUBLIC_PROTO}://${CANDIG_AUTH_DOMAIN} KEYCLOAK_PRIVATE_URL=${KEYCLOAK_PRIVATE_PROTO}://${CANDIG_AUTH_DOMAIN}:${KEYCLOAK_CONTAINER_PORT} diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index 903e26d1b..07e1de158 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -6,7 +6,7 @@ services: context: $PWD/lib/candig-data-portal args: katsu_api_target_url: "${TYK_KATSU_API_TARGET}" -# image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} + image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} networks: - ${DOCKER_NET} ports: diff --git a/lib/keycloak/docker-compose.yml b/lib/keycloak/docker-compose.yml index 2ba6d83ea..95ddb409a 100644 --- a/lib/keycloak/docker-compose.yml +++ b/lib/keycloak/docker-compose.yml @@ -5,12 +5,10 @@ services: build: context: ${PWD}/lib/keycloak args: - - BASE_IMAGE=jboss/keycloak:${KEYCLOAK_VERSION} - container_name: ${CANDIG_AUTH_DOMAIN} - #TODO: define image: tag + - BASE_IMAGE=candig/keycloak:${KEYCLOAK_VERSION} command: [ "-b", "${KEYCLOAK_HOST}", "-Dkeycloak.migration.strategy=IGNORE_EXISTING" ] ports: - - "${KEYCLOAK_PORT}:${KEYCLOAK_CONTAINER_PORT}" + - "${KEYCLOAK_CONTAINER_PORT}:8080" networks: - ${DOCKER_NET} volumes: @@ -33,13 +31,15 @@ services: environment: - KEYCLOAK_USER_FILE=/run/secrets/keycloak-admin-user - KEYCLOAK_PASSWORD_FILE=/run/secrets/keycloak-admin-password + - PROXY_ADDRESS_FORWARDING=${KEYCLOAK_ENABLE_PROXY} + #- KEYCLOAK_FRONTEND_URL=${KEYCLOAK_PUBLIC_URL_PROD} secrets: - source: keycloak-admin-user target: /run/secrets/keycloak-admin-user - source: keycloak-admin-password target: /run/secrets/keycloak-admin-password healthcheck: - test: [ "CMD", "curl", "-f", "http://0.0.0.0:${KEYCLOAK_CONTAINER_PORT}" ] + test: [ "CMD", "curl", "-f", "http://0.0.0.0:8080" ] interval: 30s timeout: 20s retries: 3 diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh index f4f65edb8..4752a8859 100644 --- a/lib/keycloak/keycloak_setup.sh +++ b/lib/keycloak/keycloak_setup.sh @@ -17,6 +17,7 @@ if [[ $KEYCLOAK_CONTAINERS -eq 0 ]]; then echo "Keycloak container started." | tee -a $LOGFILE fi +KEYCLOAK_CONTAINER=$(docker ps | grep keycloak | awk '{print $1}') add_user() { # CANDIG_AUTH_DOMAIN is the name of the keycloak server inside the compose network local username=$1 @@ -43,7 +44,7 @@ add_user() { user_id=`curl --stderr - \ -i -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ -X POST -H "Content-Type: application/json" -d "${JSON}" \ - "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/users" -k | grep "Location:" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/users" -k | grep -i "Location:" \ | sed -E s/.*users.\([a-z0-9-]+\).*/\\\1/` echo "Created user ${user_id}" | tee -a $LOGFILE @@ -163,7 +164,7 @@ set_client() { new_scope=`curl --stderr - \ -i -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ -X POST -H "Content-Type: application/json" -d "${scope_json}" \ - "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/client-scopes" -k | grep "Location:" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/client-scopes" -k | grep -i "Location:" \ | sed -E s/.*client-scopes.\([a-z0-9-]+\).*/\\\\1/` echo "Created client scope ${new_scope}" | tee -a $LOGFILE @@ -204,7 +205,7 @@ set_client() { new_client=`curl --stderr - \ -i -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ -X POST -H "Content-Type: application/json" -d "${client_json}" \ - "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/clients" -k | grep "Location:" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/clients" -k | grep -i "Location:" \ | sed -E s/.*clients.\([a-z0-9-]+\).*/\\\\1/` # TODO: security issue fix this, -k flag above ignores cert, even if the url is https @@ -277,5 +278,5 @@ fi #set_trusted_researcher "$(cat tmp/secrets/keycloak-test-user)" echo "Waiting for keycloak to restart" | tee -a $LOGFILE -while ! docker logs --tail 5 ${CANDIG_AUTH_DOMAIN} | grep "Admin console listening on http://127.0.0.1:9990"; do sleep 1; done +while ! docker logs --tail 5 ${KEYCLOAK_CONTAINER} | grep "Admin console listening on http://127.0.0.1:9990"; do sleep 1; done echo "Keycloak setup done!" | tee -a $LOGFILE diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index 22e758a13..e4cc4436b 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -7,7 +7,7 @@ services: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" katsu_url: "${CHORD_METADATA_PUBLIC_URL}" - idp: "${KEYCLOAK_PRIVATE_URL}/auth/realms/${KEYCLOAK_REALM}" + idp: "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" client_id: "${KEYCLOAK_CLIENT_ID}" opa_site_admin_key: "${OPA_SITE_ADMIN_KEY}" networks: @@ -32,7 +32,7 @@ services: CLIENT_SECRET_ROOT: ${CANDIG_OPA_SECRET} CLIENT_SECRET_SERVICE: ${CANDIG_OPA_SECRET_SERVICE} OPA_URL: ${OPA_URL} - IDP: "${KEYCLOAK_PRIVATE_URL}/auth/realms/${KEYCLOAK_REALM}" + IDP: "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" KATSU_URL: "${CHORD_METADATA_PUBLIC_URL}" opa: diff --git a/lib/tyk/configuration_templates/api_auth.json.tpl b/lib/tyk/configuration_templates/api_auth.json.tpl index f512f4941..74b67302b 100644 --- a/lib/tyk/configuration_templates/api_auth.json.tpl +++ b/lib/tyk/configuration_templates/api_auth.json.tpl @@ -9,6 +9,7 @@ "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", "KEYCLOAK_SERVER": "${KEYCLOAK_PUBLIC_URL}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "KEYCLOAK_SCOPE": "openid+email", "KEYCLOAK_RMODE": "query", "USE_SSL": false, diff --git a/lib/tyk/configuration_templates/virtualLogin.js b/lib/tyk/configuration_templates/virtualLogin.js index cbe68bae1..d1d4e74ba 100644 --- a/lib/tyk/configuration_templates/virtualLogin.js +++ b/lib/tyk/configuration_templates/virtualLogin.js @@ -65,7 +65,7 @@ var loginHelper = { "Headers": { "Content-Type": "application/x-www-form-urlencoded" }, - "Domain": spec.config_data.KEYCLOAK_SERVER, + "Domain": spec.config_data.KEYCLOAK_PRIVATE_URL, "Resource": "/auth/realms/" + spec.config_data.KEYCLOAK_REALM + "/protocol/openid-connect/token" } return loginHelper.handleTykRequest(tokenRequest) @@ -126,4 +126,4 @@ function loginHandler(request, session, spec) { return TykJsResponse(responseObject, session.meta_data) } -log("Virtual authentication endpoint initialised") \ No newline at end of file +log("Virtual authentication endpoint initialised") diff --git a/lib/tyk/configuration_templates/virtualToken.js b/lib/tyk/configuration_templates/virtualToken.js index 194cd243a..44fe0146b 100644 --- a/lib/tyk/configuration_templates/virtualToken.js +++ b/lib/tyk/configuration_templates/virtualToken.js @@ -19,7 +19,7 @@ tokenHelper = { "Headers": { "Content-Type": "application/x-www-form-urlencoded" }, - "Domain": spec.config_data.KEYCLOAK_SERVER, + "Domain": spec.config_data.KEYCLOAK_PRIVATE_URL, "Resource": "/auth/realms/" + spec.config_data.KEYCLOAK_REALM + "/protocol/openid-connect/token" } return tokenHelper.handleTykRequest(tokenRequest) diff --git a/lib/vault/docker-compose.yml b/lib/vault/docker-compose.yml index fc8d4b707..012a6b9f9 100644 --- a/lib/vault/docker-compose.yml +++ b/lib/vault/docker-compose.yml @@ -14,6 +14,7 @@ services: - vault-data:/vault environment: - VAULT_ADDR=http://127.0.0.1:8200 + - VAULT_DISABLE_MLOCK=true cap_add: - IPC_LOCK networks: diff --git a/lib/vault/vault_setup.sh b/lib/vault/vault_setup.sh index e9ad92fd8..40cd771d8 100755 --- a/lib/vault/vault_setup.sh +++ b/lib/vault/vault_setup.sh @@ -20,6 +20,7 @@ echo "Working on vault-config.json .." envsubst < ${PWD}/lib/vault/configuration_templates/vault-config.json.tpl > ${PWD}/lib/vault/tmp/vault-config.json # boot container +make build-vault make compose-vault # -- todo: run only if not already initialized -- @@ -93,7 +94,7 @@ docker exec $vault sh -c "vault write auth/jwt/role/researcher user_claim=prefer # configure jwt echo echo ">> configuring jwt stuff" -docker exec $vault sh -c "vault write auth/jwt/config oidc_discovery_url=\"${KEYCLOAK_PRIVATE_URL}/auth/realms/candig\" bound_issuer=\"${KEYCLOAK_PRIVATE_URL}/auth/realms/candig\" default_role=\"researcher\"" +docker exec $vault sh -c "vault write auth/jwt/config oidc_discovery_url=\"${KEYCLOAK_PUBLIC_URL}/auth/realms/candig\" bound_issuer=\"${KEYCLOAK_PUBLIC_URL}/auth/realms/candig\" default_role=\"researcher\"" # create users echo From dd44bd54f2c9ab800a87e75bfb3b4680d2d8a1e2 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 14 Jun 2022 10:54:58 -0700 Subject: [PATCH 084/236] Update minio container (#140) * update urls for portal * don't bother with test datasets in opa * Add an ingest URL for bypassing tyk * inside katsu, the opa url should be the internal one * Update chord_metadata_service * add a site_admin user with user attribute * allow minio console access * Update ssl certs stuff * update minio setup ports etc * add ssl support to minio * Revert "add a site_admin user with user attribute" This reverts commit 78b52080fdbc73282e45177b2c01bc61c4ff4c54. * Update alt_names.txt * add minio keys to htsget * DIG-828: HTSget should be behind Tyk (#138) * add htsget to tyk policy/api * Add htsget to tyk * add htsget stuff to .env * Update example.env * Revert "Update example.env" This reverts commit ae612ec7a3e6edee8092d98c17a2405087848dd5. * minio and ssl * remove redundant settings.py file * don't change bucket name * update to correct commit of htsget * don't need to redo ssl-certs in minio-secrets * touch up seds and alt_names.txt * add MINIO_SELF_CERT flag * pass in MINIO_SELF_CERT to minio-runner * only set up certs if MINIO_SELF_CERT is 1 * Update docker-compose.yml --- Makefile | 10 +- etc/env/example.env | 9 +- etc/ssl/alt_names.txt | 1 + etc/ssl/site.cnf | 2 +- lib/chord-metadata/docker-compose.yml | 2 - lib/chord-metadata/settings.py | 238 -------------------------- lib/compose/docker-compose.yml | 6 + lib/htsget-server/docker-compose.yml | 11 ++ lib/htsget-server/htsget_app | 2 +- lib/minio/docker-compose.yml | 36 +++- lib/minio/minio/Dockerfile | 19 ++ lib/minio/minio/setup.sh | 16 ++ 12 files changed, 105 insertions(+), 247 deletions(-) create mode 100644 etc/ssl/alt_names.txt delete mode 100644 lib/chord-metadata/settings.py create mode 100644 lib/minio/minio/Dockerfile create mode 100644 lib/minio/minio/setup.sh diff --git a/Makefile b/Makefile index 41a331795..7dbf4d5ce 100644 --- a/Makefile +++ b/Makefile @@ -669,11 +669,19 @@ ssl-cert: openssl req -new -key $(DIR)/tmp/ssl/selfsigned-site.key \ -out $(DIR)/tmp/ssl/selfsigned-site.csr -sha256 \ -subj '/C=CA/ST=ON/L=Toronto/O=CanDIG/CN=CanDIG Self-Signed Cert' + sed -i s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/etc/ssl/site.cnf openssl x509 -req -days 750 -in $(DIR)/tmp/ssl/selfsigned-site.csr -sha256 \ -CA $(DIR)/tmp/ssl/selfsigned-root-ca.crt \ -CAkey $(DIR)/tmp/ssl/selfsigned-root-ca.key \ -CAcreateserial -out $(DIR)/tmp/ssl/selfsigned-site.crt \ -extfile $(DIR)/etc/ssl/site.cnf -extensions server + sed -i s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/etc/ssl/alt_names.txt + openssl x509 -req -days 365 -in $(DIR)/tmp/ssl/selfsigned-root-ca.csr \ + -sha256 \ + -signkey $(DIR)/tmp/ssl/selfsigned-root-ca.key \ + -extfile $(DIR)/etc/ssl/alt_names.txt \ + -out $(DIR)/tmp/ssl/public.crt + openssl x509 -in $(DIR)/tmp/ssl/public.crt -out $(DIR)/tmp/ssl/cert.pem #>>> @@ -687,7 +695,7 @@ stack: #>>> -# deploy/test indivudual modules using docker stack +# deploy/test individual modules using docker stack # $module is the name of the sub-folder in lib/ # make stack-$module diff --git a/etc/env/example.env b/etc/env/example.env index d953f6ba5..195b7559b 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -110,12 +110,17 @@ TRAEFIK_SOCKET=unix:///var/run/docker.sock # minio server MINIO_VERSION=latest -MINIO_UI_PORT=9000 +MINIO_UI_PORT=9090 MINIO_PORT=9000 -MINIO_URL=http://minio:9000 +MINIO_PUBLIC_URL=https://${CANDIG_DOMAIN}:${MINIO_PORT} +MINIO_PRIVATE_URL=http://minio:9000 MINIO_BUCKET=samples MINIO_REGION=us-east-1 MINIO_DATA_DIR=/data + +# set to 1 if using SSL via self-signed certificate +MINIO_SELF_CERT=1 + # docker volume options for minio-data #MINIO_VOLUME_OPT=--driver=local #MINIO_VOLUME_OPT+=--opt=type=ext4 diff --git a/etc/ssl/alt_names.txt b/etc/ssl/alt_names.txt new file mode 100644 index 000000000..12a00cb92 --- /dev/null +++ b/etc/ssl/alt_names.txt @@ -0,0 +1 @@ +subjectAltName = DNS:CANDIG_DOMAIN diff --git a/etc/ssl/site.cnf b/etc/ssl/site.cnf index 82559b9cf..d4004c29e 100644 --- a/etc/ssl/site.cnf +++ b/etc/ssl/site.cnf @@ -3,5 +3,5 @@ authorityKeyIdentifier=keyid,issuer basicConstraints = critical,CA:FALSE extendedKeyUsage=serverAuth keyUsage = critical, digitalSignature, keyEncipherment -subjectAltName = DNS:localhost, IP:127.0.0.1 +subjectAltName = DNS:CANDIG_DOMAIN, IP:127.0.0.1 subjectKeyIdentifier=hash diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 2998c9255..cae7cc964 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -42,8 +42,6 @@ services: - CACHE_TIME=${CACHE_TIME} - CANDIG_OPA_SECRET=${CANDIG_OPA_SECRET} secrets: - - source: metadata-settings - target: /app/chord_metadata_service/metadata/settings.py - source: metadata-app-secret target: metadata_app_secret - source: metadata-db-user diff --git a/lib/chord-metadata/settings.py b/lib/chord-metadata/settings.py deleted file mode 100644 index 037fae7c5..000000000 --- a/lib/chord-metadata/settings.py +++ /dev/null @@ -1,238 +0,0 @@ -""" -Django settings for metadata project. - -Generated by 'django-admin startproject' using Django 2.2.5. - -For more information on this file, see -https://docs.djangoproject.com/en/2.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.2/ref/settings/ -""" - -import os -import sys -import logging - -from urllib.parse import urlparse -from dotenv import load_dotenv - -from .. import __version__ - -load_dotenv() - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get("SERVICE_SECRET_KEY", metadata_app_secret.read()) - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.environ.get("CHORD_DEBUG", "true").lower() == "true" - - -# CHORD-specific settings - -CHORD_URL = os.environ.get("CHORD_URL") # Leave None if not specified, for running in other contexts - -# SECURITY WARNING: Don't run with CHORD_PERMISSIONS turned off in production, -# unless an alternative permissions system is in place. -CHORD_PERMISSIONS = os.environ.get("CHORD_PERMISSIONS", str(not DEBUG)).lower() == "true" - -CHORD_SERVICE_ARTIFACT = "metadata" -CHORD_SERVICE_TYPE = f"ca.c3g.chord:{CHORD_SERVICE_ARTIFACT}:{__version__}" -CHORD_SERVICE_ID = os.environ.get("SERVICE_ID", CHORD_SERVICE_TYPE) - -# SECURITY WARNING: don't run with AUTH_OVERRIDE turned on in production! -AUTH_OVERRIDE = not CHORD_PERMISSIONS - - -# Allowed hosts - TODO: Derive from CHORD_URL - -CHORD_HOST = urlparse(CHORD_URL or "").netloc -ALLOWED_HOSTS = [CHORD_HOST or "localhost"] -if DEBUG: - ALLOWED_HOSTS = list(set(ALLOWED_HOSTS + ["localhost", "127.0.0.1", "[::1]"])) - -APPEND_SLASH = False - -# Candig-specific settings - -INSIDE_CANDIG = os.environ.get("INSIDE_CANDIG", "false").lower() == "true" -CANDIG_OPA_URL = os.environ.get("CANDIG_OPA_URL") - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'chord_metadata_service.chord.apps.ChordConfig', - 'chord_metadata_service.experiments.apps.ExperimentsConfig', - 'chord_metadata_service.patients.apps.PatientsConfig', - 'chord_metadata_service.phenopackets.apps.PhenopacketsConfig', - 'chord_metadata_service.mcode.apps.McodeConfig', - 'chord_metadata_service.resources.apps.ResourcesConfig', - 'chord_metadata_service.restapi.apps.RestapiConfig', - - 'corsheaders', - 'django_filters', - 'rest_framework', - 'django_nose', - 'rest_framework_swagger', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'bento_lib.auth.django_remote_user.BentoRemoteUserMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'chord_metadata_service.restapi.middleware.CandigAuthzMiddleware', -] - -CORS_ALLOWED_ORIGINS = [] - -ROOT_URLCONF = 'chord_metadata_service.metadata.urls' - -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'chord_metadata_service.metadata.wsgi.application' - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'console': { - 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - }, - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'console', - }, - }, - 'loggers': { - '': { - 'level': 'INFO', - 'handlers': ['console'], - }, - }, -} - -# if we are running the test suite, only log CRITICAL messages -if len(sys.argv) > 1 and sys.argv[1] == 'test': - logging.disable(logging.CRITICAL) - -# Database -# https://docs.djangoproject.com/en/2.2/ref/settings/#databases - -# DATABASES = { -# 'default': { -# 'ENGINE': 'django.db.backends.sqlite3', -# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), -# } -# } - -metadata_db_secret = open('/run/secrets/metadata_db_secret', 'r') - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get("POSTGRES_DATABASE", 'metadata'), - 'USER': os.environ.get("POSTGRES_USER", 'admin'), - 'PASSWORD': os.environ.get("POSTGRES_PASSWORD", metadata_db_secret.read()), - - # Use sockets if we're inside a CHORD container / as a priority - 'HOST': os.environ.get("POSTGRES_SOCKET_DIR", os.environ.get("POSTGRES_HOST", "localhost")), - 'PORT': os.environ.get("POSTGRES_PORT", "5432"), - } -} - -FHIR_INDEX_NAME = 'fhir_metadata' - -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'bento_lib.auth.django_remote_user.BentoRemoteUserAuthentication' - ], - 'DEFAULT_PARSER_CLASSES': ( - # allows serializers to use snake_case field names, but parse incoming data as camelCase - 'djangorestframework_camel_case.parser.CamelCaseJSONParser', - 'djangorestframework_camel_case.parser.CamelCaseFormParser', - 'djangorestframework_camel_case.parser.CamelCaseMultiPartParser', - ), - 'DEFAULT_PERMISSION_CLASSES': ['chord_metadata_service.chord.permissions.OverrideOrSuperUserOnly'], - 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', - 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] -} - -# Password validation -# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -AUTHENTICATION_BACKENDS = ["bento_lib.auth.django_remote_user.BentoRemoteUserBackend"] + ( - ["django.contrib.auth.backends.ModelBackend"] if DEBUG else []) - - -# Internationalization -# https://docs.djangoproject.com/en/2.2/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.2/howto/static-files/ - -STATIC_URL = '/static/' diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index 181619709..e7b56c56a 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -80,3 +80,9 @@ secrets: file: $PWD/tmp/secrets/keycloak-test-user-password keycloak-client-local-candig-secret: file: $PWD/tmp/secrets/keycloak-client-${KEYCLOAK_CLIENT_ID}-secret + selfsigned-site-crt: + file: $PWD/tmp/ssl/public.crt + selfsigned-site-key: + file: $PWD/tmp/ssl/selfsigned-root-ca.key + selfsigned-site-pem: + file: $PWD/tmp/ssl/cert.pem diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index e29237d26..e4c3ca295 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -10,6 +10,8 @@ services: opa_secret: "${CANDIG_OPA_SECRET}" opa_url: "${OPA_URL}" candig_auth: "${CANDIG_AUTHORIZATION}" + minio_url: "${MINIO_PRIVATE_URL}" + minio_bucket_name: "${MINIO_BUCKET}" image: ${DOCKER_REGISTRY}/htsget-app:${HTSGET_APP_VERSION:-latest} networks: - ${DOCKER_NET} @@ -29,5 +31,14 @@ services: - "traefik.docker.lbswarm=true" - "traefik.http.routers.htsget-app.rule=Host(`htsget-app.${CANDIG_DOMAIN}`)" - "traefik.http.services.htsget-app.loadbalancer.server.port=${HTSGET_APP_PORT}" + secrets: + - source: minio-access-key + target: access + - source: minio-secret-key + target: secret + - source: selfsigned-site-pem + target: cert.pem logging: *default-logging + environment: + HTSGET_TEST_KEY: "hoodlebug" command: ["--host", "0.0.0.0", "--port", "3000"] diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index b5c44f53b..6b5be827f 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit b5c44f53ba6e01956180474c667bd902df78df1c +Subproject commit 6b5be827f5816e7a81d1d99c1f3f35e3ca3b523a diff --git a/lib/minio/docker-compose.yml b/lib/minio/docker-compose.yml index 4d156083f..996e4ec41 100644 --- a/lib/minio/docker-compose.yml +++ b/lib/minio/docker-compose.yml @@ -1,6 +1,36 @@ version: '3.7' services: + minio-runner: + build: + context: $PWD/lib/minio/minio + args: + venv_python: "${VENV_PYTHON}" + alpine_version: "${ALPINE_VERSION}" + candig_domain: "${CANDIG_DOMAIN}" + networks: + - ${DOCKER_NET} + deploy: + placement: + constraints: + - node.role == worker + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + secrets: + - source: selfsigned-site-crt + target: public.crt + - source: selfsigned-site-key + target: private.key + environment: + - CANDIG_DOMAIN=${CANDIG_DOMAIN} + - MINIO_SELF_CERT=${MINIO_SELF_CERT} + logging: *default-logging + volumes: + - minio-data:/data + - minio-config:/root/.minio minio: image: minio/minio:${MINIO_VERSION:-latest} volumes: @@ -9,7 +39,8 @@ services: networks: - ${DOCKER_NET} ports: - - "${MINIO_UI_PORT}:9000" + - "${MINIO_UI_PORT}:9090" + - "${MINIO_PORT}:9000" deploy: placement: constraints: @@ -27,12 +58,13 @@ services: environment: - MINIO_REGION=${MINIO_REGION} - CANDIG_DOMAIN="${CANDIG_DOMAIN}" + - MINIO_SERVER_URL=${MINIO_PUBLIC_URL} secrets: - source: minio-access-key target: access_key - source: minio-secret-key target: secret_key - command: ["server", "/data"] + command: ["server", "/data","--console-address", ":${MINIO_UI_PORT}"] healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s diff --git a/lib/minio/minio/Dockerfile b/lib/minio/minio/Dockerfile new file mode 100644 index 000000000..a6ad6acdd --- /dev/null +++ b/lib/minio/minio/Dockerfile @@ -0,0 +1,19 @@ +ARG venv_python +ARG alpine_version +FROM python:${venv_python}-alpine${alpine_version} + +LABEL Maintainer="CanDIG Project" + +USER root + +RUN apk update + +RUN apk add --no-cache \ + bash \ + expect \ + jq \ + curl + +COPY ./ /app/ + +ENTRYPOINT ["bash", "/app/setup.sh"] diff --git a/lib/minio/minio/setup.sh b/lib/minio/minio/setup.sh new file mode 100644 index 000000000..546ccb5d4 --- /dev/null +++ b/lib/minio/minio/setup.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -Euo pipefail + +if [ $MINIO_SELF_CERT -eq 1 ]; then + rm /root/.minio/certs + mkdir /root/.minio/certs + cp /run/secrets/private.key /root/.minio/certs + cp /run/secrets/public.crt /root/.minio/certs + + mkdir /root/.minio/certs/$CANDIG_DOMAIN + cp /run/secrets/private.key /root/.minio/certs/$CANDIG_DOMAIN/ + cp /run/secrets/public.crt /root/.minio/certs/$CANDIG_DOMAIN/ +fi + +top -b From af148a39c358b88b3eaacf33a68807662ed73986 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 14 Jun 2022 11:41:33 -0700 Subject: [PATCH 085/236] Opa behind Tyk (#141) * update urls for portal * don't bother with test datasets in opa * Add an ingest URL for bypassing tyk * inside katsu, the opa url should be the internal one * Update chord_metadata_service * add a site_admin user with user attribute * allow minio console access * Update ssl certs stuff * update minio setup ports etc * add ssl support to minio * Revert "add a site_admin user with user attribute" This reverts commit 78b52080fdbc73282e45177b2c01bc61c4ff4c54. * Update alt_names.txt * add minio keys to htsget * DIG-828: HTSget should be behind Tyk (#138) * add htsget to tyk policy/api * Add htsget to tyk * add htsget stuff to .env * Update example.env * Revert "Update example.env" This reverts commit ae612ec7a3e6edee8092d98c17a2405087848dd5. * minio and ssl * remove redundant settings.py file * move vault keys to the standard tmp location * add opa to tyk * don't change bucket name * ha, forgot opa tyk api * Update example.env * forgot to update the opa commit * pick up changes * pick up changes * Update docker-compose.yml --- etc/env/example.env | 6 + lib/opa/opa | 2 +- lib/tyk/Dockerfile | 7 +- .../configuration_templates/api_opa.json.tpl | 153 ++++++++++++++++++ .../configuration_templates/policies.json.tpl | 8 + lib/tyk/tyk_setup.sh | 4 + lib/vault/vault_setup.sh | 14 +- 7 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 lib/tyk/configuration_templates/api_opa.json.tpl diff --git a/etc/env/example.env b/etc/env/example.env index 195b7559b..8a6969999 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -297,6 +297,12 @@ TYK_HTSGET_API_SLUG=htsget TYK_HTSGET_API_TARGET=${HTSGET_PRIVATE_URL} TYK_HTSGET_API_LISTEN_PATH=genomics +## api - opa +TYK_OPA_API_ID=71 +TYK_OPA_API_SLUG=opa +TYK_OPA_API_TARGET=${OPA_PRIVATE_URL} +TYK_OPA_API_LISTEN_PATH=policy + ## Extra APIs can be added here ## api - example #TYK_EXAMPLE_API_ID=666 diff --git a/lib/opa/opa b/lib/opa/opa index aafe9cd2d..ca37d74ec 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit aafe9cd2dae4f48010ee24367fec6827244e5698 +Subproject commit ca37d74ecc9e80c0b67f741c8360555be3a26d7a diff --git a/lib/tyk/Dockerfile b/lib/tyk/Dockerfile index c2e4976ff..6b293e0a6 100644 --- a/lib/tyk/Dockerfile +++ b/lib/tyk/Dockerfile @@ -13,13 +13,8 @@ COPY ./tmp/middleware/permissionsStoreMiddleware.js /opt/tyk-gateway/middleware/ COPY ./tmp/middleware/virtualLogin.js /opt/tyk-gateway/middleware/virtualLogin.js COPY ./tmp/middleware/virtualLogout.js /opt/tyk-gateway/middleware/virtualLogout.js COPY ./tmp/middleware/virtualToken.js /opt/tyk-gateway/middleware/virtualToken.js -COPY ./tmp/apps/api_candig.json /opt/tyk-gateway/apps/api_candig.json -COPY ./tmp/apps/api_auth.json /opt/tyk-gateway/apps/api_auth.json +COPY ./tmp/apps /opt/tyk-gateway/apps COPY ./tmp/policies/policies.json /opt/tyk-gateway/policies/policies.json -COPY ./tmp/apps/api_katsu.json /opt/tyk-gateway/apps/api_katsu.json -COPY ./tmp/apps/api_candig_data_portal.json /opt/tyk-gateway/apps/api_candig_data_portal.json -COPY ./tmp/apps/api_graphql.json /opt/tyk-gateway/apps/api_graphql.json -COPY ./tmp/apps/api_htsget.json /opt/tyk-gateway/apps/api_htsget.json ## Extra APIs can be added here #COPY ./tmp/apps/api_example.json /opt/tyk-gateway/apps/api_example.json diff --git a/lib/tyk/configuration_templates/api_opa.json.tpl b/lib/tyk/configuration_templates/api_opa.json.tpl new file mode 100644 index 000000000..502394ee6 --- /dev/null +++ b/lib/tyk/configuration_templates/api_opa.json.tpl @@ -0,0 +1,153 @@ +{ + "api_id": "${TYK_OPA_API_ID}", + "name": "${TYK_OPA_API_SLUG}", + "use_openid": true, + "active": true, + "slug": "${TYK_OPA_API_SLUG}", + + "enable_signature_checking": false, + + "jwt_issued_at_validation_skew": 0, + "jwt_expires_at_validation_skew": 0, + "upstream_certificates": {}, + "use_keyless": false, + "enable_coprocess_auth": false, + "base_identity_provided_by": "", + + "proxy": { + "target_url": "${TYK_OPA_API_TARGET}", + "strip_listen_path": true, + "disable_strip_slash": false, + "listen_path": "/${TYK_OPA_API_LISTEN_PATH}", + "transport": { + "ssl_insecure_skip_verify": false, + "ssl_ciphers": [], + "ssl_min_version": 0, + "proxy_url": "" + }, + "target_list": [], + "preserve_host_header": false + }, + + "version_data": { + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true + } + }, + "extended_paths": { + "ignored": [ + { + "path": "${KEYCLOAK_LOGIN_REDIRECT_PATH}", + "method_actions": { + "GET": { + "action": "no_action", + "code": 200, + "headers": {} + } + } + } + ] + } + }, + "custom_middleware": { + "pre": [ + { + "name": "backendAuthMiddleware", + "path": "/opt/tyk-gateway/middleware/backendAuthMiddleware.js", + "require_session": false + } + ], + "post": [ + { + "name": "permissionsStoreMiddleware", + "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", + "require_session": false + } + ], + "id_extractor": { + "extract_with": "", + "extract_from": "", + "extractor_config": {} + }, + "driver": "", + "auth_check": { + "path": "", + "require_session": false, + "name": "" + }, + "post_key_auth": [], + "response": [] + }, + + "config_data": { + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", + "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", + "VAULT_ROLE":"researcher" + }, + "openid_options": { + "segregate_by_client": false, + "providers": [ + { + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", + "client_ids": { + "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" + } + } + ] + }, + + + "definition": { + "location": "header", + "key": "x-api-version" + }, + + + "internal": false, + "jwt_skip_kid": false, + "enable_batch_request_support": false, + "response_processors": [], + "use_mutual_tls_auth": false, + "basic_auth": { + "disable_caching": false, + "cache_ttl": 0, + "extract_from_body": false, + "body_user_regexp": "", + "body_password_regexp": "" + }, + "use_standard_auth": false, + "session_lifetime": 0, + "use_oauth2": false, + "jwt_source": "", + "jwt_signing_method": "", + "jwt_not_before_validation_skew": 0, + "jwt_identity_base_field": "", + + "session_provider": { + "name": "", + "storage_engine": "", + "meta": {} + }, + + "auth": { + "use_param": false, + "param_name": "", + "use_cookie": false, + "cookie_name": "", + "auth_header_name": "", + "use_certificate": false, + "validate_signature": false, + "signature": { + "algorithm": "", + "header": "", + "secret": "", + "allowed_clock_skew": 0, + "error_code": 0, + "error_message": "" + } + } +} diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 8db93ea8d..9b8ebbb08 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -40,6 +40,14 @@ "versions": [ "Default" ] + }, + "${TYK_OPA_API_ID}": { + "allowed_urls": [], + "api_id": "${TYK_OPA_API_ID}", + "api_name": "${TYK_OPA_API_SLUG}", + "versions": [ + "Default" + ] } }, "active": true, diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh index 22b86fd25..1721c2e7a 100755 --- a/lib/tyk/tyk_setup.sh +++ b/lib/tyk/tyk_setup.sh @@ -83,6 +83,10 @@ envsubst < ${PWD}/lib/tyk/configuration_templates/api_graphql.json.tpl > ${CONFI echo "Working on api_htsget.json" | tee -a $LOGFILE envsubst < ${PWD}/lib/tyk/configuration_templates/api_htsget.json.tpl > ${CONFIG_DIR}/apps/api_htsget.json +echo "Working on api_opa.json" +envsubst < ${PWD}/lib/tyk/configuration_templates/api_opa.json.tpl > ${CONFIG_DIR}/apps/api_opa.json + + # Extra APIs can be added here #echo "Working on api_example.json" #envsubst < ${PWD}/lib/tyk/configuration_templates/api_example.json.tpl > ${CONFIG_DIR}/apps/api_example.json diff --git a/lib/vault/vault_setup.sh b/lib/vault/vault_setup.sh index 40cd771d8..86ecbf7e8 100755 --- a/lib/vault/vault_setup.sh +++ b/lib/vault/vault_setup.sh @@ -50,13 +50,13 @@ echo "found key5: ${key_5}" echo "found root: ${key_root}" # save keys -touch ${PWD}/lib/vault/tmp/keys.txt -echo -e "key1: ${key_1}" >> ${PWD}/lib/vault/tmp/keys.txt -echo -e "key2: ${key_2}" >> ${PWD}/lib/vault/tmp/keys.txt -echo -e "key3: ${key_3}" >> ${PWD}/lib/vault/tmp/keys.txt -echo -e "key4: ${key_4}" >> ${PWD}/lib/vault/tmp/keys.txt -echo -e "key5: ${key_5}" >> ${PWD}/lib/vault/tmp/keys.txt -echo -e "root: ${key_root}" >> ${PWD}/lib/vault/tmp/keys.txt +touch ${PWD}/tmp/vault/keys.txt +echo -e "keys: \n${key_1}" > ${PWD}/tmp/vault/keys.txt +echo -e "${key_2}" >> ${PWD}/tmp/vault/keys.txt +echo -e "${key_3}" >> ${PWD}/tmp/vault/keys.txt +echo -e "${key_4}" >> ${PWD}/tmp/vault/keys.txt +echo -e "${key_5}" >> ${PWD}/tmp/vault/keys.txt +echo -e "root: ${key_root}" >> ${PWD}/tmp/vault/keys.txt echo ">> attempting to automatically unseal vault:" From 8c129318ad9bce65effc74df025712170747e071 Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Fri, 17 Jun 2022 14:27:19 -0400 Subject: [PATCH 086/236] new release: (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bump htsget-server to v0.1.6 🧬 - bump federation-service to v0.5.2 🌎 - bump katsu to v1.4.3 📜 Co-authored-by: Shaikh Rashid --- etc/env/example.env | 6 +++--- lib/chord-metadata/chord_metadata_service | 2 +- lib/compose/docker-compose.yml | 6 +++--- lib/federation-service/docker-compose.yml | 4 ++-- lib/federation-service/federation_service | 2 +- lib/htsget-server/htsget_app | 2 +- lib/opa/opa | 2 +- lib/swarm/docker-compose.yml | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 8a6969999..2cb612518 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -131,7 +131,7 @@ CHORD_DRS_VERSION=v0.4.0 CHORD_DRS_PORT=6000 # htsget-app -HTSGET_APP_VERSION=v0.1.5 +HTSGET_APP_VERSION=v0.1.6 HTSGET_PRIVATE_URL=http://htsget-app:3000 HTSGET_APP_PORT=3333 @@ -185,12 +185,12 @@ JUPYTER_ENABLE_LAB=yes JUPYTER_ENABLE_SUDO=yes # federation_service -FEDERATION_VERSION=v0.5.1 +FEDERATION_VERSION=v0.5.2 FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 # chord metadata service -CHORD_METADATA_VERSION=v1.4.1 +CHORD_METADATA_VERSION=v1.4.3 CHORD_METADATA_PORT=8008 CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 819158313..adaf6e0cb 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 8191583135d669289eb90ae8d1f8ef6100f27684 +Subproject commit adaf6e0cb210aa0a9e979955863f1d4774a5734c diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index e7b56c56a..597709ac0 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -44,10 +44,10 @@ volumes: secrets: aws-credentials: file: $PWD/tmp/secrets/aws-credentials - federation-peers: - file: $PWD/lib/federation-service/federation_service/configs/peers.json + federation-servers: + file: $PWD/tmp/secrets/federation-servers.json federation-services: - file: $PWD/lib/federation-service/federation_service/configs/services.json + file: $PWD/tmp/secrets/federation-services.json metadata-app-secret: file: $PWD/tmp/secrets/metadata-app-secret metadata-db-user: diff --git a/lib/federation-service/docker-compose.yml b/lib/federation-service/docker-compose.yml index ff4cfa94a..fa5b00fac 100644 --- a/lib/federation-service/docker-compose.yml +++ b/lib/federation-service/docker-compose.yml @@ -28,8 +28,8 @@ services: - "traefik.http.services.federation-service.loadbalancer.server.port=${FEDERATION_PORT}" logging: *default-logging secrets: - - source: federation-peers - target: /app/federation_service/configs/peers.json + - source: federation-servers + target: /app/federation_service/configs/servers.json - source: federation-services target: /app/federation_service/configs/services.json entrypoint: ["uwsgi", "federation.ini", "--http", "0.0.0.0:4232"] diff --git a/lib/federation-service/federation_service b/lib/federation-service/federation_service index 20b9805a7..8c35db856 160000 --- a/lib/federation-service/federation_service +++ b/lib/federation-service/federation_service @@ -1 +1 @@ -Subproject commit 20b9805a7835ee25c7f176e6760be19fc7da233c +Subproject commit 8c35db856e99f4f20c5e7a18efb96731ae93eff2 diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 6b5be827f..10d07dece 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 6b5be827f5816e7a81d1d99c1f3f35e3ca3b523a +Subproject commit 10d07dece60b12c9d0d9bdf590ec729f096e9157 diff --git a/lib/opa/opa b/lib/opa/opa index ca37d74ec..daa139051 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit ca37d74ecc9e80c0b67f741c8360555be3a26d7a +Subproject commit daa139051be68094d584d8f6f7d6aecf87f5083f diff --git a/lib/swarm/docker-compose.yml b/lib/swarm/docker-compose.yml index 3bdbdd668..170cc97cd 100644 --- a/lib/swarm/docker-compose.yml +++ b/lib/swarm/docker-compose.yml @@ -45,7 +45,7 @@ volumes: secrets: aws-credentials: external: true - federation-peers: + federation-servers: external: true federation-services: external: true From c3521bed6b410ba101e8f3428aff74e11e3f12ea Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Tue, 21 Jun 2022 17:30:34 -0400 Subject: [PATCH 087/236] =?UTF-8?q?pin=20python=20version=20and=20alpine?= =?UTF-8?q?=20version=20for=20katsu=F0=9F=93=8C=20(#149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * pin python version and alpine version for katsu📌 * disabled toil from docker pull Co-authored-by: Shaikh Rashid --- Makefile | 2 +- lib/chord-metadata/docker-compose.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e0c62fadb..e37a75ad6 100644 --- a/Makefile +++ b/Makefile @@ -423,7 +423,7 @@ docker-networks: .PHONY: docker-pull docker-pull: $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) pull-$(MODULE);) - $(foreach MODULE, $(TOIL_MODULES), docker pull $(DOCKER_REGISTRY)/$(MODULE):latest;) + #$(foreach MODULE, $(TOIL_MODULES), docker pull $(DOCKER_REGISTRY)/$(MODULE):latest;) #>>> diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index cae7cc964..2e564f98d 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -5,8 +5,8 @@ services: build: context: $PWD/lib/chord-metadata/chord_metadata_service args: - venv_python: "${VENV_PYTHON}" - alpine_version: "${ALPINE_VERSION}" + venv_python: "3.10" + alpine_version: "3.16" image: ${DOCKER_REGISTRY}/chord-metadata:${CHORD_METADATA_VERSION:-latest} networks: - ${DOCKER_NET} From bd8248b2789815d446ac807fbf2c29d34791547f Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 22 Jun 2022 11:42:32 -0700 Subject: [PATCH 088/236] Vault stores s3 secrets (#142) * add a site_admin user with user attribute * Update ssl certs stuff * update minio setup ports etc * add htsget to tyk policy/api * Update example.env * minio and ssl * add vault_s3_token * reorg vault so that we can do more active token refreshing * allow vault to be accessed via tyk * Update candig-data-portal * crontab for vault * TYK_USE_SSL flag for prod * katsu needs OPA_SITE_ADMIN_KEY var * bump candig-server to version 1.6.0 * remove unneeded args --- Makefile | 14 +- Makefile.authx | 1 - etc/env/example.env | 12 +- lib/candig-data-portal/candig-data-portal | 2 +- lib/chord-metadata/docker-compose.yml | 1 + lib/compose/docker-compose.yml | 2 + lib/htsget-server/docker-compose.yml | 11 +- lib/keycloak/keycloak_setup.sh | 4 +- .../configuration_templates/api_auth.json.tpl | 2 +- .../api_vault.json.tpl | 141 ++++++++++++++++++ .../configuration_templates/policies.json.tpl | 8 + lib/tyk/tyk_key_generation.sh | 10 ++ lib/tyk/tyk_setup.sh | 2 + lib/vault/Dockerfile | 51 +++---- lib/vault/create_token.sh | 9 ++ lib/vault/docker-compose.yml | 41 ++++- lib/vault/entrypoint.sh | 33 ++++ lib/vault/vault_setup.sh | 35 +++-- 18 files changed, 311 insertions(+), 68 deletions(-) create mode 100644 lib/tyk/configuration_templates/api_vault.json.tpl create mode 100644 lib/vault/create_token.sh create mode 100644 lib/vault/entrypoint.sh diff --git a/Makefile b/Makefile index e37a75ad6..a47d6efea 100644 --- a/Makefile +++ b/Makefile @@ -463,6 +463,8 @@ docker-secrets: mkdir minio-secrets $(MAKE) secret-tyk-secret-key $(MAKE) secret-tyk-node-secret-key $(MAKE) secret-tyk-analytics-admin-key + + $(MAKE) secret-vault-s3-token #>>> @@ -615,6 +617,8 @@ minio-secrets: @echo '[default]' > $(DIR)/tmp/secrets/aws-credentials @echo "aws_access_key_id=`cat tmp/secrets/minio-access-key`" >> $(DIR)/tmp/secrets/aws-credentials @echo "aws_secret_access_key=`cat tmp/secrets/minio-secret-key`" >> $(DIR)/tmp/secrets/aws-credentials + cp $(DIR)/tmp/ssl/selfsigned-site.crt $(DIR)/tmp/secrets/selfsigned-site-crt + cp $(DIR)/tmp/ssl/selfsigned-site.key $(DIR)/tmp/secrets/selfsigned-site-key #>>> @@ -669,17 +673,19 @@ ssl-cert: openssl req -new -key $(DIR)/tmp/ssl/selfsigned-site.key \ -out $(DIR)/tmp/ssl/selfsigned-site.csr -sha256 \ -subj '/C=CA/ST=ON/L=Toronto/O=CanDIG/CN=CanDIG Self-Signed Cert' - sed -i s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/etc/ssl/site.cnf + cp $(DIR)/etc/ssl/site.cnf $(DIR)/tmp/ssl/site.cnf + sed -i s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/tmp/ssl/site.cnf openssl x509 -req -days 750 -in $(DIR)/tmp/ssl/selfsigned-site.csr -sha256 \ -CA $(DIR)/tmp/ssl/selfsigned-root-ca.crt \ -CAkey $(DIR)/tmp/ssl/selfsigned-root-ca.key \ -CAcreateserial -out $(DIR)/tmp/ssl/selfsigned-site.crt \ - -extfile $(DIR)/etc/ssl/site.cnf -extensions server - sed -i s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/etc/ssl/alt_names.txt + -extfile $(DIR)/tmp/ssl/site.cnf -extensions server + cp $(DIR)/etc/ssl/alt_names.txt $(DIR)/tmp/ssl/alt_names.txt + sed -i s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/tmp/ssl/alt_names.txt openssl x509 -req -days 365 -in $(DIR)/tmp/ssl/selfsigned-root-ca.csr \ -sha256 \ -signkey $(DIR)/tmp/ssl/selfsigned-root-ca.key \ - -extfile $(DIR)/etc/ssl/alt_names.txt \ + -extfile $(DIR)/tmp/ssl/alt_names.txt \ -out $(DIR)/tmp/ssl/public.crt openssl x509 -in $(DIR)/tmp/ssl/public.crt -out $(DIR)/tmp/ssl/cert.pem diff --git a/Makefile.authx b/Makefile.authx index 535e31551..767895fb9 100644 --- a/Makefile.authx +++ b/Makefile.authx @@ -91,7 +91,6 @@ init-authx: mkdir $(MAKE) compose-tyk; \ source ${PWD}/lib/tyk/tyk_key_generation.sh; \ echo "Setting up Vault"; \ - $(MAKE) build-vault; \ source ${PWD}/lib/vault/vault_setup.sh; \ echo "Setting up Opa"; \ $(MAKE) build-opa; \ diff --git a/etc/env/example.env b/etc/env/example.env index 2cb612518..a1a70c460 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -7,7 +7,7 @@ CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] CANDIG_DOMAIN=docker.localhost -CANDIG_AUTH_DOMAIN=auth.docker.localhost +CANDIG_AUTH_DOMAIN=docker.localhost CANDIG_SITE_LOCATION=UHN # miniconda venv @@ -211,7 +211,7 @@ CNV_SERVICE_HOST=0.0.0.0 CNV_SERVICE_PORT=8870 # candig server -CANDIG_SERVER_VERSION=1.5.0 +CANDIG_SERVER_VERSION=1.6.0 CANDIG_SERVER_HOST=0.0.0.0 CANDIG_SERVER_PORT=3001 CANDIG_INGEST_VERSION=1.5.0 @@ -262,6 +262,7 @@ TYK_ANALYTICS_FROM_EMAIL=admin@distributedgenomics.ca TYK_ANALYTICS_FROM_NAME=CanDIG Admin TYK_LISTEN_PATH= TYK_POLICY_ID=candig_policy +TYK_USE_SSL=false ## api - authentication TYK_AUTH_API_ID=11 @@ -303,6 +304,12 @@ TYK_OPA_API_SLUG=opa TYK_OPA_API_TARGET=${OPA_PRIVATE_URL} TYK_OPA_API_LISTEN_PATH=policy +## api - vault +TYK_VAULT_API_ID=81 +TYK_VAULT_API_SLUG=vault +TYK_VAULT_API_TARGET=${VAULT_SERVICE_URL} +TYK_VAULT_API_LISTEN_PATH=vault + ## Extra APIs can be added here ## api - example #TYK_EXAMPLE_API_ID=666 @@ -311,6 +318,7 @@ TYK_OPA_API_LISTEN_PATH=policy #TYK_EXAMPLE_API_LISTEN_PATH=example # vault service +VAULT_VERSION=1.10.4 VAULT_FILE_PATH="/vault/data" VAULT_TLS_DISABLE=1 VAULT_UI=true diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal index abd76927b..2a180e00f 160000 --- a/lib/candig-data-portal/candig-data-portal +++ b/lib/candig-data-portal/candig-data-portal @@ -1 +1 @@ -Subproject commit abd76927be5c198a0cc0c3169a2bed2477b50bf9 +Subproject commit 2a180e00f3dd221c4616eaeba4733eacbde2b675 diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 2e564f98d..932473128 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -41,6 +41,7 @@ services: - CANDIG_AUTHORIZATION=${CANDIG_AUTHORIZATION} - CACHE_TIME=${CACHE_TIME} - CANDIG_OPA_SECRET=${CANDIG_OPA_SECRET} + - CANDIG_OPA_SITE_ADMIN_KEY=${OPA_SITE_ADMIN_KEY} secrets: - source: metadata-app-secret target: metadata_app_secret diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index 597709ac0..a3b56b0f2 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -86,3 +86,5 @@ secrets: file: $PWD/tmp/ssl/selfsigned-root-ca.key selfsigned-site-pem: file: $PWD/tmp/ssl/cert.pem + vault-s3-token: + file: $PWD/tmp/secrets/vault-s3-token diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index e4c3ca295..686d503f5 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -7,11 +7,6 @@ services: args: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" - opa_secret: "${CANDIG_OPA_SECRET}" - opa_url: "${OPA_URL}" - candig_auth: "${CANDIG_AUTHORIZATION}" - minio_url: "${MINIO_PRIVATE_URL}" - minio_bucket_name: "${MINIO_BUCKET}" image: ${DOCKER_REGISTRY}/htsget-app:${HTSGET_APP_VERSION:-latest} networks: - ${DOCKER_NET} @@ -38,7 +33,13 @@ services: target: secret - source: selfsigned-site-pem target: cert.pem + - source: vault-s3-token + target: vault-s3-token logging: *default-logging environment: HTSGET_TEST_KEY: "hoodlebug" + OPA_SECRET: ${CANDIG_OPA_SECRET} + OPA_URL: ${OPA_URL} + CANDIG_AUTH: ${CANDIG_AUTHORIZATION} + VAULT_URL: ${VAULT_SERVICE_URL} command: ["--host", "0.0.0.0", "--port", "3000"] diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh index 4752a8859..f14ac4514 100644 --- a/lib/keycloak/keycloak_setup.sh +++ b/lib/keycloak/keycloak_setup.sh @@ -141,7 +141,7 @@ set_client() { "id.token.claim": "true", "access.token.claim": "true", "claim.name": "trusted_researcher", - "jsonType.label": "boolean" + "jsonType.label": "String" } }, { @@ -155,7 +155,7 @@ set_client() { "id.token.claim": "true", "access.token.claim": "true", "claim.name": "'${OPA_SITE_ADMIN_KEY}'", - "jsonType.label": "boolean" + "jsonType.label": "String" } } ] diff --git a/lib/tyk/configuration_templates/api_auth.json.tpl b/lib/tyk/configuration_templates/api_auth.json.tpl index 74b67302b..5adb562a2 100644 --- a/lib/tyk/configuration_templates/api_auth.json.tpl +++ b/lib/tyk/configuration_templates/api_auth.json.tpl @@ -12,7 +12,7 @@ "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "KEYCLOAK_SCOPE": "openid+email", "KEYCLOAK_RMODE": "query", - "USE_SSL": false, + "USE_SSL": ${TYK_USE_SSL}, "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", "MAX_TOKEN_AGE": 43200 diff --git a/lib/tyk/configuration_templates/api_vault.json.tpl b/lib/tyk/configuration_templates/api_vault.json.tpl new file mode 100644 index 000000000..3c3c3694e --- /dev/null +++ b/lib/tyk/configuration_templates/api_vault.json.tpl @@ -0,0 +1,141 @@ +{ + "api_id": "${TYK_VAULT_API_ID}", + "name": "${TYK_VAULT_API_SLUG}", + "use_openid": true, + "active": true, + "slug": "${TYK_VAULT_API_SLUG}", + + "enable_signature_checking": false, + + "jwt_issued_at_validation_skew": 0, + "jwt_expires_at_validation_skew": 0, + "upstream_certificates": {}, + "use_keyless": false, + "enable_coprocess_auth": false, + "base_identity_provided_by": "", + + "proxy": { + "target_url": "${TYK_VAULT_API_TARGET}", + "strip_listen_path": true, + "disable_strip_slash": false, + "listen_path": "/${TYK_VAULT_API_LISTEN_PATH}", + "transport": { + "ssl_insecure_skip_verify": false, + "ssl_ciphers": [], + "ssl_min_version": 0, + "proxy_url": "" + }, + "target_list": [], + "preserve_host_header": false + }, + + "version_data": { + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true + } + }, + "extended_paths": { + "ignored": [ + { + "path": "${KEYCLOAK_LOGIN_REDIRECT_PATH}", + "method_actions": { + "GET": { + "action": "no_action", + "code": 200, + "headers": {} + } + } + } + ] + } + }, + "custom_middleware": { + "pre": [], + "post": [], + "id_extractor": { + "extract_with": "", + "extract_from": "", + "extractor_config": {} + }, + "driver": "", + "auth_check": { + "path": "", + "require_session": false, + "name": "" + }, + "post_key_auth": [], + "response": [] + }, + + "config_data": { + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", + "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", + "VAULT_ROLE":"researcher" + }, + "openid_options": { + "segregate_by_client": false, + "providers": [ + { + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", + "client_ids": { + "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" + } + } + ] + }, + + + "definition": { + "location": "header", + "key": "x-api-version" + }, + + + "internal": false, + "jwt_skip_kid": false, + "enable_batch_request_support": false, + "response_processors": [], + "use_mutual_tls_auth": false, + "basic_auth": { + "disable_caching": false, + "cache_ttl": 0, + "extract_from_body": false, + "body_user_regexp": "", + "body_password_regexp": "" + }, + "use_standard_auth": false, + "session_lifetime": 0, + "use_oauth2": false, + "jwt_source": "", + "jwt_signing_method": "", + "jwt_not_before_validation_skew": 0, + "jwt_identity_base_field": "", + + "session_provider": { + "name": "", + "storage_engine": "", + "meta": {} + }, + + "auth": { + "use_param": false, + "param_name": "", + "use_cookie": false, + "cookie_name": "", + "auth_header_name": "", + "use_certificate": false, + "validate_signature": false, + "signature": { + "algorithm": "", + "header": "", + "secret": "", + "allowed_clock_skew": 0, + "error_code": 0, + "error_message": "" + } + } +} diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index 9b8ebbb08..cd224acba 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -48,6 +48,14 @@ "versions": [ "Default" ] + }, + "${TYK_VAULT_API_ID}": { + "allowed_urls": [], + "api_id": "${TYK_VAULT_API_ID}", + "api_name": "${TYK_VAULT_API_SLUG}", + "versions": [ + "Default" + ] } }, "active": true, diff --git a/lib/tyk/tyk_key_generation.sh b/lib/tyk/tyk_key_generation.sh index 63e8974d0..ba6af5810 100644 --- a/lib/tyk/tyk_key_generation.sh +++ b/lib/tyk/tyk_key_generation.sh @@ -53,6 +53,16 @@ generate_key() { "api_id": "'"${TYK_HTSGET_API_ID}"'", "api_name": "'"${TYK_HTSGET_API_SLUG}"'", "Versions": ["Default"] + }, + "'"${TYK_OPA_API_ID}"'": { + "api_id": "'"${TYK_OPA_API_ID}"'", + "api_name": "'"${TYK_OPA_API_SLUG}"'", + "Versions": ["Default"] + }, + "'"${TYK_VAULT_API_ID}"'": { + "api_id": "'"${TYK_VAULT_API_ID}"'", + "api_name": "'"${TYK_VAULT_API_SLUG}"'", + "Versions": ["Default"] } } }' diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh index 1721c2e7a..61afb75ae 100755 --- a/lib/tyk/tyk_setup.sh +++ b/lib/tyk/tyk_setup.sh @@ -86,6 +86,8 @@ envsubst < ${PWD}/lib/tyk/configuration_templates/api_htsget.json.tpl > ${CONFIG echo "Working on api_opa.json" envsubst < ${PWD}/lib/tyk/configuration_templates/api_opa.json.tpl > ${CONFIG_DIR}/apps/api_opa.json +echo "Working on api_vault.json" +envsubst < ${PWD}/lib/tyk/configuration_templates/api_vault.json.tpl > ${CONFIG_DIR}/apps/api_vault.json # Extra APIs can be added here #echo "Working on api_example.json" diff --git a/lib/vault/Dockerfile b/lib/vault/Dockerfile index 6f275d92b..143aceb02 100644 --- a/lib/vault/Dockerfile +++ b/lib/vault/Dockerfile @@ -1,45 +1,28 @@ -# From https://www.bogotobogo.com/DevOps/Docker/Docker-Vault-Consul.php -# base image -FROM alpine:3.7 +ARG alpine_version +FROM alpine:${alpine_version} -# set vault version -ARG VAULT_VERSION +LABEL Maintainer="CanDIG Project" -# maintainer -LABEL maintainer="brennan.brouillette@c3g.ca" +USER root -# create a new directory -RUN mkdir -p /vault/config +RUN apk update -WORKDIR /vault +RUN apk add --no-cache \ + bash \ + expect \ + jq \ + curl -# download dependencies -RUN apk --no-cache add \ - bash \ - ca-certificates \ - wget - -# download and set up vault -RUN wget --quiet --output-document=/tmp/vault.zip https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip && \ - unzip /tmp/vault.zip -d /vault && \ - rm -f /tmp/vault.zip && \ - chmod +x . - -# update PATH -ENV PATH="PATH=$PATH:$PWD" +WORKDIR /app # add the config file COPY ./tmp/vault-config.json /vault/config/vault-config.json -# expose port 8200 -EXPOSE 8200 - -# debugging -# RUN touch /tmp/run.sh -# RUN chmod +x /tmp/run.sh -# RUN echo "#!/bin/bash" >> /tmp/run.sh -# RUN echo "while true; do echo \"looping..\"; sleep 2; done" >> /tmp/run.sh -# ENTRYPOINT ["/tmp/run.sh"] +# copy entrypoint +COPY . /vault +RUN mkdir /vault/data +RUN chmod 777 /vault/data +RUN touch initial_setup # run vault -ENTRYPOINT ["vault"] \ No newline at end of file +ENTRYPOINT ["bash", "/vault/entrypoint.sh"] \ No newline at end of file diff --git a/lib/vault/create_token.sh b/lib/vault/create_token.sh new file mode 100644 index 000000000..bb85db2b7 --- /dev/null +++ b/lib/vault/create_token.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -Euo pipefail + + +export ROOT=$(tail -n 1 /vault/config/keys.txt) +export VAULT_S3_TOKEN=$(cat /run/secrets/vault-s3-token) +echo '{ "id": "'$VAULT_S3_TOKEN'", "policies": ["aws"], "period":"5h" }' > token.json +curl --request POST --header "X-Vault-Token: "$ROOT"" --data @token.json $VAULT_URL/v1/auth/token/create diff --git a/lib/vault/docker-compose.yml b/lib/vault/docker-compose.yml index 012a6b9f9..fd18fc811 100644 --- a/lib/vault/docker-compose.yml +++ b/lib/vault/docker-compose.yml @@ -2,12 +2,7 @@ version: '3.7' services: vault: - build: - context: ${PWD}/lib/vault - args: - - VAULT_VERSION=1.9.3 - - venv_python=${VENV_PYTHON} - image: ${DOCKER_REGISTRY}/vault:${VAULT_VERSION:-latest} + image: library/vault:${VAULT_VERSION} ports: - ${VAULT_SERVICE_PORT}:8200 volumes: @@ -33,10 +28,42 @@ services: - "traefik.docker.lbswarm=true" - "traefik.http.routers.vault.rule=Host(`vault.${CANDIG_DOMAIN}`)" - "traefik.http.services.vault.loadbalancer.server.port=${VAULT_SERVICE_PORT}" + secrets: + - source: vault-s3-token + target: vault-s3-token logging: *default-logging - command: server -config=/vault/config/vault-config.json + command: vault server -config=/vault/config/vault-config.json healthcheck: test: [ "CMD", "curl", "-f", "http://0.0.0.0:${VAULT_SERVICE_PORT}/ui/" ] interval: 30s timeout: 20s retries: 3 + vault-runner: + build: + context: $PWD/lib/vault + args: + alpine_version: "${ALPINE_VERSION}" + volumes: + - vault-data:/vault + networks: + - ${DOCKER_NET} + environment: + - VAULT_URL=${VAULT_SERVICE_URL} + deploy: + placement: + constraints: + - node.role == manager + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + labels: + - "traefik.enable=true" + - "traefik.docker.lbswarm=true" + - "traefik.http.routers.vault.rule=Host(`vault.${CANDIG_DOMAIN}`)" + - "traefik.http.services.vault.loadbalancer.server.port=${VAULT_SERVICE_PORT}" + secrets: + - source: vault-s3-token + target: vault-s3-token + logging: *default-logging diff --git a/lib/vault/entrypoint.sh b/lib/vault/entrypoint.sh new file mode 100644 index 000000000..600a5c777 --- /dev/null +++ b/lib/vault/entrypoint.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -Euo pipefail + + +if [[ -f "initial_setup" ]]; then + rm initial_setup +else + sleep 10 + # unseal vault + KEY=$(head -n 2 /vault/config/keys.txt | tail -n 1) + echo '{ "key": "'$KEY'" }' > payload.json + curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal + KEY=$(head -n 3 /vault/config/keys.txt | tail -n 1) + echo '{ "key": "'$KEY'" }' > payload.json + curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal + KEY=$(head -n 4 /vault/config/keys.txt | tail -n 1) + echo '{ "key": "'$KEY'" }' > payload.json + curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal + +fi + +bash /vault/create_token.sh +# set up crontab +crontab -l > cron_bkp +echo "0 */5 * * * bash /vault/create_token.sh" >> cron_bkp +crontab cron_bkp +rm cron_bkp + +while [ 0 -eq 0 ] +do + sleep 60 +done diff --git a/lib/vault/vault_setup.sh b/lib/vault/vault_setup.sh index 86ecbf7e8..9fbbbb1e4 100755 --- a/lib/vault/vault_setup.sh +++ b/lib/vault/vault_setup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -Eexo pipefail +set -Euo pipefail # This script will set up a full vault environment on your local CanDIGv2 cluster @@ -23,15 +23,19 @@ envsubst < ${PWD}/lib/vault/configuration_templates/vault-config.json.tpl > ${PW make build-vault make compose-vault -# -- todo: run only if not already initialized -- -# --- temp -echo ">> waiting 3 seconds to let vault start" -sleep 3 -# --- +echo ">> waiting for vault to start" +docker ps | grep vault +while [ $? -ne 0 ] +do + echo "..." + sleep 1 + docker ps | grep vault +done +sleep 5 # gather keys and login token echo ">> gathering keys" -vault=$(docker ps | grep vault | awk '{print $1}') +vault=$(docker ps | grep vault_1 | awk '{print $1}') stuff=$(docker exec $vault sh -c "vault operator init") # | head -7 | rev | cut -d " " -f1 | rev) echo "found stuff as ${stuff}" @@ -56,8 +60,9 @@ echo -e "${key_2}" >> ${PWD}/tmp/vault/keys.txt echo -e "${key_3}" >> ${PWD}/tmp/vault/keys.txt echo -e "${key_4}" >> ${PWD}/tmp/vault/keys.txt echo -e "${key_5}" >> ${PWD}/tmp/vault/keys.txt -echo -e "root: ${key_root}" >> ${PWD}/tmp/vault/keys.txt +echo -e "root: \n${key_root}" >> ${PWD}/tmp/vault/keys.txt +docker cp ${PWD}/tmp/vault/keys.txt $vault:/vault/config/ echo ">> attempting to automatically unseal vault:" docker exec $vault sh -c "vault operator unseal ${key_1}" @@ -86,11 +91,17 @@ echo echo ">> setting up tyk policy" docker exec $vault sh -c "echo 'path \"identity/oidc/token/*\" {capabilities = [\"create\", \"read\"]}' >> vault-policy.hcl; vault policy write tyk vault-policy.hcl" +echo +echo ">> setting up aws policy" +docker exec $vault sh -c "echo 'path \"aws/*\" {capabilities = [\"create\", \"read\"]}' >> vault-policy.hcl; vault policy write aws vault-policy.hcl" + # user claims echo echo ">> setting up user claims" docker exec $vault sh -c "vault write auth/jwt/role/researcher user_claim=preferred_username bound_audiences=${KEYCLOAK_CLIENT_ID} role_type=jwt policies=tyk ttl=1h" +docker exec $vault sh -c "vault write auth/jwt/role/site_admin user_claim=site_admin bound_audiences=${KEYCLOAK_CLIENT_ID} role_type=jwt policies=aws ttl=1h" + # configure jwt echo echo ">> configuring jwt stuff" @@ -157,8 +168,10 @@ echo ">> matching key and inserting custom info into the jwt" # messes up Vault and it complains that there is a mismatch in balance of braces VAULT_IDENTITY_ROLE_TEMPLATE=$(envsubst < ${PWD}/lib/vault/configuration_templates/vault-datastructure.json.tpl) docker exec $vault sh -c "echo '${VAULT_IDENTITY_ROLE_TEMPLATE}' > researcher.json; vault write identity/oidc/role/researcher @researcher.json; rm researcher.json;" -echo - -# --- +echo +echo "enable kv store for aws secrets" +docker exec $vault vault secrets enable -path="aws" -description="AWS-style ID/secret pairs" kv +vault_runner=$(docker ps | grep vault-runner | awk '{print $1}') +docker restart candigv2_vault-runner_1 From 047bc51837d85b2565533daa2d919f956bbf2579 Mon Sep 17 00:00:00 2001 From: Shaikh Rashid Date: Fri, 24 Jun 2022 10:54:15 -0400 Subject: [PATCH 089/236] =?UTF-8?q?bump=20version=20and=20submodule=20for?= =?UTF-8?q?=20federation-service=20and=20data-portal=20=F0=9F=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/env/example.env | 2 +- lib/candig-data-portal/candig-data-portal | 2 +- lib/federation-service/federation_service | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index a1a70c460..1a0bf8c12 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -185,7 +185,7 @@ JUPYTER_ENABLE_LAB=yes JUPYTER_ENABLE_SUDO=yes # federation_service -FEDERATION_VERSION=v0.5.2 +FEDERATION_VERSION=v0.5.3 FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal index 2a180e00f..abd76927b 160000 --- a/lib/candig-data-portal/candig-data-portal +++ b/lib/candig-data-portal/candig-data-portal @@ -1 +1 @@ -Subproject commit 2a180e00f3dd221c4616eaeba4733eacbde2b675 +Subproject commit abd76927be5c198a0cc0c3169a2bed2477b50bf9 diff --git a/lib/federation-service/federation_service b/lib/federation-service/federation_service index 8c35db856..87dfdc557 160000 --- a/lib/federation-service/federation_service +++ b/lib/federation-service/federation_service @@ -1 +1 @@ -Subproject commit 8c35db856e99f4f20c5e7a18efb96731ae93eff2 +Subproject commit 87dfdc5577dc5462ab025565756a23980dc27fa4 From 96b759eda6b166dd9c65d84672572195fc837538 Mon Sep 17 00:00:00 2001 From: Shaikh Rashid Date: Fri, 24 Jun 2022 11:54:27 -0400 Subject: [PATCH 090/236] federation-service needs to be started manually --- Makefile | 1 + etc/env/example.env | 2 +- lib/compose/docker-compose.yml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a551d3522..9809869cf 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ mkdir: mkdir -p $(DIR)/tmp/secrets mkdir -p $(DIR)/tmp/ssl mkdir -p $(DIR)/tmp/{keycloak,tyk,vault} + mkdir -o ${DIR}/tmp/federation #>>> diff --git a/etc/env/example.env b/etc/env/example.env index 1a0bf8c12..58fec6412 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope minio drs-server htsget-server chord-metadata candig-server federation-service candig-data-portal #wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard +CANDIG_MODULES=weavescope minio drs-server htsget-server chord-metadata candig-server candig-data-portal #wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard federation-service CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index a3b56b0f2..bb5e0fdb6 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -45,9 +45,9 @@ secrets: aws-credentials: file: $PWD/tmp/secrets/aws-credentials federation-servers: - file: $PWD/tmp/secrets/federation-servers.json + file: $PWD/tmp/federation/servers.json federation-services: - file: $PWD/tmp/secrets/federation-services.json + file: $PWD/tmp/federation/services.json metadata-app-secret: file: $PWD/tmp/secrets/metadata-app-secret metadata-db-user: From 23889f1908812a42a3d977ce5d7bb0bd702779ed Mon Sep 17 00:00:00 2001 From: Shaikh Rashid Date: Fri, 24 Jun 2022 16:18:07 -0400 Subject: [PATCH 091/236] minor syntax fixes --- Makefile | 2 +- lib/vault/vault_setup.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9809869cf..a15917722 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ mkdir: mkdir -p $(DIR)/tmp/secrets mkdir -p $(DIR)/tmp/ssl mkdir -p $(DIR)/tmp/{keycloak,tyk,vault} - mkdir -o ${DIR}/tmp/federation + mkdir -p ${DIR}/tmp/federation #>>> diff --git a/lib/vault/vault_setup.sh b/lib/vault/vault_setup.sh index 9fbbbb1e4..1e19e7b4f 100755 --- a/lib/vault/vault_setup.sh +++ b/lib/vault/vault_setup.sh @@ -174,4 +174,4 @@ echo "enable kv store for aws secrets" docker exec $vault vault secrets enable -path="aws" -description="AWS-style ID/secret pairs" kv vault_runner=$(docker ps | grep vault-runner | awk '{print $1}') -docker restart candigv2_vault-runner_1 +docker restart $vault_runner From f069d3b8189af454d951e6c6f54c7b552373edb4 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 28 Jun 2022 10:34:47 -0700 Subject: [PATCH 092/236] Small fixes (#151) * v1.2.0 release (#150) * v1.2.1 patch * htsget uses opa_private_url * add external volume for htsget-data * add debug flag * add db_path * pick up paths in htsget * Update htsget_app * Update htsget_app * Update htsget_app * bump htsget version Co-authored-by: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Co-authored-by: Shaikh Rashid --- Makefile | 1 + etc/env/example.env | 3 ++- lib/compose/docker-compose.yml | 2 ++ lib/htsget-server/docker-compose.yml | 6 +++++- lib/htsget-server/htsget_app | 2 +- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a15917722..d9e91a2ab 100644 --- a/Makefile +++ b/Makefile @@ -491,6 +491,7 @@ docker-volumes: docker volume create tyk-redis-data docker volume create vault-data docker volume create opa-data + docker volume create htsget-data #>>> diff --git a/etc/env/example.env b/etc/env/example.env index 58fec6412..5a4c9dd25 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -9,6 +9,7 @@ CANDIG_AUTH_MODULES=keycloak tyk opa vault CANDIG_DOMAIN=docker.localhost CANDIG_AUTH_DOMAIN=docker.localhost CANDIG_SITE_LOCATION=UHN +CANDIG_DEBUG_MODE=1 # miniconda venv # options are [linux, darwin] @@ -131,7 +132,7 @@ CHORD_DRS_VERSION=v0.4.0 CHORD_DRS_PORT=6000 # htsget-app -HTSGET_APP_VERSION=v0.1.6 +HTSGET_APP_VERSION=v0.1.7 HTSGET_PRIVATE_URL=http://htsget-app:3000 HTSGET_APP_PORT=3333 diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index bb5e0fdb6..03723418c 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -40,6 +40,8 @@ volumes: external: true vault-data: external: true + htsget-data: + external: true secrets: aws-credentials: diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index 686d503f5..81fd18d17 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -8,6 +8,8 @@ services: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" image: ${DOCKER_REGISTRY}/htsget-app:${HTSGET_APP_VERSION:-latest} + volumes: + - htsget-data:/data networks: - ${DOCKER_NET} ports: @@ -39,7 +41,9 @@ services: environment: HTSGET_TEST_KEY: "hoodlebug" OPA_SECRET: ${CANDIG_OPA_SECRET} - OPA_URL: ${OPA_URL} + OPA_URL: ${OPA_PRIVATE_URL} CANDIG_AUTH: ${CANDIG_AUTHORIZATION} VAULT_URL: ${VAULT_SERVICE_URL} + DEBUG_MODE: ${CANDIG_DEBUG_MODE} + DB_PATH: /data/files.sql command: ["--host", "0.0.0.0", "--port", "3000"] diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 10d07dece..339e2469d 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 10d07dece60b12c9d0d9bdf590ec729f096e9157 +Subproject commit 339e2469d774b409b03f3aa6a188998b47c5d4be From 4929c1c29dea8b6549c3bd54ec7c280d122576a1 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 28 Jun 2022 17:33:30 -0700 Subject: [PATCH 093/236] in case we're uploading things to our own minio --- lib/candig-server/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/candig-server/docker-compose.yml b/lib/candig-server/docker-compose.yml index 3a64edeb6..de4842d62 100644 --- a/lib/candig-server/docker-compose.yml +++ b/lib/candig-server/docker-compose.yml @@ -9,6 +9,8 @@ services: #candig_version: ${CANDIG_SERVER_VERSION} #candig_ingest: ${CANDIG_INGEST_VERSION} image: ${DOCKER_REGISTRY}/candig-server:${CANDIG_SERVER_VERSION} + volumes: + - minio-data:/data networks: - ${DOCKER_NET} ports: From d3aa19b457f838faeba3fbf3a8c97b3fc1398e64 Mon Sep 17 00:00:00 2001 From: Shaikh Rashid Date: Mon, 4 Jul 2022 15:16:01 -0400 Subject: [PATCH 094/236] bump htsget_app to v0.1.6 --- lib/htsget-server/htsget_app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 339e2469d..10d07dece 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 339e2469d774b409b03f3aa6a188998b47c5d4be +Subproject commit 10d07dece60b12c9d0d9bdf590ec729f096e9157 From 6c478698d4ff1a202a6c7c19772431e3b29ed844 Mon Sep 17 00:00:00 2001 From: Shaikh Rashid Date: Mon, 4 Jul 2022 17:11:11 -0400 Subject: [PATCH 095/236] =?UTF-8?q?candig-data-portal=20v0.1.2=20?= =?UTF-8?q?=F0=9F=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/candig-data-portal/candig-data-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal index abd76927b..1b62785a7 160000 --- a/lib/candig-data-portal/candig-data-portal +++ b/lib/candig-data-portal/candig-data-portal @@ -1 +1 @@ -Subproject commit abd76927be5c198a0cc0c3169a2bed2477b50bf9 +Subproject commit 1b62785a7ae5c9e2ae15a9cb5d735c8b9271f1f0 From d4c7055ac9ec9a8dede0f194349c8cd7fb168727 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 18 Jul 2022 11:08:53 -0700 Subject: [PATCH 096/236] in case we're uploading things to our own minio (#152) --- lib/candig-server/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/candig-server/docker-compose.yml b/lib/candig-server/docker-compose.yml index 3a64edeb6..de4842d62 100644 --- a/lib/candig-server/docker-compose.yml +++ b/lib/candig-server/docker-compose.yml @@ -9,6 +9,8 @@ services: #candig_version: ${CANDIG_SERVER_VERSION} #candig_ingest: ${CANDIG_INGEST_VERSION} image: ${DOCKER_REGISTRY}/candig-server:${CANDIG_SERVER_VERSION} + volumes: + - minio-data:/data networks: - ${DOCKER_NET} ports: From 5db6968cec6ef87af817ecea17321c2db1c0bdc6 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Thu, 4 Aug 2022 10:32:33 -0700 Subject: [PATCH 097/236] OPA secrets as docker secrets (#153) * secrets instead of env vars * Secrets should have fewer unpredictable chars * load secrets * Update opa * Delete test.yml --- Makefile | 5 ++++- etc/env/example.env | 2 -- lib/chord-metadata/docker-compose.yml | 4 ++++ lib/compose/docker-compose.yml | 4 ++++ lib/htsget-server/docker-compose.yml | 3 ++- lib/opa/docker-compose.yml | 10 +++++----- lib/opa/opa | 2 +- 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index d9e91a2ab..dbab80458 100644 --- a/Makefile +++ b/Makefile @@ -466,6 +466,9 @@ docker-secrets: mkdir minio-secrets $(MAKE) secret-tyk-analytics-admin-key $(MAKE) secret-vault-s3-token + + $(MAKE) secret-opa-root-token + $(MAKE) secret-opa-service-token #>>> @@ -654,7 +657,7 @@ push-%: #<<< secret-%: @dd if=/dev/urandom bs=1 count=16 2>/dev/null \ - | base64 | rev | cut -b 2- | rev | tr -d '\n\r+' > $(DIR)/tmp/secrets/$* + | base64 | tr -d '\n\r+' | sed s/[^A-Za-z0-9]/%/g > $(DIR)/tmp/secrets/$* #>>> diff --git a/etc/env/example.env b/etc/env/example.env index 5a4c9dd25..d0f0728c5 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -335,8 +335,6 @@ VAULT_SERVICE_URL=http://vault:8200 OPA_VERSION=latest OPA_PORT=8181 OPA_LOG_LEVEL=debug -CANDIG_OPA_SECRET=my-secret-root-token -CANDIG_OPA_SECRET_SERVICE=my-secret-service-token OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} OPA_PRIVATE_URL=http://opa:8181 OPA_SITE_ADMIN_KEY=site_admin diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 932473128..17451c0d7 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -49,6 +49,10 @@ services: target: metadata_db_user - source: metadata-db-secret target: metadata_db_secret + - source: opa-root-token + target: opa-root-token + - source: opa-service-token + target: opa-service-token entrypoint: - /bin/bash - -c diff --git a/lib/compose/docker-compose.yml b/lib/compose/docker-compose.yml index 03723418c..e625d45b2 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/compose/docker-compose.yml @@ -90,3 +90,7 @@ secrets: file: $PWD/tmp/ssl/cert.pem vault-s3-token: file: $PWD/tmp/secrets/vault-s3-token + opa-root-token: + file: $PWD/tmp/secrets/opa-root-token + opa-service-token: + file: $PWD/tmp/secrets/opa-service-token diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index 81fd18d17..45a0f4904 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -37,10 +37,11 @@ services: target: cert.pem - source: vault-s3-token target: vault-s3-token + - source: opa-service-token + target: opa-service-token logging: *default-logging environment: HTSGET_TEST_KEY: "hoodlebug" - OPA_SECRET: ${CANDIG_OPA_SECRET} OPA_URL: ${OPA_PRIVATE_URL} CANDIG_AUTH: ${CANDIG_AUTHORIZATION} VAULT_URL: ${VAULT_SERVICE_URL} diff --git a/lib/opa/docker-compose.yml b/lib/opa/docker-compose.yml index e4cc4436b..1bd88234e 100644 --- a/lib/opa/docker-compose.yml +++ b/lib/opa/docker-compose.yml @@ -8,7 +8,6 @@ services: alpine_version: "${ALPINE_VERSION}" katsu_url: "${CHORD_METADATA_PUBLIC_URL}" idp: "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" - client_id: "${KEYCLOAK_CLIENT_ID}" opa_site_admin_key: "${OPA_SITE_ADMIN_KEY}" networks: - ${DOCKER_NET} @@ -27,10 +26,13 @@ services: secrets: - source: keycloak-client-local-candig-secret target: idp_client_secret + - source: opa-root-token + target: opa-root-token + - source: opa-service-token + target: opa-service-token environment: IDP_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} - CLIENT_SECRET_ROOT: ${CANDIG_OPA_SECRET} - CLIENT_SECRET_SERVICE: ${CANDIG_OPA_SECRET_SERVICE} + OPA_SITE_ADMIN_KEY: ${OPA_SITE_ADMIN_KEY} OPA_URL: ${OPA_URL} IDP: "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}" KATSU_URL: "${CHORD_METADATA_PUBLIC_URL}" @@ -42,8 +44,6 @@ services: volumes: - opa-data:/app environment: - CLIENT_SECRET_ROOT: ${CANDIG_OPA_SECRET} - CLIENT_SECRET_SERVICE: ${CANDIG_OPA_SECRET_SERVICE} IDP: ${KEYCLOAK_REALM_URL} command: - "run" diff --git a/lib/opa/opa b/lib/opa/opa index daa139051..832948cff 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit daa139051be68094d584d8f6f7d6aecf87f5083f +Subproject commit 832948cffde2be40442fe4502eb2176f2bb4d66b From d639186a827f00e33372b60477ddec6c49ccad1e Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 19 Aug 2022 12:33:38 -0700 Subject: [PATCH 098/236] Add switches to Makefile to download M1 binaries (#154) * add arm64mac flag * add switches for arm64 macs * remove kubernetes targets * remove tabs * try again * fix target for traefik * update traefik version in example.env for M1 support * add sed backup suffix for cross-platform usage; fix miniconda copy-paste error * add note about location of example env file * better separate instructions for docker-compose and docker swarm Co-authored-by: Karen Cranston --- Makefile | 67 +++++++++++++++--------------------------- README.md | 5 ++-- docs/install-docker.md | 60 +++++++++++++++++++++---------------- etc/env/example.env | 6 ++-- 4 files changed, 65 insertions(+), 73 deletions(-) diff --git a/Makefile b/Makefile index dbab80458..79d648bd4 100644 --- a/Makefile +++ b/Makefile @@ -43,8 +43,7 @@ mkdir: #<<< .PHONY: bin-all -bin-all: bin-conda bin-docker-machine bin-kompose bin-kubectl \ - bin-minikube bin-minio bin-traefik bin-prometheus +bin-all: bin-conda bin-docker-machine bin-minio bin-traefik bin-prometheus #>>> @@ -61,6 +60,10 @@ endif ifeq ($(VENV_OS), darwin) curl -Lo $(DIR)/bin/miniconda_install.sh \ https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh +endif +ifeq ($(VENV_OS), arm64mac) + curl -Lo $(DIR)/bin/miniconda_install.sh \ + https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh endif bash $(DIR)/bin/miniconda_install.sh -f -b -u -p $(DIR)/bin/miniconda3 # init is needed to create bash aliases for conda but it won't work @@ -82,45 +85,6 @@ bin-docker-machine: mkdir echo " finished bin-docker-machine" >> $(LOGFILE) -#>>> -# download kompose (for kubernetes deployment) -# make bin-kompose - -#<<< -bin-kompose: mkdir - echo " started bin-kompose" >> $(LOGFILE) - curl -Lo $(DIR)/bin/kompose \ - https://github.com/kubernetes/kompose/releases/download/v1.21.0/kompose-$(VENV_OS)-amd64 - chmod 755 $(DIR)/bin/kompose - echo " finished bin-kompose" >> $(LOGFILE) - - -#>>> -# download latest kubectl (for kubernetes deployment) -# make bin-kubectl - -#<<< -bin-kubectl: mkdir - echo " started bin-kubectl" >> $(LOGFILE) - curl -Lo $(DIR)/bin/kubectl \ - https://storage.googleapis.com/kubernetes-release/release/v1.18.6/bin/$(VENV_OS)/amd64/kubectl - chmod 755 $(DIR)/bin/kubectl - echo " finished bin-kubectl" >> $(LOGFILE) - - -#>>> -# download latest minikube binary from Google repo -# make bin-minikube - -#<<< -bin-minikube: mkdir - echo " started bin-minikube" >> $(LOGFILE) - curl -Lo $(DIR)/bin/minikube \ - https://storage.googleapis.com/minikube/releases/latest/minikube-$(VENV_OS)-amd64 - chmod 755 $(DIR)/bin/minikube - echo " finished bin-minikube" >> $(LOGFILE) - - #>>> # download latest minio server/client from Minio repo # make bin-minio @@ -128,10 +92,17 @@ bin-minikube: mkdir #<<< bin-minio: mkdir echo " started bin-minio" >> $(LOGFILE) +ifeq ($(VENV_OS), arm64mac) + curl -Lo $(DIR)/bin/minio \ + https://dl.minio.io/server/minio/release/darwin-arm64/minio + curl -Lo $(DIR)/bin/mc \ + https://dl.minio.io/client/mc/release/darwin-arm64/mc +else curl -Lo $(DIR)/bin/minio \ https://dl.minio.io/server/minio/release/$(VENV_OS)-amd64/minio curl -Lo $(DIR)/bin/mc \ https://dl.minio.io/client/mc/release/$(VENV_OS)-amd64/mc +endif chmod 755 $(DIR)/bin/minio chmod 755 $(DIR)/bin/mc echo " finished bin-minio" >> $(LOGFILE) @@ -145,8 +116,13 @@ bin-minio: mkdir bin-prometheus: mkdir echo " started bin-prometheus" >> $(LOGFILE) mkdir -p $(DIR)/bin/prometheus +ifeq ($(VENV_OS), arm64mac) + curl -Lo $(DIR)/bin/prometheus/prometheus.tar.gz \ + https://github.com/prometheus/prometheus/releases/download/v$(PROMETHEUS_VERSION)/prometheus-$(PROMETHEUS_VERSION).darwin-arm64.tar.gz +else curl -Lo $(DIR)/bin/prometheus/prometheus.tar.gz \ https://github.com/prometheus/prometheus/releases/download/v$(PROMETHEUS_VERSION)/prometheus-$(PROMETHEUS_VERSION).$(VENV_OS)-amd64.tar.gz +endif tar --strip-components=1 -zxvf $(DIR)/bin/prometheus/prometheus.tar.gz -C $(DIR)/bin/prometheus chmod 755 $(DIR)/bin/prometheus/prometheus echo " finished bin-prometheus" >> $(LOGFILE) @@ -159,8 +135,13 @@ bin-prometheus: mkdir #<<< bin-traefik: mkdir echo " started bin-traefik" >> $(LOGFILE) +ifeq ($(VENV_OS), arm64mac) + curl -Lo $(DIR)/bin/traefik.tar.gz \ + https://github.com/traefik/traefik/releases/download/v$(TRAEFIK_VERSION)/traefik_v$(TRAEFIK_VERSION)_darwin_arm64.tar.gz +else curl -Lo $(DIR)/bin/traefik.tar.gz \ https://github.com/traefik/traefik/releases/download/v$(TRAEFIK_VERSION)/traefik_v$(TRAEFIK_VERSION)_$(VENV_OS)_amd64.tar.gz +endif tar -xvzf $(DIR)/bin/traefik.tar.gz -C bin/ chmod 755 $(DIR)/bin/traefik echo " finished bin-traefik" >> $(LOGFILE) @@ -680,7 +661,7 @@ ssl-cert: -subj '/C=CA/ST=ON/L=Toronto/O=CanDIG/CN=CanDIG Self-Signed Cert' cp $(DIR)/etc/ssl/site.cnf $(DIR)/tmp/ssl/site.cnf - sed -i s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/tmp/ssl/site.cnf + sed -i.bak s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/tmp/ssl/site.cnf openssl x509 -req -days 750 -in $(DIR)/tmp/ssl/selfsigned-site.csr -sha256 \ -CA $(DIR)/tmp/ssl/selfsigned-root-ca.crt \ -CAkey $(DIR)/tmp/ssl/selfsigned-root-ca.key \ @@ -688,7 +669,7 @@ ssl-cert: -extfile $(DIR)/tmp/ssl/site.cnf -extensions server cp $(DIR)/etc/ssl/alt_names.txt $(DIR)/tmp/ssl/alt_names.txt - sed -i s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/tmp/ssl/alt_names.txt + sed -i.bak s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/tmp/ssl/alt_names.txt openssl x509 -req -days 365 -in $(DIR)/tmp/ssl/selfsigned-root-ca.csr \ -sha256 \ -signkey $(DIR)/tmp/ssl/selfsigned-root-ca.key \ diff --git a/README.md b/README.md index 8953049ae..9a6faed7f 100644 --- a/README.md +++ b/README.md @@ -111,11 +111,12 @@ CanDIGv2/ ## `.env` Environment File -The `.env` file in the project root directory contains a set of global variables that are used as reference to -the various parameters, plugins, and config options that operators can modify for testing purposes. +You need an `.env` file in the project root directory, which contains a set of global variables that are used as reference to +the various parameters, plugins, and config options that operators can modify for testing purposes. There is an example `.env` file in `etc/example.env`. Some of the functionality that is controlled through `.env` are: +* operating system flags * change docker network, driver, and swarm host * modify ports, protocols, and plugins for various services * version control and app pinning diff --git a/docs/install-docker.md b/docs/install-docker.md index c4164ffaa..c9011455a 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -108,57 +108,58 @@ make bin-all make init-conda ``` -## Create CanDIGv2 Development VM -Using the provided steps will help to create a `docker-machine` cluster on VirtualBox. The `make` CLI can also be used to provision and connect a multi-vm Swarm cluster. Users are encouraged to use this docker environment for CanDIGv2 development as it provides an isolated domain from the host environment, increasing security and reducing conflicts with host processes. Modify the `MINIKUBE_*` options in `.env`, then launch a single-node or multi-node `docker-machine` with `make machine-$vm_name`, where `$vm_name` is a unique vm name. - -To build a development swarm cluster run the following: - -* create a swarm manager with `make machine-manager`, additional nodes with `make machine-manager2`... -* create a swarm worker with `make machine-worker`, additional nodes with `make machine-worker2`... +## Choose Docker Deployment Strategy -To switch your local docker-client to use `docker-machine`, run `eval $(bin/docker-machine env manager)`. Add this line into `bashrc` with `bin/docker-machine env manager >> $HOME/.bashrc` in order to set `docker-machine` as the default `$DOCKER_HOST` for all shells. +We provide instructions below for two different docker deployment strategies. Option 1 uses `docker-compose` to deploy each module. Option 2 builds a Docker Swarm cluster using `docker-machine`. We use Option 2 for production, but Option 1 is simpler for local dev installation. -You can move on to the initialize instructions for Docker. +### Option 1: Deploy CanDIGv2 Services with Compose -## Initialize CanDIGv2 (Docker) - -The following commands will initialize CanDIGv2 and set up docker networks, volumes, configs, secrets, and perform other miscellaneous actions needed before deploying a CanDIGv2 stack. Only perform these actions once as it will override any previous configurations and secrets. Once completed, you can deploy a Compose or Swarm stack. +The `init-docker` command will initialize CanDIGv2 and set up docker networks, volumes, configs, secrets, and perform other miscellaneous actions needed before deploying a CanDIGv2 stack. Running `init-docker` will override any previous configurations and secrets. ```bash # initialize docker environment make init-docker -``` -## Deploy CanDIGv2 Services (Compose) - -```bash -# create images (optional) +# (optional) create images make images -# pull latest CanDIGv2 images (instead of make images) +# pull latest CanDIGv2 images (if you didn't create images locally) make docker-pull -# deploy stack (if using docker-compose environment) +# deploy stack make compose make init-authx # TODO: post deploy auth configuration -# push updated images to $DOCKER_REGISTRY (optional) +# (optional) push updated images to $DOCKER_REGISTRY docker login make docker-push ``` -## Update hosts +## Option 2: Deploy CanDIGv2 using Docker Swarm -Get your local IP address and edit your /etc/hosts file to add: +### Create CanDIGv2 Development VM + +Using the provided steps will help to create a `docker-machine` cluster on VirtualBox. The `make` CLI can also be used to provision and connect a multi-vm Swarm cluster. Users are encouraged to use this docker environment for CanDIGv2 development as it provides an isolated domain from the host environment, increasing security and reducing conflicts with host processes. Modify the `MINIKUBE_*` options in `.env`, then launch a single-node or multi-node `docker-machine` with `make machine-$vm_name`, where `$vm_name` is a unique vm name. + +To build a development swarm cluster run the following: + +* create a swarm manager with `make machine-manager`, additional nodes with `make machine-manager2`... +* create a swarm worker with `make machine-worker`, additional nodes with `make machine-worker2`... + +To switch your local docker-client to use `docker-machine`, run `eval $(bin/docker-machine env manager)`. Add this line into `bashrc` with `bin/docker-machine env manager >> $HOME/.bashrc` in order to set `docker-machine` as the default `$DOCKER_HOST` for all shells. + +### Initialize CanDIGv2 (Docker) + +The following commands will initialize CanDIGv2 and set up docker networks, volumes, configs, secrets, and perform other miscellaneous actions needed before deploying a CanDIGv2 stack. Only perform these actions once as it will override any previous configurations and secrets. Once completed, you can deploy a Compose or Swarm stack. ```bash - docker.localhost - auth.docker.localhost +# initialize docker environment +make init-docker ``` -## Deploy CanDIGv2 Services (Swarm) +### Deploy using Swarm > Note: swarm deployment requires minimum 2 nodes connected (1 manager, 1 worker) 1. Create initial manager node @@ -188,6 +189,15 @@ docker node ls make stack ``` +## Update hosts + +Get your local IP address and edit your /etc/hosts file to add: + +```bash + docker.localhost + auth.docker.localhost +``` + ## Cleanup CanDIGv2 Compose/Swarm Environment Use the following steps to clean up running CanDIGv2 services in a docker-compose configuration. Note that these steps are destructive and will remove **ALL** containers, secrets, volumes, networks, certs, and images. If you are using docker in a shared environment (i.e. with other non-CanDIGv2 containers running) please consider running the cleanup steps manually instead. diff --git a/etc/env/example.env b/etc/env/example.env index d0f0728c5..077d3da15 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -12,7 +12,7 @@ CANDIG_SITE_LOCATION=UHN CANDIG_DEBUG_MODE=1 # miniconda venv -# options are [linux, darwin] +# options are [linux, darwin, arm64mac] VENV_OS=linux VENV_NAME=candig VENV_PYTHON=3.7 @@ -92,8 +92,8 @@ CONSUL_DNS_PORT=8600 CONSUL_LAN_PORT=8301 CONSUL_WAN_PORT=8302 -# traefik controller -TRAEFIK_VERSION=2.5.0 +# traefik controller, need at least 2.5.1 for mac M1 support +TRAEFIK_VERSION=2.5.1 # enable swarm operations # options are [true, false] TRAEFIK_SWARM=false From f9aaf6d0bd5fee82eed5a72fd1f3a7844be452a9 Mon Sep 17 00:00:00 2001 From: Son Chau Date: Fri, 19 Aug 2022 12:36:40 -0700 Subject: [PATCH 099/236] Update python and pip version to Apple Silicon (#155) * add arm64mac flag * add switches for arm64 macs * remove kubernetes targets * remove tabs * try again * update python and pip for apple silicon - Python bump from 3.7 to 3.9 - Pip bump from 20.2.4 to 21.2.2 This should resolve the error packages not available in conda channels. Co-authored-by: Daisie Huang Co-authored-by: Karen Cranston --- etc/env/example.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 077d3da15..e20378c12 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -15,8 +15,8 @@ CANDIG_DEBUG_MODE=1 # options are [linux, darwin, arm64mac] VENV_OS=linux VENV_NAME=candig -VENV_PYTHON=3.7 -VENV_PIP=20.2.4 +VENV_PYTHON=3.9 +VENV_PIP=21.2.2 # docker # options are [bridge, bridge-net, ingress, traefik-net] From 26ef9493caf0e2ccecb254bd466f16532953a63b Mon Sep 17 00:00:00 2001 From: Shaikh Rashid Date: Wed, 7 Sep 2022 12:43:51 -0400 Subject: [PATCH 100/236] =?UTF-8?q?quick=20patch=20for=20federation=5Fserv?= =?UTF-8?q?ice=20=F0=9F=A9=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/env/example.env | 2 +- lib/federation-service/federation_service | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index e20378c12..67a5e25f2 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -186,7 +186,7 @@ JUPYTER_ENABLE_LAB=yes JUPYTER_ENABLE_SUDO=yes # federation_service -FEDERATION_VERSION=v0.5.3 +FEDERATION_VERSION=v0.5.4 FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 diff --git a/lib/federation-service/federation_service b/lib/federation-service/federation_service index 87dfdc557..d3ba8e14b 160000 --- a/lib/federation-service/federation_service +++ b/lib/federation-service/federation_service @@ -1 +1 @@ -Subproject commit 87dfdc5577dc5462ab025565756a23980dc27fa4 +Subproject commit d3ba8e14b268284340b2a00194a33b776d3c9a3e From 5277b120420f855f9286239918601a1b2ddc7cd6 Mon Sep 17 00:00:00 2001 From: Shaikh Rashid Date: Wed, 7 Sep 2022 22:00:28 -0400 Subject: [PATCH 101/236] bump candig-data-portalto v0.1.3 --- etc/env/example.env | 2 +- lib/candig-data-portal/candig-data-portal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 67a5e25f2..1d0119571 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -352,7 +352,7 @@ CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search # candig-data-server (previously mcode) -CANDIG_DATA_PORTAL_VERSION=v0.1.1 +CANDIG_DATA_PORTAL_VERSION=v0.1.3 CANDIG_DATA_PORTAL_PORT=2543 CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT}/data-portal CANDIG_DATA_PORTAL_PRIVATE_URL=http://candig-data-portal:3000 diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal index 1b62785a7..19730e9f9 160000 --- a/lib/candig-data-portal/candig-data-portal +++ b/lib/candig-data-portal/candig-data-portal @@ -1 +1 @@ -Subproject commit 1b62785a7ae5c9e2ae15a9cb5d735c8b9271f1f0 +Subproject commit 19730e9f9c10db33046807d1bee10c830f5a81a3 From bc67cead65093fe6fc0afe2286639443c250c31f Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 12 Sep 2022 17:25:10 -0700 Subject: [PATCH 102/236] Update submodules (#158) * update submodules * Update example.env --- etc/env/example.env | 6 +++--- lib/chord-metadata/chord_metadata_service | 2 +- lib/htsget-server/htsget_app | 2 +- lib/opa/opa | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 1d0119571..d72fc6688 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -132,7 +132,7 @@ CHORD_DRS_VERSION=v0.4.0 CHORD_DRS_PORT=6000 # htsget-app -HTSGET_APP_VERSION=v0.1.7 +HTSGET_APP_VERSION=v0.1.8 HTSGET_PRIVATE_URL=http://htsget-app:3000 HTSGET_APP_PORT=3333 @@ -191,7 +191,7 @@ FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 # chord metadata service -CHORD_METADATA_VERSION=v1.4.3 +CHORD_METADATA_VERSION=v1.4.4 CHORD_METADATA_PORT=8008 CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false @@ -332,7 +332,7 @@ VAULT_SERVICE_URL=http://vault:8200 # OPA -OPA_VERSION=latest +OPA_VERSION=v1.1.1 OPA_PORT=8181 OPA_LOG_LEVEL=debug OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index adaf6e0cb..afac44d6c 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit adaf6e0cb210aa0a9e979955863f1d4774a5734c +Subproject commit afac44d6c077c5ef2fac5b942558ed9154395c48 diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 10d07dece..012fe3670 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 10d07dece60b12c9d0d9bdf590ec729f096e9157 +Subproject commit 012fe3670aed98d35377ae71a372d87da4842341 diff --git a/lib/opa/opa b/lib/opa/opa index 832948cff..cc16c3dff 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 832948cffde2be40442fe4502eb2176f2bb4d66b +Subproject commit cc16c3dff885198373be3d02b196dc789fc20b9e From a526eb171166f1962b4e8f17d50e90c0c509cdb5 Mon Sep 17 00:00:00 2001 From: Son Chau Date: Fri, 23 Sep 2022 11:55:39 -0700 Subject: [PATCH 103/236] fix: add compatibility (#160) For Docker Desktop 1.x use _ but 2.x use - when naming. This will option retain the compose compatibility --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 79d648bd4..a10bf645e 100644 --- a/Makefile +++ b/Makefile @@ -377,7 +377,7 @@ compose-%: echo " started compose-$*" >> $(LOGFILE) cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ $(DIR)/lib/$*/docker-compose.yml \ - | docker-compose -f - up -d + | docker-compose --compatibility -f - up -d echo " finished compose-$*" >> $(LOGFILE) From eee3579a952a6630da4ebb24095057acd3f15095 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 30 Sep 2022 16:35:58 -0700 Subject: [PATCH 104/236] pass container name into chord_metadata (#162) --- etc/env/example.env | 3 ++- lib/chord-metadata/docker-compose.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/etc/env/example.env b/etc/env/example.env index d72fc6688..b5b737460 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -198,7 +198,8 @@ CHORD_METADATA_AUTH=false CHORD_METADATA_DEBUG=false CHORD_METADATA_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_KATSU_API_LISTEN_PATH} CHORD_METADATA_INGEST_URL=http://${CANDIG_DOMAIN}:${CHORD_METADATA_PORT} -CHORD_METADATA_PRIVATE_URL=http://chord-metadata:8000 +CHORD_METADATA_CONTAINER=chord-metadata +CHORD_METADATA_PRIVATE_URL=http://${CHORD_METADATA_CONTAINER}:8000 # candig-specific katsu INSIDE_CANDIG=true diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 17451c0d7..07be09dc2 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -42,6 +42,7 @@ services: - CACHE_TIME=${CACHE_TIME} - CANDIG_OPA_SECRET=${CANDIG_OPA_SECRET} - CANDIG_OPA_SITE_ADMIN_KEY=${OPA_SITE_ADMIN_KEY} + - HOST_CONTAINER_NAME=${CHORD_METADATA_CONTAINER} secrets: - source: metadata-app-secret target: metadata_app_secret From 5f184329c2cdccecfae7c45f22d5af83d4189039 Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Thu, 13 Oct 2022 12:59:27 -0400 Subject: [PATCH 105/236] Feature/federation behind tyk api (#163) * env, templates and scripts update * patched api federation strip listen path * env, templates and scripts update * patched api federation strip listen path * reverted opa and vault command changes * update branch chord-metadata service * no symbols at all in random secrets (#164) Co-authored-by: Daisie Huang Co-authored-by: Brennan Brouillette Co-authored-by: Shaikh Rashid Co-authored-by: Daisie Huang --- Makefile | 2 +- etc/env/example.env | 7 + lib/chord-metadata/chord_metadata_service | 2 +- .../api_federation.json.tpl | 147 ++++++++++++++++++ .../configuration_templates/policies.json.tpl | 8 + lib/tyk/tyk_setup.sh | 3 + 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 lib/tyk/configuration_templates/api_federation.json.tpl diff --git a/Makefile b/Makefile index a10bf645e..99906be43 100644 --- a/Makefile +++ b/Makefile @@ -638,7 +638,7 @@ push-%: #<<< secret-%: @dd if=/dev/urandom bs=1 count=16 2>/dev/null \ - | base64 | tr -d '\n\r+' | sed s/[^A-Za-z0-9]/%/g > $(DIR)/tmp/secrets/$* + | base64 | tr -d '\n\r+' | sed s/[^A-Za-z0-9]//g > $(DIR)/tmp/secrets/$* #>>> diff --git a/etc/env/example.env b/etc/env/example.env index b5b737460..8d68c5cdc 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -189,6 +189,7 @@ JUPYTER_ENABLE_SUDO=yes FEDERATION_VERSION=v0.5.4 FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 +FEDERATION_SERVICE_URL=http://federation-service:${FEDERATION_PORT} # chord metadata service CHORD_METADATA_VERSION=v1.4.4 @@ -312,6 +313,12 @@ TYK_VAULT_API_SLUG=vault TYK_VAULT_API_TARGET=${VAULT_SERVICE_URL} TYK_VAULT_API_LISTEN_PATH=vault +## api - federation +TYK_FEDERATION_API_ID=91 +TYK_FEDERATION_API_SLUG=federation +TYK_FEDERATION_API_TARGET=${FEDERATION_SERVICE_URL} +TYK_FEDERATION_API_LISTEN_PATH=federation + ## Extra APIs can be added here ## api - example #TYK_EXAMPLE_API_ID=666 diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index afac44d6c..e3c878e6b 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit afac44d6c077c5ef2fac5b942558ed9154395c48 +Subproject commit e3c878e6bb64f222ee0b369d64740dbfc61b046f diff --git a/lib/tyk/configuration_templates/api_federation.json.tpl b/lib/tyk/configuration_templates/api_federation.json.tpl new file mode 100644 index 000000000..46a987261 --- /dev/null +++ b/lib/tyk/configuration_templates/api_federation.json.tpl @@ -0,0 +1,147 @@ +{ + "api_id": "${TYK_FEDERATION_API_ID}", + "name": "${TYK_FEDERATION_API_SLUG}", + "use_openid": true, + "active": true, + "slug": "${TYK_FEDERATION_API_SLUG}", + + "enable_signature_checking": false, + + "jwt_issued_at_validation_skew": 0, + "jwt_expires_at_validation_skew": 0, + "upstream_certificates": {}, + "use_keyless": false, + "enable_coprocess_auth": false, + "base_identity_provided_by": "", + + "proxy": { + "target_url": "${TYK_FEDERATION_API_TARGET}", + "strip_listen_path": false, + "disable_strip_slash": false, + "listen_path": "/${TYK_FEDERATION_API_LISTEN_PATH}", + "transport": { + "ssl_insecure_skip_verify": false, + "ssl_ciphers": [], + "ssl_min_version": 0, + "proxy_url": "" + }, + "target_list": [], + "preserve_host_header": false + }, + + "version_data": { + "not_versioned": true, + "versions": { + "Default": { + "name": "Default", + "use_extended_paths": true + } + }, + "extended_paths": { + "ignored": [ + { + "path": "${KEYCLOAK_LOGIN_REDIRECT_PATH}", + "method_actions": { + "GET": { + "action": "no_action", + "code": 200, + "headers": {} + } + } + } + ] + } + }, + "custom_middleware": { + "pre": [ + { + "name": "backendAuthMiddleware", + "path": "/opt/tyk-gateway/middleware/backendAuthMiddleware.js", + "require_session": false + } + ], + "post": [], + "id_extractor": { + "extract_with": "", + "extract_from": "", + "extractor_config": {} + }, + "driver": "", + "auth_check": { + "path": "", + "require_session": false, + "name": "" + }, + "post_key_auth": [], + "response": [] + }, + + "config_data": { + "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", + "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", + "VAULT_ROLE":"researcher" + }, + "openid_options": { + "segregate_by_client": false, + "providers": [ + { + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", + "client_ids": { + "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" + } + } + ] + }, + + + "definition": { + "location": "header", + "key": "x-api-version" + }, + + + "internal": false, + "jwt_skip_kid": false, + "enable_batch_request_support": false, + "response_processors": [], + "use_mutual_tls_auth": false, + "basic_auth": { + "disable_caching": false, + "cache_ttl": 0, + "extract_from_body": false, + "body_user_regexp": "", + "body_password_regexp": "" + }, + "use_standard_auth": false, + "session_lifetime": 0, + "use_oauth2": false, + "jwt_source": "", + "jwt_signing_method": "", + "jwt_not_before_validation_skew": 0, + "jwt_identity_base_field": "", + + "session_provider": { + "name": "", + "storage_engine": "", + "meta": {} + }, + + "auth": { + "use_param": false, + "param_name": "", + "use_cookie": false, + "cookie_name": "", + "auth_header_name": "", + "use_certificate": false, + "validate_signature": false, + "signature": { + "algorithm": "", + "header": "", + "secret": "", + "allowed_clock_skew": 0, + "error_code": 0, + "error_message": "" + } + } +} diff --git a/lib/tyk/configuration_templates/policies.json.tpl b/lib/tyk/configuration_templates/policies.json.tpl index cd224acba..fcf2b3c43 100644 --- a/lib/tyk/configuration_templates/policies.json.tpl +++ b/lib/tyk/configuration_templates/policies.json.tpl @@ -56,6 +56,14 @@ "versions": [ "Default" ] + }, + "${TYK_FEDERATION_API_ID}": { + "allowed_urls": [], + "api_id": "${TYK_FEDERATION_API_ID}", + "api_name": "${TYK_FEDERATION_API_SLUG}", + "versions": [ + "Default" + ] } }, "active": true, diff --git a/lib/tyk/tyk_setup.sh b/lib/tyk/tyk_setup.sh index 61afb75ae..97b4556bb 100755 --- a/lib/tyk/tyk_setup.sh +++ b/lib/tyk/tyk_setup.sh @@ -89,6 +89,9 @@ envsubst < ${PWD}/lib/tyk/configuration_templates/api_opa.json.tpl > ${CONFIG_DI echo "Working on api_vault.json" envsubst < ${PWD}/lib/tyk/configuration_templates/api_vault.json.tpl > ${CONFIG_DIR}/apps/api_vault.json +echo "Working on api_federation.json" | tee -a $LOGFILE +envsubst < ${PWD}/lib/tyk/configuration_templates/api_federation.json.tpl > ${CONFIG_DIR}/apps/api_federation.json + # Extra APIs can be added here #echo "Working on api_example.json" #envsubst < ${PWD}/lib/tyk/configuration_templates/api_example.json.tpl > ${CONFIG_DIR}/apps/api_example.json From 844a274aa643f80738abe77f442f645c68169bbc Mon Sep 17 00:00:00 2001 From: Son Chau Date: Wed, 19 Oct 2022 08:29:05 -0700 Subject: [PATCH 106/236] Sonchau/install docker m1 (#161) * docs: update docker for m1 * docs: wording docs: typo and styling docs: wording docs: wording * docs: update title * fix: update insall-docker.md * install-docker docs patch - c3g arm64-keycloak image Co-authored-by: Brennan Brouillette --- docs/install-docker.md | 161 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 10 deletions(-) diff --git a/docs/install-docker.md b/docs/install-docker.md index c9011455a..df241c681 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -1,6 +1,6 @@ # CanDIGv2 Install Guide -- - - +--- ## Install OS Dependencies @@ -108,14 +108,13 @@ make bin-all make init-conda ``` - ## Choose Docker Deployment Strategy -We provide instructions below for two different docker deployment strategies. Option 1 uses `docker-compose` to deploy each module. Option 2 builds a Docker Swarm cluster using `docker-machine`. We use Option 2 for production, but Option 1 is simpler for local dev installation. +We provide instructions below for two different docker deployment strategies. Option 1 uses `docker-compose` to deploy each module. Option 2 builds a Docker Swarm cluster using `docker-machine`. We use Option 2 for production, but Option 1 is simpler for local dev installation. ### Option 1: Deploy CanDIGv2 Services with Compose -The `init-docker` command will initialize CanDIGv2 and set up docker networks, volumes, configs, secrets, and perform other miscellaneous actions needed before deploying a CanDIGv2 stack. Running `init-docker` will override any previous configurations and secrets. +The `init-docker` command will initialize CanDIGv2 and set up docker networks, volumes, configs, secrets, and perform other miscellaneous actions needed before deploying a CanDIGv2 stack. Running `init-docker` will override any previous configurations and secrets. ```bash # initialize docker environment @@ -127,17 +126,17 @@ make images # pull latest CanDIGv2 images (if you didn't create images locally) make docker-pull -# deploy stack +# deploy stack make compose make init-authx # TODO: post deploy auth configuration -# (optional) push updated images to $DOCKER_REGISTRY +# (optional) push updated images to $DOCKER_REGISTRY docker login make docker-push ``` -## Option 2: Deploy CanDIGv2 using Docker Swarm +## Option 2: Deploy CanDIGv2 using Docker Swarm ### Create CanDIGv2 Development VM @@ -145,10 +144,10 @@ Using the provided steps will help to create a `docker-machine` cluster on Virtu To build a development swarm cluster run the following: -* create a swarm manager with `make machine-manager`, additional nodes with `make machine-manager2`... -* create a swarm worker with `make machine-worker`, additional nodes with `make machine-worker2`... +- create a swarm manager with `make machine-manager`, additional nodes with `make machine-manager2`... +- create a swarm worker with `make machine-worker`, additional nodes with `make machine-worker2`... -To switch your local docker-client to use `docker-machine`, run `eval $(bin/docker-machine env manager)`. Add this line into `bashrc` with `bin/docker-machine env manager >> $HOME/.bashrc` in order to set `docker-machine` as the default `$DOCKER_HOST` for all shells. +To switch your local docker-client to use `docker-machine`, run `eval $(bin/docker-machine env manager)`. Add this line into `bashrc` with `bin/docker-machine env manager >> $HOME/.bashrc` in order to set `docker-machine` as the default `$DOCKER_HOST` for all shells. ### Initialize CanDIGv2 (Docker) @@ -160,6 +159,7 @@ make init-docker ``` ### Deploy using Swarm + > Note: swarm deployment requires minimum 2 nodes connected (1 manager, 1 worker) 1. Create initial manager node @@ -241,3 +241,144 @@ make clean-conda make clean-bin ``` +# Mac Apple Silicon Installation + +### 1) Step1: Install OS Dependencies + +Mac users can get [docker desktop](https://docs.docker.com/desktop/mac/apple-silicon/). Also installed rosetta and used Docker Compose V2 as suggested at the moment. + +- **Optional**: these installations are not mentioned but might be needed: + - Install [brew](https://brew.sh/) + - Install md5sha1sum (`brew install md5sha1sum`) + - Install PostgreSQL (`brew install postgresql`) + +### Step 2: Initialize CanDIGv2 Repo + +```bash +# 1. initialize repo and submodules +git clone -b develop https://github.com/CanDIG/CanDIGv2.git +cd CanDIGv2 +git submodule update --init --recursive + +# 2. copy and edit .env with your site's local configuration +cp -i etc/env/example.env .env +``` + +- Edit the .env file: + +```bash +# options are [, , host.docker.internal, docker.localhost] +CANDIG_DOMAIN=host.docker.internal +CANDIG_AUTH_DOMAIN=host.docker.internal +... +# options are [linux, darwin, arm64mac] +VENV_OS=arm64mac +VENV_NAME=candig +``` + +- Continue to run `make` + +```bash +# 3. fetch binaries and initialize candig virtualenv +make bin-all +make init-conda +``` + +- To activate conda env, do the following: + +```bash +conda env list +# Copy the whole path that contains `/envs/candig` +conda activate {path_to_folder}/CanDIGv2/bin/miniconda3/envs/candig +``` + +- Note: The reason we cannot activate it automatically on Mac was described in this [post](https://stackoverflow.com/questions/57527131/conda-environment-has-no-name-visible-in-conda-env-list-how-do-i-activate-it-a). If `conda env` is not in the root folder, it won't have a name. + +### Step 3: Initialize CanDIGv2 (Docker) + +- Make sure you are in `candig` virtual environment (activate it in previous step) + +```bash +make init-docker +``` + +### Step 4: Deploy CanDIGv2 Services (Compose) + +```bash +make compose +``` + +### Step 5: Update hosts + +- Get the local IP address in the terminal: + +```bash +dig -4 TXT +short o-o.myaddr.l.google.com @ns1.google.com +``` + +- Then edit your /etc/hosts file: + +```bash +sudo nano /etc/hosts +``` + +- Add the IP address to the end of the file so it look like this: + +```bash +# Other settings +127.0.0.1 host.docker.internal +``` + +### Step 6: Create Auth Stack + +In the .env, comment out all the `WES_OPT+=…` (We don't use it right now) + +```bash +# WES_OPT=--opt=extra=--batchSystem=Mesos +... +# WES_OPT+=--opt=extra=--metrics +``` + +The old keycloak image (15.0.0) is not compatible with M1, so we need to upgrade it. + +Go to `lib/keycloak/docker-compose.yml` and replace the `- BASE_IMAGE=candig/keycloak:${KEYCLOAK_VERSION}` with one of the following: + +```bash +- BASE_IMAGE=mihaibob/keycloak:18.0.2-legacy +# or +- BASE_IMAGE=quay.io/c3genomics/keycloak:16.1.1.arm64 # (an alternative built on an M1, for an M1) +``` + +(I found it on StackOverflow, and it worked, but @shaikh-rashid might want to look for an "official" one or build a candig version for us) + +Note: for local development, add extra_hosts: + +```bash + networks: + - ${DOCKER_NET} + extra_hosts: + - "host.docker.internal:127.0.0.1" + # comment this out too + # volumes: + # - keycloak-data:/opt/jboss/keycloak/standalone +``` + +Then run `make`: + +```bash +make init-authx +``` + +If you got this error: + +```bash +Getting keycloak token +Traceback (most recent call last): + File "", line 1, in +KeyError: 'access_token' +make: *** [init-authx] Error 1 +``` + +Then try to replace all the `keycloak` passwords in `tmp/secrets` with something simple like `thisisasupersecretpassword`, basically no special chars. + +Try `make clean-authx` and `make init-authx` and it should worked 🎉 From a65710f4c5ee0124ef9a4fde19d69363c2d0f709 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 19 Oct 2022 10:45:25 -0700 Subject: [PATCH 107/236] pass in env var for HTSGET_URL (#166) * pass in env var for HTSGET_URL * actually, igv is going to need public urls --- lib/htsget-server/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index 45a0f4904..ea31b65d3 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -42,6 +42,7 @@ services: logging: *default-logging environment: HTSGET_TEST_KEY: "hoodlebug" + HTSGET_URL: ${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} OPA_URL: ${OPA_PRIVATE_URL} CANDIG_AUTH: ${CANDIG_AUTHORIZATION} VAULT_URL: ${VAULT_SERVICE_URL} From 1c004bf69a0f83d264cf775df65ff8d4dc8e09e0 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 19 Oct 2022 10:45:43 -0700 Subject: [PATCH 108/236] Update keycloak_setup.sh (#165) * Update keycloak_setup.sh Set ${OPA_SITE_ADMIN_KEY} as a role and assign it to test user 2 * Update keycloak_setup.sh --- lib/keycloak/keycloak_setup.sh | 39 +++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh index f14ac4514..31bb67f41 100644 --- a/lib/keycloak/keycloak_setup.sh +++ b/lib/keycloak/keycloak_setup.sh @@ -53,11 +53,28 @@ add_user() { "type": "rawPassword", "value": "'${password}'" }' - echo "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/users/${user_id}" + curl \ -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ -X PUT -H "Content-Type: application/json" -d "${password_json}" \ "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/users/${user_id}/reset-password" -k + + if [[ ${attribute} == ${OPA_SITE_ADMIN_KEY} ]]; then + role_id=$(curl \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/roles" -k 2>/dev/null | + python3 -c 'import json,sys;obj=json.load(sys.stdin); print([l["id"] for l in obj if l["name"] == + "'"site_admin"'" ][0])') + local realm='[{ + "id": "'${role_id}'", + "name":"'${OPA_SITE_ADMIN_KEY}'" + }]' + curl \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X POST -H "Content-Type: application/json" -d "${realm}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/users/${user_id}/role-mappings/realm" -k + echo "Set ${username} with role ${OPA_SITE_ADMIN_KEY}" | tee -a $LOGFILE + fi } get_token() { @@ -212,6 +229,23 @@ set_client() { echo "Created client ${new_client}" | tee -a $LOGFILE } +set_role() { + local realm=$1 + local client=$2 + local role=$3 + + local JSON='{ + "name": "'${role}'" + }' + + role_id=`curl \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X POST -H "Content-Type: application/json" -d "${JSON}" \ + "${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${realm}/roles" -k` + + echo "Created role ${role}" | tee -a $LOGFILE +} + get_secret() { id=$(curl -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ ${KEYCLOAK_PUBLIC_URL}/auth/admin/realms/${KEYCLOAK_REALM}/clients -k 2>/dev/null | @@ -258,6 +292,9 @@ fi ; echo "Setting client ${KEYCLOAK_CLIENT_ID}" | tee -a $LOGFILE set_client "${KEYCLOAK_REALM}" "${KEYCLOAK_CLIENT_ID}" "${KEYCLOAK_LOGIN_REDIRECT_PATH}" +echo "Setting role ${OPA_SITE_ADMIN_KEY}" | tee -a $LOGFILE +set_role "${KEYCLOAK_REALM}" "${KEYCLOAK_CLIENT_ID}" "${OPA_SITE_ADMIN_KEY}" + echo "Getting keycloak secret" | tee -a $LOGFILE KEYCLOAK_SECRET_RESPONSE=$(get_secret ${KEYCLOAK_REALM}) export KEYCLOAK_SECRET=$KEYCLOAK_SECRET_RESPONSE From c7c37a7daebf917524878497b012e38258e75c6c Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 19 Oct 2022 13:56:56 -0700 Subject: [PATCH 109/236] Updates for docker build of candig-data-portal (#167) * moved Dockerfile inside repo * update versions for python and alpine * pass in env vars --- etc/env/example.env | 4 ++-- lib/candig-data-portal/Dockerfile | 21 --------------------- lib/candig-data-portal/docker-compose.yml | 7 ++++--- 3 files changed, 6 insertions(+), 26 deletions(-) delete mode 100644 lib/candig-data-portal/Dockerfile diff --git a/etc/env/example.env b/etc/env/example.env index 8d68c5cdc..7fb66fd5b 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -15,7 +15,7 @@ CANDIG_DEBUG_MODE=1 # options are [linux, darwin, arm64mac] VENV_OS=linux VENV_NAME=candig -VENV_PYTHON=3.9 +VENV_PYTHON=3.10 VENV_PIP=21.2.2 # docker @@ -29,7 +29,7 @@ DOCKER_NAMESPACE=candig DOCKER_REGISTRY=candig # options are [json, fluentd] DOCKER_LOG_DRIVER=json -ALPINE_VERSION=3.13 +ALPINE_VERSION=3.14 # docker swarm # options are [manager, worker] diff --git a/lib/candig-data-portal/Dockerfile b/lib/candig-data-portal/Dockerfile deleted file mode 100644 index 36bb8875a..000000000 --- a/lib/candig-data-portal/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -ARG katsu_api_target_url - -FROM node:14.16.0-alpine as build - -LABEL Maintainer="CanDIG Project" - -ENV TYK_KATSU_API_TARGET=$katsu_api_target_url - -RUN apk update && apk add gettext - -WORKDIR /app/candig-data-portal -ENV PATH /app/candig-data-portal/node_modules/.bin:$PATH - -RUN apk add --no-cache git curl vim - -COPY candig-data-portal . - -RUN npm install -RUN npm run build - -ENTRYPOINT ["npm", "start"] diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index 07e1de158..b661b5c4d 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.7' services: candig-data-portal: build: - context: $PWD/lib/candig-data-portal + context: $PWD/lib/candig-data-portal/candig-data-portal args: katsu_api_target_url: "${TYK_KATSU_API_TARGET}" image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} @@ -29,8 +29,9 @@ services: environment: - REACT_APP_KATSU_API_SERVER=${CHORD_METADATA_PUBLIC_URL} - REACT_APP_CANDIG_SERVER=${CANDIG_PUBLIC_URL} - #- REACT_APP_FEDERATION_API_SERVER=http://${CANDIG_DOMAIN}:${FEDERATION_PORT} - #- REACT_APP_BASE_NAME='/' + - REACT_APP_HTSGET_SERVER=${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} + - REACT_APP_FEDERATION_API_SERVER=http://${CANDIG_DOMAIN}:${FEDERATION_PORT} + - REACT_APP_BASE_NAME='/' - REACT_APP_SITE_LOCATION=${CANDIG_SITE_LOCATION} healthcheck: test: [ "CMD", "curl", "http://localhost:3000" ] From 610bf9261d5f6b7c9335bb1de9975d41837e9908 Mon Sep 17 00:00:00 2001 From: Son Chau Date: Thu, 20 Oct 2022 16:20:12 -0700 Subject: [PATCH 110/236] Sonchau/install docker m1 (#170) * docs: update docker for m1 * docs: wording docs: typo and styling docs: wording docs: wording * docs: update title * fix: update insall-docker.md * install-docker docs patch - c3g arm64-keycloak image * update md with docker.localhost no longer use host.docker.internal * docs: update mac m1 readme no longer use host.docker.internal Co-authored-by: Brennan Brouillette Co-authored-by: Daisie Huang --- docs/install-docker.md | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/docs/install-docker.md b/docs/install-docker.md index df241c681..5cbab603e 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -268,8 +268,8 @@ cp -i etc/env/example.env .env ```bash # options are [, , host.docker.internal, docker.localhost] -CANDIG_DOMAIN=host.docker.internal -CANDIG_AUTH_DOMAIN=host.docker.internal +CANDIG_DOMAIN=docker.localhost +CANDIG_AUTH_DOMAIN=docker.localhost ... # options are [linux, darwin, arm64mac] VENV_OS=arm64mac @@ -313,7 +313,7 @@ make compose - Get the local IP address in the terminal: ```bash -dig -4 TXT +short o-o.myaddr.l.google.com @ns1.google.com +ifconfig -l | xargs -n1 ipconfig getifaddr ``` - Then edit your /etc/hosts file: @@ -326,7 +326,7 @@ sudo nano /etc/hosts ```bash # Other settings -127.0.0.1 host.docker.internal +192.168.X.XX docker.localhost ``` ### Step 6: Create Auth Stack @@ -344,25 +344,11 @@ The old keycloak image (15.0.0) is not compatible with M1, so we need to upgrade Go to `lib/keycloak/docker-compose.yml` and replace the `- BASE_IMAGE=candig/keycloak:${KEYCLOAK_VERSION}` with one of the following: ```bash -- BASE_IMAGE=mihaibob/keycloak:18.0.2-legacy +- BASE_IMAGE=mihaibob/keycloak:18.0.2-legacy # (from StackOverflow) # or - BASE_IMAGE=quay.io/c3genomics/keycloak:16.1.1.arm64 # (an alternative built on an M1, for an M1) ``` -(I found it on StackOverflow, and it worked, but @shaikh-rashid might want to look for an "official" one or build a candig version for us) - -Note: for local development, add extra_hosts: - -```bash - networks: - - ${DOCKER_NET} - extra_hosts: - - "host.docker.internal:127.0.0.1" - # comment this out too - # volumes: - # - keycloak-data:/opt/jboss/keycloak/standalone -``` - Then run `make`: ```bash From d58179a57307c58cd48c60b3de15a7e4052b1fe0 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 24 Oct 2022 11:18:29 -0700 Subject: [PATCH 111/236] Remove chord-drs/drs-server from stack (#168) --- .gitmodules | 3 --- etc/env/example.env | 17 +---------------- lib/drs-server/chord_drs | 1 - lib/drs-server/docker-compose.yml | 30 ------------------------------ lib/htsget-server/htsget_app | 2 +- 5 files changed, 2 insertions(+), 51 deletions(-) delete mode 160000 lib/drs-server/chord_drs delete mode 100644 lib/drs-server/docker-compose.yml diff --git a/.gitmodules b/.gitmodules index 3b0e987d4..0fc3489d7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "lib/htsget-server/htsget_app"] path = lib/htsget-server/htsget_app url = https://github.com/CanDIG/htsget_app.git -[submodule "lib/drs-server/chord_drs"] - path = lib/drs-server/chord_drs - url = https://github.com/CanDIG/chord_drs.git [submodule "lib/federation-service/federation_service"] path = lib/federation-service/federation_service url = https://github.com/CanDIG/federation_service diff --git a/etc/env/example.env b/etc/env/example.env index 7fb66fd5b..2f0fca2a1 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope minio drs-server htsget-server chord-metadata candig-server candig-data-portal #wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard federation-service +CANDIG_MODULES=weavescope minio htsget-server chord-metadata candig-server candig-data-portal #drs-server wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget federation-service CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -127,10 +127,6 @@ MINIO_SELF_CERT=1 #MINIO_VOLUME_OPT+=--opt=type=ext4 #MINIO_VOLUME_OPT+=--opt=device=/dev/sdb1 -# chord-drs -CHORD_DRS_VERSION=v0.4.0 -CHORD_DRS_PORT=6000 - # htsget-app HTSGET_APP_VERSION=v0.1.8 HTSGET_PRIVATE_URL=http://htsget-app:3000 @@ -348,17 +344,6 @@ OPA_PRIVATE_URL=http://opa:8181 OPA_SITE_ADMIN_KEY=site_admin -# cancogen_dashboard -CANCOGEN_DASHBOARD_VERSION=v0.4.0 -CANCOGEN_DASHBOARD_HOST=0.0.0.0 -CANCOGEN_DASHBOARD_PORT=3002 -CANCOGEN_BASE_URL=http://candig-server:3001 -CANCOGEN_METADATA_URL=http://chord-metadata:8008 -CANCOGEN_HTSGET_URL=http://htsget-app:3333 -CANCOGEN_DRS_URL=http://chord-drs:6000 -CANCOGEN_FEDERATION_URL=http://federation-service:4232/federation/search - - # candig-data-server (previously mcode) CANDIG_DATA_PORTAL_VERSION=v0.1.3 CANDIG_DATA_PORTAL_PORT=2543 diff --git a/lib/drs-server/chord_drs b/lib/drs-server/chord_drs deleted file mode 160000 index fa14c5e3c..000000000 --- a/lib/drs-server/chord_drs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fa14c5e3cb26c7b90c926cbf14ba2c894c09e9fd diff --git a/lib/drs-server/docker-compose.yml b/lib/drs-server/docker-compose.yml deleted file mode 100644 index 0b6cbcfdb..000000000 --- a/lib/drs-server/docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: '3.7' - -services: - chord-drs: - build: - context: $PWD/lib/drs-server/chord_drs - args: - venv_python: "${VENV_PYTHON}" - alpine_version: "${ALPINE_VERSION}" - image: ${DOCKER_REGISTRY}/chord-drs:${CHORD_DRS_VERSION:-latest} - networks: - - ${DOCKER_NET} - ports: - - "${CHORD_DRS_PORT}:5000" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.chord-drs.rule=Host(`chord-drs.${CANDIG_DOMAIN}`)" - - "traefik.http.services.chord-drs.loadbalancer.server.port=${CHORD_DRS_PORT}" - logging: *default-logging - command: ["--host", "0.0.0.0", "--port", "5000"] diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 012fe3670..62b152e5d 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 012fe3670aed98d35377ae71a372d87da4842341 +Subproject commit 62b152e5d760b621bbd43a4ad9e1c19c0bb70875 From 64cd1b79717f41df731088e4ad9e9b71e0af8796 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 24 Oct 2022 11:18:54 -0700 Subject: [PATCH 112/236] Tiny fixes (#171) * no base name for data portal * token cookie can't be httponly * I could've sworn I turned crond on... --- lib/candig-data-portal/docker-compose.yml | 2 +- lib/tyk/configuration_templates/virtualLogin.js | 2 +- lib/vault/entrypoint.sh | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index b661b5c4d..f6b4795c2 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -31,7 +31,7 @@ services: - REACT_APP_CANDIG_SERVER=${CANDIG_PUBLIC_URL} - REACT_APP_HTSGET_SERVER=${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} - REACT_APP_FEDERATION_API_SERVER=http://${CANDIG_DOMAIN}:${FEDERATION_PORT} - - REACT_APP_BASE_NAME='/' + - REACT_APP_BASE_NAME= - REACT_APP_SITE_LOCATION=${CANDIG_SITE_LOCATION} healthcheck: test: [ "CMD", "curl", "http://localhost:3000" ] diff --git a/lib/tyk/configuration_templates/virtualLogin.js b/lib/tyk/configuration_templates/virtualLogin.js index d1d4e74ba..d4e0fa377 100644 --- a/lib/tyk/configuration_templates/virtualLogin.js +++ b/lib/tyk/configuration_templates/virtualLogin.js @@ -50,7 +50,7 @@ var loginHelper = { var cookie = "session_id=" + token cookie += ";Path=/" cookie += ";Max-Age=" + spec.config_data.MAX_TOKEN_AGE - cookie += ";HttpOnly" + // cookie += ";HttpOnly" if (spec.config_data.USE_SSL) { cookie += ";Secure" diff --git a/lib/vault/entrypoint.sh b/lib/vault/entrypoint.sh index 600a5c777..d9c9cb614 100644 --- a/lib/vault/entrypoint.sh +++ b/lib/vault/entrypoint.sh @@ -26,6 +26,7 @@ crontab -l > cron_bkp echo "0 */5 * * * bash /vault/create_token.sh" >> cron_bkp crontab cron_bkp rm cron_bkp +crond while [ 0 -eq 0 ] do From 83561003d71280432a6a2e6ec4021c3a0324193f Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 25 Oct 2022 23:39:56 -0700 Subject: [PATCH 113/236] Refresh token rotation (#173) --- etc/env/example.env | 2 + lib/candig-data-portal/docker-compose.yml | 4 +- lib/htsget-server/htsget_app | 2 +- .../api_candig.json.tpl | 4 + .../api_federation.json.tpl | 4 + .../api_graphql.json.tpl | 4 + .../api_htsget.json.tpl | 4 + .../api_katsu_chord.json.tpl | 4 + .../api_mcode_candig_data_portal.json.tpl | 4 + .../configuration_templates/api_opa.json.tpl | 4 + .../api_vault.json.tpl | 4 + .../backendAuthMiddleware.js | 50 +++++++++++- .../frontendAuthMiddleware.js | 78 +++++++++++-------- .../configuration_templates/virtualLogin.js | 9 +-- 14 files changed, 135 insertions(+), 42 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 2f0fca2a1..55ce01ab6 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -130,6 +130,7 @@ MINIO_SELF_CERT=1 # htsget-app HTSGET_APP_VERSION=v0.1.8 HTSGET_PRIVATE_URL=http://htsget-app:3000 +HTSGET_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} HTSGET_APP_PORT=3333 # wes server @@ -186,6 +187,7 @@ FEDERATION_VERSION=v0.5.4 FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 FEDERATION_SERVICE_URL=http://federation-service:${FEDERATION_PORT} +FEDERATION_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_FEDERATION_API_LISTEN_PATH} # chord metadata service CHORD_METADATA_VERSION=v1.4.4 diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index f6b4795c2..14d534ce7 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -29,8 +29,8 @@ services: environment: - REACT_APP_KATSU_API_SERVER=${CHORD_METADATA_PUBLIC_URL} - REACT_APP_CANDIG_SERVER=${CANDIG_PUBLIC_URL} - - REACT_APP_HTSGET_SERVER=${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} - - REACT_APP_FEDERATION_API_SERVER=http://${CANDIG_DOMAIN}:${FEDERATION_PORT} + - REACT_APP_HTSGET_SERVER=${HTSGET_PUBLIC_URL} + - REACT_APP_FEDERATION_API_SERVER=${FEDERATION_PUBLIC_URL} - REACT_APP_BASE_NAME= - REACT_APP_SITE_LOCATION=${CANDIG_SITE_LOCATION} healthcheck: diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index 62b152e5d..af4f07d28 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit 62b152e5d760b621bbd43a4ad9e1c19c0bb70875 +Subproject commit af4f07d28924a24ad66ddffaf483bafff5c7a66e diff --git a/lib/tyk/configuration_templates/api_candig.json.tpl b/lib/tyk/configuration_templates/api_candig.json.tpl index 370ebf70a..05fec4917 100644 --- a/lib/tyk/configuration_templates/api_candig.json.tpl +++ b/lib/tyk/configuration_templates/api_candig.json.tpl @@ -84,6 +84,10 @@ "config_data": { "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" diff --git a/lib/tyk/configuration_templates/api_federation.json.tpl b/lib/tyk/configuration_templates/api_federation.json.tpl index 46a987261..fc5faf9f5 100644 --- a/lib/tyk/configuration_templates/api_federation.json.tpl +++ b/lib/tyk/configuration_templates/api_federation.json.tpl @@ -78,6 +78,10 @@ "config_data": { "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" diff --git a/lib/tyk/configuration_templates/api_graphql.json.tpl b/lib/tyk/configuration_templates/api_graphql.json.tpl index 8bfdad2a7..8adf65739 100644 --- a/lib/tyk/configuration_templates/api_graphql.json.tpl +++ b/lib/tyk/configuration_templates/api_graphql.json.tpl @@ -84,6 +84,10 @@ "config_data": { "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" diff --git a/lib/tyk/configuration_templates/api_htsget.json.tpl b/lib/tyk/configuration_templates/api_htsget.json.tpl index 386d794c7..ac80bd185 100644 --- a/lib/tyk/configuration_templates/api_htsget.json.tpl +++ b/lib/tyk/configuration_templates/api_htsget.json.tpl @@ -84,6 +84,10 @@ "config_data": { "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" diff --git a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl index 8d49e8aee..0a1a72ab2 100644 --- a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl +++ b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl @@ -84,6 +84,10 @@ "config_data": { "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" diff --git a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl index c2ab9d2b0..f177c8c2c 100644 --- a/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl +++ b/lib/tyk/configuration_templates/api_mcode_candig_data_portal.json.tpl @@ -87,6 +87,10 @@ "/data-portal" ], "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" diff --git a/lib/tyk/configuration_templates/api_opa.json.tpl b/lib/tyk/configuration_templates/api_opa.json.tpl index 502394ee6..068e55d44 100644 --- a/lib/tyk/configuration_templates/api_opa.json.tpl +++ b/lib/tyk/configuration_templates/api_opa.json.tpl @@ -84,6 +84,10 @@ "config_data": { "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" diff --git a/lib/tyk/configuration_templates/api_vault.json.tpl b/lib/tyk/configuration_templates/api_vault.json.tpl index 3c3c3694e..a227c45b8 100644 --- a/lib/tyk/configuration_templates/api_vault.json.tpl +++ b/lib/tyk/configuration_templates/api_vault.json.tpl @@ -72,6 +72,10 @@ "config_data": { "TYK_SERVER": "${TYK_LOGIN_TARGET_URL}", + "KEYCLOAK_SECRET": "${KEYCLOAK_SECRET}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}", + "KEYCLOAK_PRIVATE_URL": "${KEYCLOAK_PRIVATE_URL}", "VAULT_SERVICE_URL":"${VAULT_SERVICE_URL}", "VAULT_SERVICE_RESOURCE":"/v1/auth/jwt/login", "VAULT_ROLE":"researcher" diff --git a/lib/tyk/configuration_templates/backendAuthMiddleware.js b/lib/tyk/configuration_templates/backendAuthMiddleware.js index 544038b84..ac9133a6c 100644 --- a/lib/tyk/configuration_templates/backendAuthMiddleware.js +++ b/lib/tyk/configuration_templates/backendAuthMiddleware.js @@ -16,18 +16,62 @@ function getCookie(request, cookie_name) { return valueCookie } +function exchangeRefreshTokenForIdToken(refreshToken, request, spec) { + body = { + "grant_type": "refresh_token", + "client_id": spec.config_data.KEYCLOAK_CLIENT_ID, + "client_secret": spec.config_data.KEYCLOAK_SECRET, + "refresh_token": refreshToken + } + + tokenRequest = { + "Method": "POST", + "FormData": body, + "Headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "Domain": spec.config_data.KEYCLOAK_PRIVATE_URL, + "Resource": "/auth/realms/" + spec.config_data.KEYCLOAK_REALM + "/protocol/openid-connect/token" + } + + var encodedResponse = TykMakeHttpRequest(JSON.stringify(tokenRequest)); + var decodedResponse = JSON.parse(encodedResponse); + try { + var decodedBody = JSON.parse(decodedResponse.Body); + if (decodedBody != undefined) { + if (_.has(decodedBody, "error")) { + log(decodedBody.error) + return undefined + } + log("old token is " + refreshToken + ", refresh token is " + decodedBody.refresh_token) + request.SetHeaders["Authorization"] = "Bearer " + decodedBody.id_token; + return decodedBody.refresh_token; + } + } catch (err) { + log(err) + return undefined + } + return undefined +} + backendAuthMiddleware.NewProcessRequest(function(request, session, spec) { - // log("Running Authorization JSVM middleware") + log("Running Backend Authorization JSVM middleware") if (request.Headers["Authorization"] === undefined) { try { var tokenCookie = getCookie(request, "session_id") - var idToken = tokenCookie.split("=")[1]; - request.SetHeaders["Authorization"] = "Bearer " + idToken; } catch(err) { log(err) var tokenCookie = undefined } + if (tokenCookie != undefined) { + var refreshToken = tokenCookie.split("=")[1]; + result = exchangeRefreshTokenForIdToken(refreshToken, request, spec); + request.ReturnOverrides.ResponseHeaders = { + "Set-Cookie": result + } + return backendAuthMiddleware.ReturnData(request, session.meta_data); + } } return backendAuthMiddleware.ReturnData(request, session.meta_data); diff --git a/lib/tyk/configuration_templates/frontendAuthMiddleware.js b/lib/tyk/configuration_templates/frontendAuthMiddleware.js index c83f8d29d..1cfe62575 100644 --- a/lib/tyk/configuration_templates/frontendAuthMiddleware.js +++ b/lib/tyk/configuration_templates/frontendAuthMiddleware.js @@ -16,25 +16,46 @@ function getCookie(request, cookie_name) { return valueCookie } -function isTokenExpired(idToken) { - tokenPayload = idToken.split(".")[1] - padding = tokenPayload.length % 4 - - if (padding != 0) { - _.times(4-padding, function() { - tokenPayload += "=" - }) +function exchangeRefreshTokenForIdToken(refreshToken, request, spec) { + body = { + "grant_type": "refresh_token", + "client_id": spec.config_data.KEYCLOAK_CLIENT_ID, + "client_secret": spec.config_data.KEYCLOAK_SECRET, + "refresh_token": refreshToken } - - decodedPayload = JSON.parse(b64dec(tokenPayload)) - tokenExpires = decodedPayload["exp"] - sysTime = (new Date).getTime()/1000 | 0; - - return sysTime>tokenExpires + + tokenRequest = { + "Method": "POST", + "FormData": body, + "Headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "Domain": spec.config_data.KEYCLOAK_PRIVATE_URL, + "Resource": "/auth/realms/" + spec.config_data.KEYCLOAK_REALM + "/protocol/openid-connect/token" + } + + var encodedResponse = TykMakeHttpRequest(JSON.stringify(tokenRequest)); + var decodedResponse = JSON.parse(encodedResponse); + try { + var decodedBody = JSON.parse(decodedResponse.Body); + if (decodedBody != undefined) { + if (_.has(decodedBody, "error")) { + log(decodedBody.error) + return undefined + } + log("old token is " + refreshToken + ", refresh token is " + decodedBody.refresh_token) + request.SetHeaders["Authorization"] = "Bearer " + decodedBody.id_token; + return decodedBody.refresh_token; + } + } catch (err) { + log(err) + return undefined + } + return undefined } frontendAuthMiddleware.NewProcessRequest(function(request, session, spec) { - // log("Running Authorization JSVM middleware") + log("Running Frontend Authorization JSVM middleware ") if (request.Headers["Authorization"] === undefined) { try { @@ -43,27 +64,22 @@ frontendAuthMiddleware.NewProcessRequest(function(request, session, spec) { log(err) var tokenCookie = undefined } - if (tokenCookie != undefined) { - var idToken = tokenCookie.split("=")[1]; - - if (isTokenExpired(idToken)) { - request.ReturnOverrides.ResponseCode = 302; + var refreshToken = tokenCookie.split("=")[1]; + result = exchangeRefreshTokenForIdToken(refreshToken, request, spec); + if (result != undefined) { request.ReturnOverrides.ResponseHeaders = { - "Location": spec.config_data.TYK_SERVER + "/auth/login?app_url=" + request.URL - }; - } else { - request.SetHeaders["Authorization"] = "Bearer " + idToken; - } - - } else { - request.ReturnOverrides.ResponseCode = 302 - request.ReturnOverrides.ResponseHeaders = { - "Location": spec.config_data.TYK_SERVER + "/auth/login?app_url=" + request.URL + "Set-Cookie": result + } + return frontendAuthMiddleware.ReturnData(request, session.meta_data); } } } - + // if we couldn't get a valid token from the cookie, send the user back to login: + request.ReturnOverrides.ResponseCode = 302 + request.ReturnOverrides.ResponseHeaders = { + "Location": spec.config_data.TYK_SERVER + "/auth/login?app_url=" + request.URL + } return frontendAuthMiddleware.ReturnData(request, session.meta_data); }); diff --git a/lib/tyk/configuration_templates/virtualLogin.js b/lib/tyk/configuration_templates/virtualLogin.js index d4e0fa377..5476de09d 100644 --- a/lib/tyk/configuration_templates/virtualLogin.js +++ b/lib/tyk/configuration_templates/virtualLogin.js @@ -50,7 +50,7 @@ var loginHelper = { var cookie = "session_id=" + token cookie += ";Path=/" cookie += ";Max-Age=" + spec.config_data.MAX_TOKEN_AGE - // cookie += ";HttpOnly" + cookie += ";HttpOnly" if (spec.config_data.USE_SSL) { cookie += ";Secure" @@ -98,13 +98,12 @@ function loginHandler(request, session, spec) { location = "" } - if (_.has(decodedBody, "id_token")) { - var idToken = decodedBody["id_token"] + if (_.has(decodedBody, "id_token") && _.has(decodedBody, "refresh_token")) { var responseObject = { Body: "Success", Headers: { - "Authorization": "Bearer " + idToken, - "Set-Cookie": loginHelper.setCookie(idToken, spec), + "Authorization": "Bearer " + decodedBody["id_token"], + "Set-Cookie": loginHelper.setCookie(decodedBody["refresh_token"], spec), "Location": spec.config_data.TYK_SERVER + location }, Code: 302 From 6833ed267fef587ed302a26290d27baf7e975854 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 15 Nov 2022 15:51:21 -0800 Subject: [PATCH 114/236] Update submods (#169) * Update submods * bump * Update candig-data-portal * Update chord_metadata_service * Update federation_service * Update htsget_app * update CANDIG_MODULES to include federation-service * remove vars for cancogen_dashboard * Update htsget_app * Update chord_metadata_service * Update candig-data-portal * Update example.env * Update htsget * Update candig-data-portal * bump htsget version * Update candig-data-portal * Update opa * Update htsget docker-compose's DB_PATH * remove unused env vars and secrets * Update htsget_app * Update candig-data-portal * Update federation_service * Update versions for submodules --- etc/env/example.env | 12 ++++++------ lib/candig-data-portal/candig-data-portal | 2 +- lib/chord-metadata/chord_metadata_service | 2 +- lib/chord-metadata/docker-compose.yml | 3 --- lib/federation-service/federation_service | 2 +- lib/htsget-server/docker-compose.yml | 3 ++- lib/htsget-server/htsget_app | 2 +- lib/opa/opa | 2 +- 8 files changed, 13 insertions(+), 15 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index 55ce01ab6..4cb26b515 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope minio htsget-server chord-metadata candig-server candig-data-portal #drs-server wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget federation-service +CANDIG_MODULES=weavescope minio htsget-server chord-metadata candig-server candig-data-portal #federation-service drs-server wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -128,7 +128,7 @@ MINIO_SELF_CERT=1 #MINIO_VOLUME_OPT+=--opt=device=/dev/sdb1 # htsget-app -HTSGET_APP_VERSION=v0.1.8 +HTSGET_APP_VERSION=v1.0.0 HTSGET_PRIVATE_URL=http://htsget-app:3000 HTSGET_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} HTSGET_APP_PORT=3333 @@ -183,14 +183,14 @@ JUPYTER_ENABLE_LAB=yes JUPYTER_ENABLE_SUDO=yes # federation_service -FEDERATION_VERSION=v0.5.4 +FEDERATION_VERSION=v0.5.5 FEDERATION_IP=0.0.0.0 FEDERATION_PORT=4232 FEDERATION_SERVICE_URL=http://federation-service:${FEDERATION_PORT} FEDERATION_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_FEDERATION_API_LISTEN_PATH} # chord metadata service -CHORD_METADATA_VERSION=v1.4.4 +CHORD_METADATA_VERSION=v1.5.0 CHORD_METADATA_PORT=8008 CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false @@ -338,7 +338,7 @@ VAULT_SERVICE_URL=http://vault:8200 # OPA -OPA_VERSION=v1.1.1 +OPA_VERSION=v1.2.0 OPA_PORT=8181 OPA_LOG_LEVEL=debug OPA_URL=http://${CANDIG_DOMAIN}:${OPA_PORT} @@ -347,7 +347,7 @@ OPA_SITE_ADMIN_KEY=site_admin # candig-data-server (previously mcode) -CANDIG_DATA_PORTAL_VERSION=v0.1.3 +CANDIG_DATA_PORTAL_VERSION=v0.1.8 CANDIG_DATA_PORTAL_PORT=2543 CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT}/data-portal CANDIG_DATA_PORTAL_PRIVATE_URL=http://candig-data-portal:3000 diff --git a/lib/candig-data-portal/candig-data-portal b/lib/candig-data-portal/candig-data-portal index 19730e9f9..4cd490716 160000 --- a/lib/candig-data-portal/candig-data-portal +++ b/lib/candig-data-portal/candig-data-portal @@ -1 +1 @@ -Subproject commit 19730e9f9c10db33046807d1bee10c830f5a81a3 +Subproject commit 4cd490716aa437a4658ce5598f870e341aee3f5a diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index e3c878e6b..405308b93 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit e3c878e6bb64f222ee0b369d64740dbfc61b046f +Subproject commit 405308b9304c883248bef0a46f7a7834424dad60 diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 07be09dc2..8ef84e886 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -40,7 +40,6 @@ services: - CANDIG_OPA_URL=${OPA_PRIVATE_URL} - CANDIG_AUTHORIZATION=${CANDIG_AUTHORIZATION} - CACHE_TIME=${CACHE_TIME} - - CANDIG_OPA_SECRET=${CANDIG_OPA_SECRET} - CANDIG_OPA_SITE_ADMIN_KEY=${OPA_SITE_ADMIN_KEY} - HOST_CONTAINER_NAME=${CHORD_METADATA_CONTAINER} secrets: @@ -52,8 +51,6 @@ services: target: metadata_db_secret - source: opa-root-token target: opa-root-token - - source: opa-service-token - target: opa-service-token entrypoint: - /bin/bash - -c diff --git a/lib/federation-service/federation_service b/lib/federation-service/federation_service index d3ba8e14b..5e5a5ad58 160000 --- a/lib/federation-service/federation_service +++ b/lib/federation-service/federation_service @@ -1 +1 @@ -Subproject commit d3ba8e14b268284340b2a00194a33b776d3c9a3e +Subproject commit 5e5a5ad58a8ffefb6d39206dcf29ff8741a09483 diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index ea31b65d3..5337dd748 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -47,5 +47,6 @@ services: CANDIG_AUTH: ${CANDIG_AUTHORIZATION} VAULT_URL: ${VAULT_SERVICE_URL} DEBUG_MODE: ${CANDIG_DEBUG_MODE} - DB_PATH: /data/files.sql + DB_PATH: /data/files.db + TESTENV_URL: ${HTSGET_PRIVATE_URL} command: ["--host", "0.0.0.0", "--port", "3000"] diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index af4f07d28..ccb7827da 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit af4f07d28924a24ad66ddffaf483bafff5c7a66e +Subproject commit ccb7827da72c61e95b6d96825f41e8a63a5680ee diff --git a/lib/opa/opa b/lib/opa/opa index cc16c3dff..9e105da86 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit cc16c3dff885198373be3d02b196dc789fc20b9e +Subproject commit 9e105da86f372d4c41ab167198249fa5a1c61ab7 From b5281d58f40896fa60c44c324dfd79b9ba5b1b88 Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Tue, 22 Nov 2022 15:23:52 -0500 Subject: [PATCH 115/236] Tyk federation fixes (#175) * disable vault permissionsStoreMiddleware for federated apis * Documentation for federation-service, candig-prod chagnes * Update candig-data-portal * Update chord_metadata_service * Update federation_service * Update htsget_app * Update example.env * Update opa * Update htsget docker-compose's DB_PATH * remove unused env vars and secrets Co-authored-by: Shaikh Rashid Co-authored-by: Daisie Huang --- docs/configure-federation.md | 60 +++++++++++++++++++ etc/env/example.env | 8 +-- lib/candig-data-portal/docker-compose.yml | 9 ++- lib/htsget-server/htsget_app | 2 +- lib/opa/opa | 2 +- .../api_htsget.json.tpl | 8 +-- .../api_katsu_chord.json.tpl | 8 +-- 7 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 docs/configure-federation.md diff --git a/docs/configure-federation.md b/docs/configure-federation.md new file mode 100644 index 000000000..34894476c --- /dev/null +++ b/docs/configure-federation.md @@ -0,0 +1,60 @@ +# Configuring Federation Service + +## Initialize Federation Service + +Before running federation service, we first need to define the servers and services that is needed for the federation-service `docker-compose.yml`. The two main files that need to be created are `servers.json` and `services.json`. These files need to be added in the `/tmp/federation/` directory of the CanDIGv2 repo. See `/lib/federation-service/federation_service/configs/servers.json` and `/lib/federation-service/federation_service/configs/services.json` for examples. For the CanDIGv2 network, the `servers.json` is: + +```json +{ + "servers": [ + { + "url": "https://candigtest.bcgsc.ca/federation/search", + "location": ["BCGSC", "British Columbia", "ca-bc"] + }, + { + "url": "https://candigv2.calculquebec.ca:8081/federation/search", + "location": ["C3G", "Quebec", "ca-qc"] + }, + { + "url": "https://candig.uhnresearch.ca/federation/search", + "location": ["UHN", "Ontario", "ca-on"] + } + ] +} +``` + +Services need to follow a consistent naming pattern so consult with federation site operators regarding the convention for service names. But generally, the names of services in `services.json` is the accepted naming scheme. As for the service URL, this will be the PUBLIC_URL of the corresponding data service. + +Once these files are set, you can add `federation-service` to the list of `CANDIG_MODULES` in `.env` and then run `make docker-pull` or `make build-federation-service` if you want to either pull the latest federation-service image matching `FEDERATION_VERSION` or build it from source. After this, you can run `make compose-federation-service` to deploy the federation-service in the CanDIGv2 stack. You can test the service by folloring the testing steps provided in `/lib/federation-service/federation_service/README.md`. + +## Running Federation Service Behind Tyk + +Once the federation-service is running, you will need to update your tyk configuration templates in order to allow other federation servers to peer with each other. To do this, you will need to add the `issuer` and `client_ids` of the trusted nodes into any of the `api_*.json.tpl` files in `/lib/tyk/configuration_templates/`. This must be done for each service defined in `services.json` that you want federation peer(s) to access. For example, if you wanted to allow the UHN CanDIGv2 node to make federated searches to `katsu` data service, you would need to modify the `/lib/tyk/configuration_templates/api_katsu_chord.json.tpl` file and change the `providers` section to: + +```json +... + "providers": [ + { + "issuer": "${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM}", + "client_ids": { + "${KEYCLOAK_CLIENT_ID_64}": "${TYK_POLICY_ID}" + } + }, + { + "issuer": "https://candigauth.uhnresearch.ca/auth/realms/candig", + "client_ids": { + "Y2FuZGlnX3Vobg==": "candig_policy" + } + } + ] + }, +... +``` + +After you have made the necessary changes, you can apply them to your tyk instance by running `make redeploy-tyk`. Once the tyk service has be updated, you can verify the service by requesting the peer(s) to perform an HTTP request to the `/federation/servers` endpoint of your federation-service. Additionally, can request the federated peer perform a search using a OIDC jwt as a Bearer token in their request header. This may be successful, but could potentially fail if the username and access token has not been granted access through OPA. + +## Where to Find Your CanDIG Node's OIDC Provider + +If you have a running tyk gateway, you can see your `providers` config under `lib/tyk/tmp/apps/api_*.json`. Provide your `issuer`, `client_id`, and `tyk_policy_id` to your respective federation peers to enable federated search capabilities on their CanDIGv2 nodes. + +Keep in mind, if you change your `KEYCLOAK_CLIENT_ID`, `TYK_POLICY_ID`, or `KEYCLOAK_PUBLIC_URL` in `.env` you will need to notify and provide the updated `providers` config to other CanDIGv2 nodes or you may no longer be able to run federated search. diff --git a/etc/env/example.env b/etc/env/example.env index 4cb26b515..fbd51ab83 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=weavescope minio htsget-server chord-metadata candig-server candig-data-portal #federation-service drs-server wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget cancogen-dashboard +CANDIG_MODULES=minio htsget-server chord-metadata candig-server candig-data-portal #drs-server wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget federation-service CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -16,7 +16,7 @@ CANDIG_DEBUG_MODE=1 VENV_OS=linux VENV_NAME=candig VENV_PYTHON=3.10 -VENV_PIP=21.2.2 +VENV_PIP=22.2.2 # docker # options are [bridge, bridge-net, ingress, traefik-net] @@ -113,14 +113,14 @@ TRAEFIK_SOCKET=unix:///var/run/docker.sock MINIO_VERSION=latest MINIO_UI_PORT=9090 MINIO_PORT=9000 -MINIO_PUBLIC_URL=https://${CANDIG_DOMAIN}:${MINIO_PORT} +MINIO_PUBLIC_URL=http://${CANDIG_DOMAIN}:${MINIO_PORT} MINIO_PRIVATE_URL=http://minio:9000 MINIO_BUCKET=samples MINIO_REGION=us-east-1 MINIO_DATA_DIR=/data # set to 1 if using SSL via self-signed certificate -MINIO_SELF_CERT=1 +MINIO_SELF_CERT=0 # docker volume options for minio-data #MINIO_VOLUME_OPT=--driver=local diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index 14d534ce7..2a180204c 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -10,7 +10,7 @@ services: networks: - ${DOCKER_NET} ports: - - "${CANDIG_DATA_PORTAL_PORT}:3000" + - "${CANDIG_DATA_PORTAL_PORT}:2543" deploy: placement: constraints: @@ -26,6 +26,11 @@ services: - "traefik.http.routers.candig-data-portal.rule=Host(`candig-data-portal.${CANDIG_DOMAIN}`)" - "traefik.http.services.candig-data-portal.loadbalancer.server.port=${CANDIG_DATA_PORTAL_PORT}" logging: *default-logging + secrets: + - source: vault-s3-token + target: vault-s3-token + - source: opa-service-token + target: opa-service-token environment: - REACT_APP_KATSU_API_SERVER=${CHORD_METADATA_PUBLIC_URL} - REACT_APP_CANDIG_SERVER=${CANDIG_PUBLIC_URL} @@ -34,7 +39,7 @@ services: - REACT_APP_BASE_NAME= - REACT_APP_SITE_LOCATION=${CANDIG_SITE_LOCATION} healthcheck: - test: [ "CMD", "curl", "http://localhost:3000" ] + test: [ "CMD", "curl", "http://localhost:2543" ] interval: 30s timeout: 20s retries: 3 diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index ccb7827da..cf6c331b5 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit ccb7827da72c61e95b6d96825f41e8a63a5680ee +Subproject commit cf6c331b5c5b5288e11e3c271478aecfedca3f58 diff --git a/lib/opa/opa b/lib/opa/opa index 9e105da86..9e32ff4f7 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 9e105da86f372d4c41ab167198249fa5a1c61ab7 +Subproject commit 9e32ff4f78d31f56b0037ad6cc56bbbc19757391 diff --git a/lib/tyk/configuration_templates/api_htsget.json.tpl b/lib/tyk/configuration_templates/api_htsget.json.tpl index ac80bd185..2a49c01c3 100644 --- a/lib/tyk/configuration_templates/api_htsget.json.tpl +++ b/lib/tyk/configuration_templates/api_htsget.json.tpl @@ -60,13 +60,7 @@ "require_session": false } ], - "post": [ - { - "name": "permissionsStoreMiddleware", - "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", - "require_session": false - } - ], + "post": [], "id_extractor": { "extract_with": "", "extract_from": "", diff --git a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl index 0a1a72ab2..6dabaaa79 100644 --- a/lib/tyk/configuration_templates/api_katsu_chord.json.tpl +++ b/lib/tyk/configuration_templates/api_katsu_chord.json.tpl @@ -60,13 +60,7 @@ "require_session": false } ], - "post": [ - { - "name": "permissionsStoreMiddleware", - "path": "/opt/tyk-gateway/middleware/permissionsStoreMiddleware.js", - "require_session": false - } - ], + "post": [], "id_extractor": { "extract_with": "", "extract_from": "", From ecba8326e46c7f1681925dd5eb9674b19349d86f Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 25 Nov 2022 11:53:18 -0800 Subject: [PATCH 116/236] DIG-895: integrating candig-authx module (#177) * update env vars to match candigv2-authx * update katsu to match candigv2-authx env vars * Update chord_metadata_service * Update htsget_app * Update htsget_app * bump htsget version * bump katsu version --- etc/env/example.env | 4 ++-- lib/chord-metadata/chord_metadata_service | 2 +- lib/chord-metadata/docker-compose.yml | 4 ++-- lib/htsget-server/htsget_app | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/etc/env/example.env b/etc/env/example.env index fbd51ab83..992b49a3f 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -128,7 +128,7 @@ MINIO_SELF_CERT=0 #MINIO_VOLUME_OPT+=--opt=device=/dev/sdb1 # htsget-app -HTSGET_APP_VERSION=v1.0.0 +HTSGET_APP_VERSION=v1.0.1 HTSGET_PRIVATE_URL=http://htsget-app:3000 HTSGET_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} HTSGET_APP_PORT=3333 @@ -190,7 +190,7 @@ FEDERATION_SERVICE_URL=http://federation-service:${FEDERATION_PORT} FEDERATION_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_FEDERATION_API_LISTEN_PATH} # chord metadata service -CHORD_METADATA_VERSION=v1.5.0 +CHORD_METADATA_VERSION=v1.5.1 CHORD_METADATA_PORT=8008 CHORD_METADATA_HOST='*' CHORD_METADATA_AUTH=false diff --git a/lib/chord-metadata/chord_metadata_service b/lib/chord-metadata/chord_metadata_service index 405308b93..c0caf6e8d 160000 --- a/lib/chord-metadata/chord_metadata_service +++ b/lib/chord-metadata/chord_metadata_service @@ -1 +1 @@ -Subproject commit 405308b9304c883248bef0a46f7a7834424dad60 +Subproject commit c0caf6e8dab117ed90bb405a2ad6d806c46c8616 diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index 8ef84e886..b3ef5fdb7 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -37,10 +37,10 @@ services: - POSTGRES_USER=admin - POSTGRES_PASSWORD_FILE=/run/secrets/metadata_db_secret - INSIDE_CANDIG=${INSIDE_CANDIG} - - CANDIG_OPA_URL=${OPA_PRIVATE_URL} + - OPA_URL=${OPA_PRIVATE_URL} - CANDIG_AUTHORIZATION=${CANDIG_AUTHORIZATION} - CACHE_TIME=${CACHE_TIME} - - CANDIG_OPA_SITE_ADMIN_KEY=${OPA_SITE_ADMIN_KEY} + - OPA_SITE_ADMIN_KEY=${OPA_SITE_ADMIN_KEY} - HOST_CONTAINER_NAME=${CHORD_METADATA_CONTAINER} secrets: - source: metadata-app-secret diff --git a/lib/htsget-server/htsget_app b/lib/htsget-server/htsget_app index cf6c331b5..76d1c43c4 160000 --- a/lib/htsget-server/htsget_app +++ b/lib/htsget-server/htsget_app @@ -1 +1 @@ -Subproject commit cf6c331b5c5b5288e11e3c271478aecfedca3f58 +Subproject commit 76d1c43c46d46823b133331bb5e16a4fb7b306bc From abc0b8b2c46ee7c8ad96aaee487da98145f01f41 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Fri, 25 Nov 2022 11:57:04 -0800 Subject: [PATCH 117/236] fix reversion --- lib/opa/opa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/opa/opa b/lib/opa/opa index 9e32ff4f7..9e105da86 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 9e32ff4f78d31f56b0037ad6cc56bbbc19757391 +Subproject commit 9e105da86f372d4c41ab167198249fa5a1c61ab7 From 3ed66e3148a457e8d99df0166f9de87632b4d879 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 28 Nov 2022 12:00:00 -0800 Subject: [PATCH 118/236] set emails for fake users (#178) --- lib/keycloak/keycloak_setup.sh | 1 + lib/opa/opa | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/keycloak/keycloak_setup.sh b/lib/keycloak/keycloak_setup.sh index 31bb67f41..88023c711 100644 --- a/lib/keycloak/keycloak_setup.sh +++ b/lib/keycloak/keycloak_setup.sh @@ -26,6 +26,7 @@ add_user() { local JSON=' { "username": "'${username}'", + "email": "'${username}'@test.ca", "enabled": true, "attributes": { "'${attribute}'": [ diff --git a/lib/opa/opa b/lib/opa/opa index 9e105da86..b653b3760 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit 9e105da86f372d4c41ab167198249fa5a1c61ab7 +Subproject commit b653b376010d9c2d521a64bd0bb189264f53ba13 From 857c619ebe2c779a8c9a91bc42481178c2aa9184 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 29 Nov 2022 11:56:40 -0800 Subject: [PATCH 119/236] Update opa --- lib/opa/opa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/opa/opa b/lib/opa/opa index b653b3760..dd32df351 160000 --- a/lib/opa/opa +++ b/lib/opa/opa @@ -1 +1 @@ -Subproject commit b653b376010d9c2d521a64bd0bb189264f53ba13 +Subproject commit dd32df351116d6c988e5200ffbe06065e2e42572 From 1946ded0fc3aaef6d9865459de968001ff61dbcb Mon Sep 17 00:00:00 2001 From: Son Chau Date: Tue, 29 Nov 2022 13:32:27 -0800 Subject: [PATCH 120/236] post-deployment party instruction cleanup (#179) * clean up all instructions for m1 * highlight docker deployment guide * add note about location of M1 instructions * stub of testing instructions * explicit mention of env file * documentation of module configuration * remove outdated architecture diagram * update project structure * [Documentation] Add in further Host-editing documentation * Add WSL instructions Signed-off-by: Courtney Gosselin courtney@gosselin.io * updated hosts/firewall docs * add ingest instructions * changes from PR review * one more note about hosts * update email user2 example * add federation service instruction * fix the copy path to katsu * Update README.md Co-authored-by: OrdiNeu * Update docs/ingest-and-test.md Co-authored-by: OrdiNeu * Update docs/ingest-and-test.md Co-authored-by: OrdiNeu Signed-off-by: Courtney Gosselin courtney@gosselin.io Co-authored-by: Karen Cranston Co-authored-by: fnguyen Co-authored-by: Courtney Gosselin Co-authored-by: yavyx --- README.md | 132 +++++++++++----------------------------- docs/ingest-and-test.md | 125 +++++++++++++++++++++++++++++++++++++ docs/install-docker.md | 58 +++++++++--------- 3 files changed, 190 insertions(+), 125 deletions(-) create mode 100644 docs/ingest-and-test.md diff --git a/README.md b/README.md index 9a6faed7f..491e849d2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CanDIG v2 PoC +# CanDIG v2 - - - @@ -7,78 +7,6 @@ The CanDIG v2 project is a collection of heterogeneous services designed to work together to facilitate end to end dataflow for genomic data. -```plaintext - +--------------+ - | candig.local | - +-------+------+ - | - +-----------+-----------+ - | portainer:9010 (tcp) | - | portainer_agent | - +-----------+-----------+ - | - | +----------------------------+ - +----------+---------+ | consul:8300 (tcp) | - +-----------------------+ | traefik:8000 (tcp) | | :8400 (tcp) | - | weave_app:4040 (tcp) +-----+ :80 (tcp) +-------+ :8500 (tcp) | - | weave_probe | | :443 (tcp) | | :8502 (tcp) | - +-----------------------+ +----+-----+-----+---+ | :8301-8302 (tcp/udp) | - | | | | :8600 (udp) | - | | | +----------------------------+ - | | | - +----------------monitoring--+ | | | +-------------------logging--+ - | +-----------------------+ | | | | | +------------------------+ | - | | prometheus:9090 (tcp) | | | | | | | fluentd:24224 (tcp/udp)| | - | +-----------------------+ | | | | | +------------------------+ | - | +-----------+ | +-----------+ | - | +------------------------+ | | | | - | |node_exporter:9100 (tcp)| | | | | - | +------------------------+ | | | | - | | | | +------------------------+ | - | +-------------------+ | | | |elasticsearch:9200 (tcp)| | - | |cadvisor:9080 (tcp)| | | | | :9300 (tcp)| | - | +-------------------+ | | | +------------------------+ | - | | | | | - | +-----------------------+ | | | | - | |alertmanager:9093 (tcp)| | | | | - | +-----------------------+ | | | | - | | | | | - | +------------------+ | | | +-------------------+ | - | |grafana:9888 (tcp)| | | | | kibana:5601 (tcp) | | - | +------------------+ | | | +-------------------+ | - +----------------------------+ | +----------------------------+ - | - | -+---------------------------------+ +--------------------------------------+ +----------------------------+ -| +-----------------------+ | | | | +------------------------+ | -| | chord_metadata:8008 | | | | | | cnv_service:8870 (tcp) | | -| +-----------------------+ | | +-------------------------------+ | | +------------------------+ | -| +-----------------------------+ +----+ | federation_service:4232 (tcp) | +---+ | -| | datasets_service:8880 (tcp) | | | +-------------------------------+ | | +-------------------+ | -| +-----------------------------+ | | | | | rnaget:3005 (tcp) | | -+---------------------------------+ +--------------------------------------+ | +-------------------+ | - | +----------------------------+ - +--------+-----------+ - | jupyter:8888 (tcp) | - +---------------------------+ rstudio:8787 (tcp) | +--------------------------+ - | +--------+-----------+ | candig_server:3001 (tcp) | - +----------------------------------+ | +--------------------------+ - | +------------------------------+ | +---------------------------+ - | | wes_server:5000 (tcp) | | | +-----------------------+ | +-------------------+ - | | toil_master:5050 (tcp) | | | | htsget_app:3333 (tcp) | +------+ igv_js:9091 (tcp) | - | | toil_ui:3000 (tcp) | | | +-----------------------+ | +-------------------+ - | +------------------------------+ | | +-----------------------+ | - | | | | chord_drs:6000 (tcp) | | - | +------------------------------+ | | +-----------------------+ | - | | toil_worker:5051 (tcp) | | +---------------------------+ - | +------------------------------+ | | - +----------------------------------+ +--------+---------+ - | minio:9000 (tcp) | - | minio_client | - +------------------+ - -``` - ## Project Structure ```plaintext @@ -87,32 +15,32 @@ CanDIGv2/ ├── Makefile - functions for repeatable testing/deployment (Docker/Kubernetes) ├── tox.ini - functions for repeatable testing/deployment (Python Venv/Screen) ├── bin/ - local binaries directory - ├── docs/ - documentation for various aspects of CanDIGv2 + ├── docs/ - documentation, installation instructions ├── etc/ - contains misc files/config/scripts │    ├── docker/ - docker configurations - │    ├── env/ - sample env files for site.env + │    ├── env/ - sample .env file │    ├── ssl/ - ssl root-ca/site configs and certs + | ├── tests/ - integration tests (under development) │    ├── venv/ - dependency files for virtualenvs (conda, pip, etc.) │    └── yml/ - various yaml based configs (toil, traefik, etc.) ├── lib/ - contains modules of servies/apps - │   ├── compose/ - set of base docker variables for Compose - │   ├── kubernetes/ - set of base docker variables for Kubernetes - │   ├── swarm/ - set of base docker variables for Swarm - │   ├── templates/ - set of template files used to create new module(s) - │   └── ga4gh-dos/ - example module, folder name = module name (e.g. make compose-ga4gh-dos) - │       ├── docker-compose.yml - minimum requirement of module, contains deployment context - │       ├── Dockerfile - contains build context for module - │       └── run.sh - script which used for conda deployment (DEPRECATED) └── tmp/ - contains temporary files used for runtime functionality -   ├── configs/ - directory to store config files that are added to services post-deployment -   ├── data/ - directory to store local data for running services +   ├── configs/ - config files that are added to services post-deployment +   ├── data/ - local data for running services +   ├── federation/ - federation configuration files +   ├── tyk/ - tyk configuration files +   ├── vault/ - vault keys   └── secrets/ - directory to store randomly generated secrets for service deployment ``` ## `.env` Environment File You need an `.env` file in the project root directory, which contains a set of global variables that are used as reference to -the various parameters, plugins, and config options that operators can modify for testing purposes. There is an example `.env` file in `etc/example.env`. +the various parameters, plugins, and config options that operators can modify for testing purposes. This repo contains an example `.env` file in `etc/env/example.env`. + +When deploying CanDIGv2 +using `make`, `.env` is imported by `make` and all uncommented variables are added as environment variables via +`export`. Some of the functionality that is controlled through `.env` are: @@ -122,13 +50,8 @@ Some of the functionality that is controlled through `.env` are: * version control and app pinning * pre-defined defaults for turnkey deployment -Compose supports declaring default environment variables in an environment file named `.env` placed in the folder -where the `docker-compose` command is executed (current working directory). Similarly, when deploying CanDIGv2 -using `make`, `.env` is imported by `make` and all uncommented variables are added as environment variables via -`export`. - -These evironment variables can be read in `docker-compose` scripts through the variable substitution operator -`${VAR}`. +Environment variables defined in the `.env` file can be read in `docker-compose` scripts through the variable substitution operator +`${VAR}`. ```yaml # example compose YAML using variable substitution with default option @@ -138,20 +61,37 @@ services: network_mode: ${DOCKER_MODE} ... ``` +### Configuring CanDIG modules + +Not all CanDIG modules are required for a minimal installation. The `CANDIG_MODULES` and `CANDIG_AUTH_MODULES` define which modules are included in the deployment. + +By default (if you copy the sample file from `etc/env/example.env`) the installation includes the minimal list of modules: + + CANDIG_MODULES=minio htsget-server chord-metadata candig-server candig-data-portal + +Optional modules follow the `#` and include federation service, various monitoring components, workflow execution, and some older modules not generally installed. + +For federated installations, you will need `federation-service`. + +For production deployments, you will probably want _add prod modules here_. + +Authorization and authentication modules defined in `CANDIG_AUTH_MODULES` are only installed if you run `make init-authx` during deployment. ## `make` Deployment -To deploy CanDIGv2, follow one of the available install guides in `docs/`: +To deploy CanDIGv2, follow the docker deployment guide in `docs/`: -* [Vagrant Deployment Guide (with instructions for OpenStack)](./docs/install-vagrant.md) * [Docker Deployment Guide](./docs/install-docker.md) + +There are other deprecated deployment guides in `docs`, but there are no guarantees that these still function: + +* [Vagrant Deployment Guide (with instructions for OpenStack)](./docs/install-vagrant.md) * [Kubernetes Deployment Guide](./docs/install-kubernetes.md) * [Tox Deployment Guide](./docs/install-tox.md) * [Authentication and Authorization Deployment Guide](./docs/authx-setup.md) View additional Makefile options with `make help`. - ## Services and Components ### Add new service diff --git a/docs/ingest-and-test.md b/docs/ingest-and-test.md new file mode 100644 index 000000000..946366a10 --- /dev/null +++ b/docs/ingest-and-test.md @@ -0,0 +1,125 @@ +# Testing local installation + +These instructions will lead you through some basic functionality tests, ingesting some sample data, and running some tests of the data services to make sure your local installation is working as expected. + +## Initial tests + +Check that you can see the data portal in your browser at `http://docker.localhost:5080/`. If not, you may need to follow the instructions in the [Docker Deployment Guide](./install-docker.md) + +Check that you can generate a bearer token for user2 with the following call, substituting usernames, secrets and passwords from `tmp/secrets/keycloak-test-user2`, `tmp/secrets/keycloak-client-local_candig-secret` and `tmp/secrets/keycloak-test-user2-password`. + +``` +## user2 bearer token +curl -X "POST" "http://docker.localhost:8080/auth/realms/candig/protocol/openid-connect/token" \ + -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \ + --data-urlencode "client_id=local_candig" \ + --data-urlencode "client_secret=" \ + --data-urlencode "grant_type=password" \ + --data-urlencode "username=user2" \ + --data-urlencode "password=" \ + --data-urlencode "scope=openid" +``` + +Doing much else will require test data. + +## Setup Federation Service + +Federation service is required to run most of CanDig operations. The following example will add 1 UHN node to simulate the network calls. + +- add `federation-service` to the list of `CANDIG_MODULES` in .env +- add to /tmp/federation/ the file `servers.json` + +``` +{ + "servers": [ + { + "url": "http://docker.localhost:4232/federation/search", + "location": [ + "UHN", + "Ontario", + "ca-on" + ] + } + ] +} +``` + +and `services.json` + +``` +{ + "services": { + "katsu": "http://docker.localhost:5080/katsu", + "candig-server": "http://docker.localhost:5080/candig", + "htsget-app": "http://docker.localhost:5080/genomics" + } +} +``` +If you already have federation-service running, delete the container then run +`make build-federation-service` and `make compose-federation-service` to recreate it. + +## Install test data + +Clone the [candig-ingest](https://github.com/CanDIG/candigv2-ingest) repo: + +``` +https://github.com/CanDIG/candigv2-ingest.git +``` + +Create a virtual environment named `.venv`: + +``` +# Linux +sudo apt-get install python3-venv # If needed +python3 -m venv .venv +source .venv/bin/activate + +# macOS +python3 -m venv .venv +source .venv/bin/activate + +# Windows +py -3 -m venv .venv +.venv\scripts\activate +``` + +Install the requirements: + +``` +pip install -r requirements.txt +``` + +Generate a file env.sh in the `candig2-ingest` repo: + +``` +python settings.py ../CanDIGv2/ +source env.sh +``` + +Create opa dataset policy based on the dataset name and the email +address of the user (for example, dataset_name=SYNTHETIC_1 and user_name=user2@test.ca) + +``` +python opa_ingest.py --dataset --user > access.json +``` + +Copy the access.json file to the docker: + +``` +docker cp access.json candigv2_opa_1:/app/permissions_engine/access.json +``` + +Get the `Synthetic_Clinical_Data.json` file from @daisieh (Daisie Huang) and copy it into the root folder in the docker container: + +``` +docker cp '/local_path_to_file/Synthetic_Clinical_Data.json' candigv2_chord-metadata_1:/Synthetic_Clinical_Data.json +``` + +Then run ingest command (katsu and federation should be running): + +``` +python katsu_ingest.py --dataset --input /Synthetic_Clinical_Data.json +``` + +## Test data services +To be continue diff --git a/docs/install-docker.md b/docs/install-docker.md index 5cbab603e..87abd4f51 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -1,6 +1,9 @@ # CanDIGv2 Install Guide --- +These instructions work for server deployments or local linux deployments. For local OSX using M1 architecture, follow the [Mac Apple Silicon Installation](#mac-apple-silicon-installation) instructions at the bottom of this file. For WSL you can follow the linux instructions and follow WSL instructions for hosts file at [update hosts](#update-hosts). + +Before beginning, you should set up your environment variables as described in the [README](README.md). ## Install OS Dependencies @@ -110,7 +113,7 @@ make init-conda ## Choose Docker Deployment Strategy -We provide instructions below for two different docker deployment strategies. Option 1 uses `docker-compose` to deploy each module. Option 2 builds a Docker Swarm cluster using `docker-machine`. We use Option 2 for production, but Option 1 is simpler for local dev installation. +We provide instructions below for two different docker deployment strategies. Option 1 uses `docker-compose` to deploy each module. Option 2 builds a Docker Swarm cluster using `docker-machine`. We use Option 2 for production, but Option 1 is simpler for local dev installation. It may be necessary to configure your Hosts file before proceeding (i.e. if you run into an error during `make init-authx`), check the [Hosts documentation](#update-hosts). ### Option 1: Deploy CanDIGv2 Services with Compose @@ -128,7 +131,7 @@ make docker-pull # deploy stack make compose -make init-authx +make init-authx # If this command fails, try the #update-hosts section of this Markdown file # TODO: post deploy auth configuration # (optional) push updated images to $DOCKER_REGISTRY @@ -191,13 +194,35 @@ make stack ## Update hosts -Get your local IP address and edit your /etc/hosts file to add: +Get your local IP address and edit your `/etc/hosts` file to add (note that the key and value are tab-delimited): ```bash docker.localhost auth.docker.localhost ``` +After saving your hosts file, make sure you reset the auth stack before retrying: +```bash +make clean-authx +make init-authx +``` + +If the command still fails, it may be necessary to disable your local firewall, or edit it to allow requests from all ports used in the Docker stack. + +Example (Ubuntu): +Go to your `.env` file and write down the IP addresses for DOCKER_BRIDGE_IP and DOCKER_GWBRIDGE_IP. + +Edit your firewall settings to allow connections from those adresses: +```bash +sudo ufw allow from to +sudo ufw allow from to +``` + +Re-run `make clean-authx` and `make init-authx` and it should work. + +### WSL +Edit your /etc/hosts file as stated above along with your Windows hosts file by adding your Windows IPv4 to both hosts files. This can be found at `C:\Windows\system32\drivers\etc`. How you edit this file will change between versions of Windows. + ## Cleanup CanDIGv2 Compose/Swarm Environment Use the following steps to clean up running CanDIGv2 services in a docker-compose configuration. Note that these steps are destructive and will remove **ALL** containers, secrets, volumes, networks, certs, and images. If you are using docker in a shared environment (i.e. with other non-CanDIGv2 containers running) please consider running the cleanup steps manually instead. @@ -267,10 +292,6 @@ cp -i etc/env/example.env .env - Edit the .env file: ```bash -# options are [, , host.docker.internal, docker.localhost] -CANDIG_DOMAIN=docker.localhost -CANDIG_AUTH_DOMAIN=docker.localhost -... # options are [linux, darwin, arm64mac] VENV_OS=arm64mac VENV_NAME=candig @@ -322,7 +343,7 @@ ifconfig -l | xargs -n1 ipconfig getifaddr sudo nano /etc/hosts ``` -- Add the IP address to the end of the file so it look like this: +- Add the IP address to the end of the file so it look like this (noting that the key and value need to be tab-delimited): ```bash # Other settings @@ -331,14 +352,6 @@ sudo nano /etc/hosts ### Step 6: Create Auth Stack -In the .env, comment out all the `WES_OPT+=…` (We don't use it right now) - -```bash -# WES_OPT=--opt=extra=--batchSystem=Mesos -... -# WES_OPT+=--opt=extra=--metrics -``` - The old keycloak image (15.0.0) is not compatible with M1, so we need to upgrade it. Go to `lib/keycloak/docker-compose.yml` and replace the `- BASE_IMAGE=candig/keycloak:${KEYCLOAK_VERSION}` with one of the following: @@ -355,16 +368,3 @@ Then run `make`: make init-authx ``` -If you got this error: - -```bash -Getting keycloak token -Traceback (most recent call last): - File "", line 1, in -KeyError: 'access_token' -make: *** [init-authx] Error 1 -``` - -Then try to replace all the `keycloak` passwords in `tmp/secrets` with something simple like `thisisasupersecretpassword`, basically no special chars. - -Try `make clean-authx` and `make init-authx` and it should worked 🎉 From 0ef9bd7cee990e24bb6c1da92731a591a62f555e Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 30 Nov 2022 12:03:19 -0800 Subject: [PATCH 121/236] DIG-931: Vault aws policy needs update permissions (#180) * aws policy needs update permissions * env var in case it's needed --- lib/htsget-server/docker-compose.yml | 1 + lib/vault/vault_setup.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index 5337dd748..46c14eb84 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -44,6 +44,7 @@ services: HTSGET_TEST_KEY: "hoodlebug" HTSGET_URL: ${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} OPA_URL: ${OPA_PRIVATE_URL} + OPA_SITE_ADMIN_KEY: ${OPA_SITE_ADMIN_KEY} # only required if not equal to "site_admin" CANDIG_AUTH: ${CANDIG_AUTHORIZATION} VAULT_URL: ${VAULT_SERVICE_URL} DEBUG_MODE: ${CANDIG_DEBUG_MODE} diff --git a/lib/vault/vault_setup.sh b/lib/vault/vault_setup.sh index 1e19e7b4f..28d3e1f42 100755 --- a/lib/vault/vault_setup.sh +++ b/lib/vault/vault_setup.sh @@ -93,7 +93,7 @@ docker exec $vault sh -c "echo 'path \"identity/oidc/token/*\" {capabilities = [ echo echo ">> setting up aws policy" -docker exec $vault sh -c "echo 'path \"aws/*\" {capabilities = [\"create\", \"read\"]}' >> vault-policy.hcl; vault policy write aws vault-policy.hcl" +docker exec $vault sh -c "echo 'path \"aws/*\" {capabilities = [\"create\", \"update\", \"read\"]}' >> vault-policy.hcl; vault policy write aws vault-policy.hcl" # user claims echo From d8f8969922eb9e125648233e98b0840a5cc05518 Mon Sep 17 00:00:00 2001 From: Karen Cranston Date: Wed, 30 Nov 2022 15:46:56 -0500 Subject: [PATCH 122/236] Remove candig server and update module list (#181) * remove candig-server from default module list * update minimal and prod modules in readme --- README.md | 4 ++-- etc/env/example.env | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 491e849d2..f356180e1 100644 --- a/README.md +++ b/README.md @@ -67,13 +67,13 @@ Not all CanDIG modules are required for a minimal installation. The `CANDIG_MODU By default (if you copy the sample file from `etc/env/example.env`) the installation includes the minimal list of modules: - CANDIG_MODULES=minio htsget-server chord-metadata candig-server candig-data-portal + CANDIG_MODULES=minio htsget-server chord-metadata candig-data-portal Optional modules follow the `#` and include federation service, various monitoring components, workflow execution, and some older modules not generally installed. For federated installations, you will need `federation-service`. -For production deployments, you will probably want _add prod modules here_. +For production deployments, you will probably want to include `federation-service weavescope logging monitoring`. Be aware that the last three require more resources, includeing storage. Authorization and authentication modules defined in `CANDIG_AUTH_MODULES` are only installed if you run `make init-authx` during deployment. diff --git a/etc/env/example.env b/etc/env/example.env index 992b49a3f..c04e01f96 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=minio htsget-server chord-metadata candig-server candig-data-portal #drs-server wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget federation-service +CANDIG_MODULES=minio htsget-server chord-metadata candig-data-portal #drs-server wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget federation-service CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] From 4c784e39fb840d90567b3cce4a5c774086e26dac Mon Sep 17 00:00:00 2001 From: Courtney <37649170+CourtneyGosselin@users.noreply.github.com> Date: Fri, 2 Dec 2022 10:48:40 -0800 Subject: [PATCH 123/236] Add documentation for Docker and submodules (#182) * Add documentation for submodules Signed-off-by: Courtney Gosselin * Add docker and submodule documentation Signed-off-by: Courtney Gosselin * add links to post-install docs * add documentation for finding module name Signed-off-by: Courtney Gosselin Co-authored-by: Karen Cranston --- docs/docker-and-submodules.md | 90 +++++++++++++++++++++++++++++++++++ docs/install-docker.md | 4 ++ 2 files changed, 94 insertions(+) create mode 100644 docs/docker-and-submodules.md diff --git a/docs/docker-and-submodules.md b/docs/docker-and-submodules.md new file mode 100644 index 000000000..eed7f4c9f --- /dev/null +++ b/docs/docker-and-submodules.md @@ -0,0 +1,90 @@ +# Docker Syncing in CanDIGv2 + +## Docker +[Docker Getting Started](https://docs.docker.com/get-started/) + +[Docker Compose](https://docs.docker.com/compose/gettingstarted/) + +Each submodule will have a docker-compose.yml file and DockerFile + +## Our Docker Quirks + +### Syncing/Retrieving Submodule Changes + +For authx, tyk, and keycloak you need to run the clean command for your changes to be properly picked up and communicated between the modules. + +```bash +make clean-authx +make init-authx +``` + +After making your change inside your submodules repo (example: a title change in candig-data-portal) you should be able to do the following + +Current directory post changes **.../CanDIGv2/lib/module/module** + +```bash +git status # show change in submodule +cd ../../.. # change directory to CanDIGv2 +docker ps # see the name of your submodule +make build-% # % represents the name of your module +make compose-% # % represents the name of your module +``` + +You can find the name of the module from the `NAMES` column in the output from `docker ps`. + +After you run these commands you should see your changes are now live. If you were making changes for candig-data-portal you will now see these on the web browser. + +If you want to understand how these commands are working you can find their source code in the MakeFile in CanDIGv2. + +## Submodules + +The below link should give you a better understanding of submodules. + +[Submodules in Git](https://git-scm.com/book/en/v2/Git-Tools-Submodules) + +### Viewing all submodules + +Current Directory **.../CanDIGv2** + +```bash +cd lib +git submodule +``` + +### Status Changes to Submodules + +If you wanted to see all the submodules you made changes to you could run the following. + +Current Directory **.../CanDIGv2** +```bash +git status +``` + +Example output +```bash +❯ git status ─╯ +On branch develop +Your branch is up to date with 'origin/develop'. + +Changes not staged for commit: + (use "git add ..." to update what will be committed) + (use "git checkout -- ..." to discard changes in working directory) + (commit or discard the untracked or modified content in submodules) + + modified: lib/candig-data-portal/candig-data-portal (new commits, modified content) + modified: lib/chord-metadata/chord_metadata_service (new commits) + modified: lib/federation-service/federation_service (new commits, modified content) + modified: lib/htsget-server/htsget_app (new commits) + modified: lib/opa/opa (new commits) + +Untracked files: + (use "git add ..." to include in what will be committed) + + docs/docker-and-syncing.md + +no changes added to commit (use "git add" and/or "git commit -a") +``` + +This will allow you to see all the changes in your submodules under lib/ as well as any changes you made to CanDIGv2. + +You can also go into each submodules individual repo to view its specific changes. diff --git a/docs/install-docker.md b/docs/install-docker.md index 87abd4f51..aef3a4daa 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -368,3 +368,7 @@ Then run `make`: make init-authx ``` +Once everything has run without errors, take a look at the documentation for +[ingesting data and testing the deployment](docs/ingest-and-test.md) as well as +[how to modify code and test changes](docs/docker-and-submodules.md) in +the context of the CanDIG stack. From e56a8761b8285b50929908e29495b0173482ccbf Mon Sep 17 00:00:00 2001 From: Karen Cranston Date: Tue, 6 Dec 2022 10:31:58 -0500 Subject: [PATCH 124/236] Update install-docker.md Fix links to other files in docs dir. --- docs/install-docker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install-docker.md b/docs/install-docker.md index aef3a4daa..a6c33f83a 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -369,6 +369,6 @@ make init-authx ``` Once everything has run without errors, take a look at the documentation for -[ingesting data and testing the deployment](docs/ingest-and-test.md) as well as -[how to modify code and test changes](docs/docker-and-submodules.md) in +[ingesting data and testing the deployment](ingest-and-test.md) as well as +[how to modify code and test changes](docker-and-submodules.md) in the context of the CanDIG stack. From 643b3d7feafdfa8cd985da4c84126960f82fcd56 Mon Sep 17 00:00:00 2001 From: Courtney <37649170+CourtneyGosselin@users.noreply.github.com> Date: Tue, 6 Dec 2022 09:38:28 -0800 Subject: [PATCH 125/236] Documentation for WSL federation configuration (#183) * Documentation for WSL federation configuration * Adding dropdown to WSL section * Change wording * Add WSL information to only one file Signed-off-by: Courtney Gosselin --- docs/configure-federation.md | 3 ++ docs/ingest-and-test.md | 71 +++++++++++++++++++++++++++++++----- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/docs/configure-federation.md b/docs/configure-federation.md index 34894476c..4946e0a02 100644 --- a/docs/configure-federation.md +++ b/docs/configure-federation.md @@ -27,6 +27,9 @@ Services need to follow a consistent naming pattern so consult with federation s Once these files are set, you can add `federation-service` to the list of `CANDIG_MODULES` in `.env` and then run `make docker-pull` or `make build-federation-service` if you want to either pull the latest federation-service image matching `FEDERATION_VERSION` or build it from source. After this, you can run `make compose-federation-service` to deploy the federation-service in the CanDIGv2 stack. You can test the service by folloring the testing steps provided in `/lib/federation-service/federation_service/README.md`. +## WSL Federation Configuration Errors +refer to [ingesting data and testing the deployment](docs/ingest-and-test.md) for errors starting up federation in WSL + ## Running Federation Service Behind Tyk Once the federation-service is running, you will need to update your tyk configuration templates in order to allow other federation servers to peer with each other. To do this, you will need to add the `issuer` and `client_ids` of the trusted nodes into any of the `api_*.json.tpl` files in `/lib/tyk/configuration_templates/`. This must be done for each service defined in `services.json` that you want federation peer(s) to access. For example, if you wanted to allow the UHN CanDIGv2 node to make federated searches to `katsu` data service, you would need to modify the `/lib/tyk/configuration_templates/api_katsu_chord.json.tpl` file and change the `providers` section to: diff --git a/docs/ingest-and-test.md b/docs/ingest-and-test.md index 946366a10..d8df80336 100644 --- a/docs/ingest-and-test.md +++ b/docs/ingest-and-test.md @@ -8,7 +8,7 @@ Check that you can see the data portal in your browser at `http://docker.localho Check that you can generate a bearer token for user2 with the following call, substituting usernames, secrets and passwords from `tmp/secrets/keycloak-test-user2`, `tmp/secrets/keycloak-client-local_candig-secret` and `tmp/secrets/keycloak-test-user2-password`. -``` +```bash ## user2 bearer token curl -X "POST" "http://docker.localhost:8080/auth/realms/candig/protocol/openid-connect/token" \ -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \ @@ -29,7 +29,7 @@ Federation service is required to run most of CanDig operations. The following e - add `federation-service` to the list of `CANDIG_MODULES` in .env - add to /tmp/federation/ the file `servers.json` -``` +```json { "servers": [ { @@ -46,7 +46,7 @@ Federation service is required to run most of CanDig operations. The following e and `services.json` -``` +```json { "services": { "katsu": "http://docker.localhost:5080/katsu", @@ -55,9 +55,62 @@ and `services.json` } } ``` + If you already have federation-service running, delete the container then run `make build-federation-service` and `make compose-federation-service` to recreate it. +## WSL Federation Configuration Errors + +
+WSL Errors + +```bash +Creating candigv2_federation-service_1 ... error + +ERROR: for candigv2_federation-service_1 Cannot create container for service federation-service: not a directory + +ERROR: for federation-service Cannot create container for service federation-service: not a directory +ERROR: Encountered errors while bringing up the project. +make: *** [Makefile:378: compose-federation-service] Error 1 +``` +If you are seeing the above directory not found error in WSL it is a issue with the communication between WSL and Windows docker in relation to the tmp folder. To get past this you need to do the following: + +Current directory: **../CanDIGv2** +```bash +#Copy the servers.json and services.json into the config folder instead: +cp tmp/federation/* lib/federation-service/federation_service/configs +``` +You will need to comment out the 'secrets' section in the lib/federation-service/docker-compose.yml file. It will look like the code chunk below: + +```yml + ... + logging: *default-logging + # secrets: + # - source: federation-servers + # target: /app/federation_service/configs/servers.json + # - source: federation-services + # target: /app/federation_service/configs/services.json + entrypoint: ["uwsgi", "federation.ini", "--http", "0.0.0.0:4232"] +``` +Start the federation-service up: +```bash +make build-federation-service +make compose-federation-service +``` +To check that it is running you can look at the candigv2_federation-service_1 container in your Window Docker GUI. You can also run the following in terminal: +```bash +curl http://docker.localhost:4232/federation/services +``` +The below is an example of what will return it should be what is in your services.json +```json +{ + "candig-server": "http://docker.localhost:5080/candig", + "htsget-app": "http://docker.localhost:5080/genomics", + "katsu": "http://docker.localhost:5080/katsu" +} +``` +
+ ## Install test data Clone the [candig-ingest](https://github.com/CanDIG/candigv2-ingest) repo: @@ -68,7 +121,7 @@ https://github.com/CanDIG/candigv2-ingest.git Create a virtual environment named `.venv`: -``` +```bash # Linux sudo apt-get install python3-venv # If needed python3 -m venv .venv @@ -91,7 +144,7 @@ pip install -r requirements.txt Generate a file env.sh in the `candig2-ingest` repo: -``` +```bash python settings.py ../CanDIGv2/ source env.sh ``` @@ -99,25 +152,25 @@ source env.sh Create opa dataset policy based on the dataset name and the email address of the user (for example, dataset_name=SYNTHETIC_1 and user_name=user2@test.ca) -``` +```bash python opa_ingest.py --dataset --user > access.json ``` Copy the access.json file to the docker: -``` +```bash docker cp access.json candigv2_opa_1:/app/permissions_engine/access.json ``` Get the `Synthetic_Clinical_Data.json` file from @daisieh (Daisie Huang) and copy it into the root folder in the docker container: -``` +```bash docker cp '/local_path_to_file/Synthetic_Clinical_Data.json' candigv2_chord-metadata_1:/Synthetic_Clinical_Data.json ``` Then run ingest command (katsu and federation should be running): -``` +```bash python katsu_ingest.py --dataset --input /Synthetic_Clinical_Data.json ``` From abd9dc4b86ea75100a4fd04003ec5ad45bb60b7e Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:01:05 -0500 Subject: [PATCH 126/236] cleanup of docs (#186) Co-authored-by: Shaikh Rashid --- docs/examples.md | 119 ------------------------------- docs/install-docker.md | 139 +++++++------------------------------ docs/install-kubernetes.md | 34 --------- docs/install-tox.md | 81 --------------------- docs/install-vagrant.md | 41 ----------- 5 files changed, 26 insertions(+), 388 deletions(-) delete mode 100644 docs/examples.md delete mode 100644 docs/install-kubernetes.md delete mode 100644 docs/install-tox.md delete mode 100644 docs/install-vagrant.md diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index f440bd214..000000000 --- a/docs/examples.md +++ /dev/null @@ -1,119 +0,0 @@ -# Notes/Examples for Services - -- - - - -## `mc` Client Examples - -```bash - -# Example - Minio Cloud Storage -mc config host add minio http://192.168.1.51 BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 - -# Example - Amazon S3 Cloud Storage -mc config host add s3 https://s3.amazonaws.com BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 - -``` - -## Data Repository Service Schemas - -```bash - -wget http://hgdownload.cse.ucsc.edu/goldenPath/hg38/chromosomes/chr22.fa.gz - -md5sum chr22.fa.gz - -# 41b47ce1cc21b558409c19b892e1c0d1 chr22.fa.gz - -curl -X POST -H 'Content-Type: application/json' \ - --data '{"data_object": - {"id": "hg38-chr22", - "name": "Human Reference Chromosome 22", - "checksums": [{"checksum": "41b47ce1cc21b558409c19b892e1c0d1", "type": "md5"}], - "urls": [{"url": "http://hgdownload.cse.ucsc.edu/goldenPath/hg38/chromosomes/chr22.fa.gz"}], - "size": "12255678"}}' http://localhost:8080/ga4gh/dos/v1/dataobjects - -# We can then get the newly created Data Object by id -curl http://localhost:8080/ga4gh/dos/v1/dataobjects/hg38-chr22 - -# Or by checksum! -curl -X GET http://localhost:8080/ga4gh/dos/v1/dataobjects -d checksum=41b47ce1cc21b558409c19b892e1c0d1 - -``` - -## HTSGET Client/Server Tools - -![HTSNexus Core Mechanics Diagram](https://raw.githubusercontent.com/wiki/dnanexus-rnd/htsnexus/htsnexus_core_mechanic.png) - -The htsnexus client tool simply emits BAM/CRAM/VCF to standard output, which can be redirected to a file or piped into samtools/bcftools. It delivers a well-formed BAM/CRAM/VCF file, with the proper header, even when slicing a genomic range. Here are the data accessions currently available: - -| namespace | accession | format | -| --- | --- | --- | -| **platinum**
Illumina Platinum Genomes stored at EBI | NA12877 NA12878 NA12879 NA12881 NA12882 NA12883 NA12884 NA12885 NA12886 NA12887 NA12888 NA12889 NA12890 NA12891 NA12892 NA12893 | BAM | -| **ENCODE**
ChIP-seq data released by the ENCODE DCC in Jan 2016 | ENCFF014ABI ENCFF024MPE ENCFF070QUN ENCFF090MZL ENCFF124VCI ENCFF137WND ENCFF180VYU ENCFF308BKD ENCFF373VCV ENCFF465GPJ ENCFF572JRO ENCFF630NYB ENCFF743FRI ENCFF800DAY ENCFF862PIC ENCFF866OLR ENCFF904PIO ENCFF929AIJ ENCFF946BKE ENCFF951SEJ | BAM | -| **lh3bamsvr**
Heng's examples | EXA00001 EXA00002 | BAM | -| **1000genomes_low_coverage**
Low-coverage whole-genome sequencing from the 1000 Genomes Project | 2,535 individual accessions (example usage above) | BAM, CRAM | -| **1000genomes**
1000 Genomes Project variant calls | 20130502_autosomes | VCF - -## `htnexus.py` Client Examples - -```bash - -# Download an ENCODE ChIP-Seq BAM -htsnexus ENCODE ENCFF904PIO > ENCFF904PIO.bam - -# Slice a genomic range out of a Platinum Genomes BAM -htsnexus -r chr12:111766922-111817529 platinum NA12878 > NA12878_ALDH2.bam - -# Count reads on chr21 in a 1000 Genomes BAM -htsnexus -r 21 1000genomes_low_coverage NA20276 | samtools view -c - - -# id. with CRAM (samtools 1.2+ needed) -htsnexus -r 21 1000genomes_low_coverage HG01102 CRAM | samtools view -c - - -# Stream reads from Heng Li's bamsvr and display as headered SAM -htsnexus -r 11:10899000-10900000 lh3bamsvr EXA00001 | samtools view -h - | less -S - -# Slice a bgzipped VCF -htsnexus -r 12:112204691-112247789 1000genomes 20130502_autosomes vcf | gzip -dc | grep rs671 | cut -f1-16 - -``` - -### Use `htsnexus` to slice genomic range - -```bash - -$ ./htsnexus -v -r chr12:111766922-111817529 platinum NA12878 | wc -c -Query URL: http://htsnexus.rnd.dnanex.us/v1/reads/platinum/NA12878?format=BAM&referenceName=chr12&start=111766922&end=111817529 -Response: { - "urls": [ - { - "url": "data:application/octet-stream;base64,[704 base64 characters]" - }, - { - "url": "https://dl.dnanex.us/F/D/8P6zFPZ0fy5z20bJzy32jbG4165F54Fv5fZFbzpK/NA12878_S1.bam", - "headers": { - "range": "bytes=81272945657-81275405960", - "referer": "http://htsnexus.rnd.dnanex.us/v1/reads/platinum/NA12878?format=BAM&referenceName=chr12&start=111766922&end=111817529" - } - }, - { - "url": "data:application/octet-stream;base64,[40 base64 characters]" - } - ], - "namespace": "platinum", - "accession": "NA12878", - "reference": "hg19", - "format": "BAM" -} -Piping: ['curl', '-LSs', '-H', 'range: bytes=81272945657-81275405960', '-H', 'referer: http://htsnexus.rnd.dnanex.us/v1/reads/platinum/NA12878?format=BAM&referenceName=chr12&start=111766922&end=111817529', 'https://dl.dnanex.us/F/D/8P6zFPZ0fy5z20bJzy32jbG4165F54Fv5fZFbzpK/NA12878_S1.bam'] -Success -2460858 - -``` - -## References - -* [Minio Client Quickstart](https://docs.minio.io/docs/minio-client-quickstart-guide#add-a-cloud-storage-service) -* [GA4GH DRS Schemas](https://github.com/ga4gh/data-repository-service-schemas) -* [GA4GH DOS Server Quickstart](https://data-object-service.readthedocs.io/en/latest/quickstart.html) -* [HTSNexus](https://github.com/dnanexus-rnd/htsnexus) diff --git a/docs/install-docker.md b/docs/install-docker.md index a6c33f83a..7cd5062f8 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -3,7 +3,7 @@ --- These instructions work for server deployments or local linux deployments. For local OSX using M1 architecture, follow the [Mac Apple Silicon Installation](#mac-apple-silicon-installation) instructions at the bottom of this file. For WSL you can follow the linux instructions and follow WSL instructions for hosts file at [update hosts](#update-hosts). -Before beginning, you should set up your environment variables as described in the [README](README.md). +Before beginning, you should set up your environment variables as described in the [README](README.md). ## Install OS Dependencies @@ -107,15 +107,23 @@ git submodule update --init --recursive cp -i etc/env/example.env .env # 3. fetch binaries and initialize candig virtualenv -make bin-all +make bin-conda make init-conda ``` -## Choose Docker Deployment Strategy + ## Update hosts -We provide instructions below for two different docker deployment strategies. Option 1 uses `docker-compose` to deploy each module. Option 2 builds a Docker Swarm cluster using `docker-machine`. We use Option 2 for production, but Option 1 is simpler for local dev installation. It may be necessary to configure your Hosts file before proceeding (i.e. if you run into an error during `make init-authx`), check the [Hosts documentation](#update-hosts). +Get your local IP address and edit your `/etc/hosts` file to add (note that the key and value are tab-delimited): + +```bash + docker.localhost + auth.docker.localhost +``` + +### WSL +Edit your /etc/hosts file as stated above along with your Windows hosts file by adding your Windows IPv4 to both hosts files. This can be found at `C:\Windows\system32\drivers\etc`. How you edit this file will change between versions of Windows. -### Option 1: Deploy CanDIGv2 Services with Compose +## Deploy CanDIGv2 Services with Compose The `init-docker` command will initialize CanDIGv2 and set up docker networks, volumes, configs, secrets, and perform other miscellaneous actions needed before deploying a CanDIGv2 stack. Running `init-docker` will override any previous configurations and secrets. @@ -123,9 +131,6 @@ The `init-docker` command will initialize CanDIGv2 and set up docker networks, v # initialize docker environment make init-docker -# (optional) create images -make images - # pull latest CanDIGv2 images (if you didn't create images locally) make docker-pull @@ -134,96 +139,19 @@ make compose make init-authx # If this command fails, try the #update-hosts section of this Markdown file # TODO: post deploy auth configuration -# (optional) push updated images to $DOCKER_REGISTRY -docker login -make docker-push ``` -## Option 2: Deploy CanDIGv2 using Docker Swarm - -### Create CanDIGv2 Development VM - -Using the provided steps will help to create a `docker-machine` cluster on VirtualBox. The `make` CLI can also be used to provision and connect a multi-vm Swarm cluster. Users are encouraged to use this docker environment for CanDIGv2 development as it provides an isolated domain from the host environment, increasing security and reducing conflicts with host processes. Modify the `MINIKUBE_*` options in `.env`, then launch a single-node or multi-node `docker-machine` with `make machine-$vm_name`, where `$vm_name` is a unique vm name. - -To build a development swarm cluster run the following: - -- create a swarm manager with `make machine-manager`, additional nodes with `make machine-manager2`... -- create a swarm worker with `make machine-worker`, additional nodes with `make machine-worker2`... - -To switch your local docker-client to use `docker-machine`, run `eval $(bin/docker-machine env manager)`. Add this line into `bashrc` with `bin/docker-machine env manager >> $HOME/.bashrc` in order to set `docker-machine` as the default `$DOCKER_HOST` for all shells. - -### Initialize CanDIGv2 (Docker) - -The following commands will initialize CanDIGv2 and set up docker networks, volumes, configs, secrets, and perform other miscellaneous actions needed before deploying a CanDIGv2 stack. Only perform these actions once as it will override any previous configurations and secrets. Once completed, you can deploy a Compose or Swarm stack. - -```bash -# initialize docker environment -make init-docker -``` - -### Deploy using Swarm - -> Note: swarm deployment requires minimum 2 nodes connected (1 manager, 1 worker) - -1. Create initial manager node - -```bash -eval $(bin/docker-machine env manager) -make init-swarm -``` - -2. Add additional manager/worker nodes - -```bash -# set the SWARM_MODE and SWARM_MANAGER_IP in .env -eval $(bin/docker-machine env worker) -make swarm-join -``` - -3. Deploy CanDIGv2 stack on the docker swarm - -```bash -eval $(bin/docker-machine env manager) - -# check cluster status (READY:ACTIVE) -docker node ls - -# deploy CanDIGv2 services -make stack -``` - -## Update hosts - -Get your local IP address and edit your `/etc/hosts` file to add (note that the key and value are tab-delimited): - -```bash - docker.localhost - auth.docker.localhost -``` - -After saving your hosts file, make sure you reset the auth stack before retrying: -```bash -make clean-authx -make init-authx -``` - -If the command still fails, it may be necessary to disable your local firewall, or edit it to allow requests from all ports used in the Docker stack. - -Example (Ubuntu): -Go to your `.env` file and write down the IP addresses for DOCKER_BRIDGE_IP and DOCKER_GWBRIDGE_IP. +If the command still fails, it may be necessary to disable your local firewall, or edit it to allow requests from all ports used in the Docker stack. Edit your firewall settings to allow connections from those adresses: ```bash -sudo ufw allow from to -sudo ufw allow from to +export DOCKER_BRIDGE_IP=$(docker network inspect bridge | grep Subnet | awk '{print $2}' | tr -d ',') +sudo ufw allow from $DOCKER_BRIDGE_IP to ``` Re-run `make clean-authx` and `make init-authx` and it should work. -### WSL -Edit your /etc/hosts file as stated above along with your Windows hosts file by adding your Windows IPv4 to both hosts files. This can be found at `C:\Windows\system32\drivers\etc`. How you edit this file will change between versions of Windows. - -## Cleanup CanDIGv2 Compose/Swarm Environment +## Cleanup CanDIGv2 Compose Environment Use the following steps to clean up running CanDIGv2 services in a docker-compose configuration. Note that these steps are destructive and will remove **ALL** containers, secrets, volumes, networks, certs, and images. If you are using docker in a shared environment (i.e. with other non-CanDIGv2 containers running) please consider running the cleanup steps manually instead. @@ -231,7 +159,6 @@ The following steps are performed by `make clean-all`: ```bash # 1. stop and remove running stacks -make clean-stack make clean-compose # 2. stop and remove remaining containers @@ -239,34 +166,21 @@ make clean-containers # 3. remove all configs/secrets from docker and local dir make clean-secrets -make clean-configs # 4. remove all docker volumes and local data dir make clean-volumes -# 5. remove all unused networks -make clean-networks - -# 6. delete all cached images +# 5. delete all cached images make clean-images -# 7. leave swarm-cluster -make clean-swarm - -# 8. destroy all docker-machine instances -make clean-machines - -# 9. remove selfsigned-certs (including root-ca) -make clean-certs - -# 10. remove conda environment +# 6. remove conda environment make clean-conda -# 11. remove bin dir (inlcuding miniconda) +# 7. remove bin dir (inlcuding miniconda) make clean-bin ``` -# Mac Apple Silicon Installation +## Mac Apple Silicon Installation ### 1) Step1: Install OS Dependencies @@ -301,7 +215,6 @@ VENV_NAME=candig ```bash # 3. fetch binaries and initialize candig virtualenv -make bin-all make init-conda ``` @@ -358,7 +271,7 @@ Go to `lib/keycloak/docker-compose.yml` and replace the `- BASE_IMAGE=candig/key ```bash - BASE_IMAGE=mihaibob/keycloak:18.0.2-legacy # (from StackOverflow) -# or +# or - BASE_IMAGE=quay.io/c3genomics/keycloak:16.1.1.arm64 # (an alternative built on an M1, for an M1) ``` @@ -368,7 +281,7 @@ Then run `make`: make init-authx ``` -Once everything has run without errors, take a look at the documentation for -[ingesting data and testing the deployment](ingest-and-test.md) as well as -[how to modify code and test changes](docker-and-submodules.md) in -the context of the CanDIG stack. +Once everything has run without errors, take a look at the documentation for +[ingesting data and testing the deployment](ingest-and-test.md) as well as +[how to modify code and test changes](docker-and-submodules.md) in +the context of the CanDIG stack. diff --git a/docs/install-kubernetes.md b/docs/install-kubernetes.md deleted file mode 100644 index 1bf193a8d..000000000 --- a/docs/install-kubernetes.md +++ /dev/null @@ -1,34 +0,0 @@ -# CanDIGv2 Install Guide (Kubernetes) - -- - - - -## Create CanDIGv2 Minikube VM - -This method is still experimental but should be able to provide a means to convert existing CanDIGv2 `docker-compose.yml` into native kubernetes service definitions. Using the provided steps will help to create a dev minikube cluster where you can test kubernetes deployments. If the stack successfully deploys on the minikube vm, is stable, and passes all QA steps, it can be reasonably assumed that the `kompose` build will work with other kubernetes clusters (e.g. Azure AKS/Amazon EKS). - -The minikube CLI can also be used to provision a multi-vm cluster using the options in `.env`. Modify the `MINIKUBE_*` options in `.env`, then launch a single-node or multi-node kubernetes cluster with `make minikube`. Once minikube is running, set kubectl context to minikube with `$PWD/bin/minikube kubectl`. - -## Deploy CanDIGv2 Services (Kubernetes) - -```bash -# deploy kubernetes (if using minikube/kubernetes environment) -# requires running minikube vm or kubectl context for existing kubernetes cluster -make init-kubernetes - -# deploy CanDIGv2 services -make kubernetes -``` - -## Cleanup CanDIGv2 Kubernetes Environment - -```bash -# 1. stop and remove running kubernetes pods -make clean-kubernetes - -# 2. remove all secrets from kubernetes and local dir -make clean-secrets - -# 3. destroy minikube instances -make clean-minikube -``` - diff --git a/docs/install-tox.md b/docs/install-tox.md deleted file mode 100644 index 4c725912b..000000000 --- a/docs/install-tox.md +++ /dev/null @@ -1,81 +0,0 @@ -# CanDIGv2 Install Guide (Tox) - -- - - - -## Install Dependencies - -Since Tox is a Python package it is recommended to install it on a Python virtual environment using: - -```bash -pip install tox -``` - -Also python-dotenv with cli option should be installed on your virtual environment using: - -```bash -pip install python-dotenv[cli] -``` - -## Install CanDIG Dependencies - -1. Clone/pull latest CanDIGv2 repo from `https://github.com/CanDIG/CanDIGv2.git` - -2. Create/modify `.env` file -```bash -# Copy and Edit `.env` with your site's local configuration -cp -i etc/env/example.env .env -``` - -3. Initialize submodules -```bash -git submodule update --init --recursive -``` - -4. Create CanDIG Daemons - -```bash -# view helpful commands -make - -# run candig services in screen terminals -make tox - -# to run an individual service -make tox-service_name_here -``` - -## Add services to tox.ini file - -In order to add and run a new service, you must follow bellow steps: - -1. Add service as a submodule (Please note, if the service is from Pypi website, you may skip this step) - -```bash -git submodule add http://github.com/username/service_name libs/service_name_dir -``` - -2. Add service to ```tox.ini``` file using bellow sections: - -```ini -[testenv:new_service_name] -changedir = {toxinidir}/lib/service_name_dir/github_dir_name -; Set 'deps' only if the service has a requirement file -deps=-r{toxinidir}/lib/service_name_dir/github_dir_name/requirements.txt - -commands= - ; Under commands you may add the commands to install the service, bellow some examples. - python setup.py install - pip install service_name - ; And also commands to run the service, below some examples. - ; Note that your machine should have 'screen' installed - screen -dmS {envname} ./manage.py runserver {env:IP_FROM_ENV_FILE}:{env:PORT_FROM_ENV_FILE} - screen -dmS {envname} flask run --host {env:IP_FROM_ENV_FILE} --port {env:PORT_FROM_ENV_FILE} - -; If you need to execute, for instance, a Unix command, that should be added under 'whitelist_externals' section following below sample: -whitelist_externals= - mkdir - -; If the service requires a specific version of Python (let's say Python3.5) you may use 'basepython' section: -; Note that your machine must have Python 3.5 installed in order to execute this operation -basepython=python3.5 -``` diff --git a/docs/install-vagrant.md b/docs/install-vagrant.md deleted file mode 100644 index 407c78d4d..000000000 --- a/docs/install-vagrant.md +++ /dev/null @@ -1,41 +0,0 @@ -# CanDIGv2 Install Guide - -- - - - -[Vagrant](https://www.vagrantup.com) can be used to automate the process of setting up a [Docker deployment](install-docker.md) of CanDIGv2 on a virtual machine on your local machine or to create an instance on an [OpenStack deployment](https://www.openstack.org). - -## Set up Vagrant - -You'll need to have compatible versions of [Vagrant](https://www.vagrantup.com) and [VirtualBox](https://www.virtualbox.org) on the system you'll be using to deploy the Vagrant VM. We tested using Vagrant 2.0.3 and VirtualBox 5.2: other versions may not play nicely together or load the plugins correctly. - -If you're using a Debian or Ubuntu system, you can try running [setup_vagrant.sh](setup_vagrant.sh) to install the recommended versions. Otherwise, install the following: - -* [Vagrant 2.0.3](https://releases.hashicorp.com/vagrant/2.0.3/) ([installation instructions](https://www.vagrantup.com/docs/installation)) -* [VirtualBox 5.2.34](https://download.virtualbox.org/virtualbox/5.2.34/) ([installation instructions](https://www.virtualbox.org/manual/ch02.html)) - -Then install plugins: -``` -vagrant plugin install vagrant-disksize -vagrant plugin install vagrant-openstack-provider -vagrant plugin install vagrant-reload -``` -## Run Vagrant - -By default, `vagrant up` will use the VirtualBox provider to create a VM on your local machine. - -You can set the IP address of your virtual machine by setting the environment variable `CUSTOM_IP` (default: 192.168.33.33). - -You can also use Vagrant to deploy an instance on OpenStack: -* Get your credentials from your OpenStack dashboard: go to Project > API Access on the sidebar, then click the "Download OpenStack RC File" button and download the OpenStack RC File (Identity API v3). -* Either run the downloaded shell script to load the required environment variables, or export them into your shell directly. -* Export two additional environment variables: - * `OS_KEYPAIR` should correspond to a valid keypair in OpenStack Dashboard > Project > Compute > Key Pairs. Choose one that does not have a passphrase associated with it. - * `OS_PRIVATEKEY_PATH` should be the path of the private key associated with that keypair. -* Run `vagrant up --provider=openstack`. - -After it's up, you can access your VM with `vagrant ssh`. If you want to suspend the VM, `vagrant suspend` or `vagrant halt` ([what's the difference?](https://stackoverflow.com/questions/42549087/in-vagrant-which-is-better-out-of-halt-and-suspend#42551494)). - -## Cleanup - -To destroy your VM entirely, run `vagrant destroy`. - From f130723d6f17a5423709fbf8dc00213623cdceda Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Fri, 9 Dec 2022 12:13:29 -0500 Subject: [PATCH 127/236] Update docs (#187) * cleanup of docs * fix doc links * fix doc links Co-authored-by: Shaikh Rashid --- CanDIGv2-demo.ipynb | 825 -------------------------------------------- README.md | 25 +- 2 files changed, 8 insertions(+), 842 deletions(-) delete mode 100644 CanDIGv2-demo.ipynb diff --git a/CanDIGv2-demo.ipynb b/CanDIGv2-demo.ipynb deleted file mode 100644 index 92a41b39e..000000000 --- a/CanDIGv2-demo.ipynb +++ /dev/null @@ -1,825 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# CanDIGv2 Services Demo\n", - "- - -\n", - "\n", - "This notebook outlines how to use the demonstration server and client to make a simple Data Object service that makes available data from a few different sources using CanDIGv2. The CanDIGv2 project is a collection of heterogeneos services designed to work together to facilitate end to end dataflow for genomic data." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo Topology\n", - "\n", - "```\n", - "+---------------------------------------------------------------------------------------------+\n", - "| |\n", - "| +--------------+ |\n", - "| | candig.local | |\n", - "| +--------------+ |\n", - "| | |\n", - "| | |\n", - "| | +------------------------+ |\n", - "| +--------------------+ | consul:8300-8310 (tcp) | |\n", - "| +-----------------------+ | traefik:8000 (tcp) | | :8400 (tcp) | |\n", - "| | weavescope:4040 (tcp) |-----| :80 (tcp) |-------| :8500 (tcp) | |\n", - "| +-----------------------+ | :443 (tcp) | | :8301-8302 (udp) | |\n", - "| +--------------------+ | :8600 (udp) | |\n", - "| | +------------------------+ |\n", - "| | |\n", - "| | |\n", - "| +-------------------+ +--------------------+ +----------------------+ |\n", - "| | htsget:4844 (tcp) |-----| jupyter:8888 (tcp) |-------| ga4gh-drs:8080 (tcp) | |\n", - "| +-------------------+ +--------------------+ +----------------------+ |\n", - "| | |\n", - "| | |\n", - "| | |\n", - "| | |\n", - "| +-----------------------+ | +----------------------------------+ |\n", - "| | +------------------+ | | | +------------------------------+ | |\n", - "| | | minio:9000 (tcp) | | | | | toil-master/wes:5050 (tcp) | | |\n", - "| | +------------------+ | | | +------------------------------+ | |\n", - "| | +-------------------+ |-----------+----------| +------------------------------+ | |\n", - "| | | minio-client (mc) | | | | toil-worker:5051 (tcp) | | |\n", - "| | +-------------------+ | | +------------------------------+ | |\n", - "| +-----------------------+ +----------------------------------+ |\n", - "| |\n", - "+---------------------------------------------------------------------------------------------+\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Prerequisites\n", - "\n", - "If you are starting from a blank jupyter-lab instance, you won't have the ipython kernel needed for CanDIGv2. So let's create it. Copy/paste the following code into your **bash** shell:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cd /notebooks/demo/CanDIGv2\n", - "\n", - "conda env create -f etc/conda/environment.yml\n", - "conda activate candig\n", - "python -m ipykernel install --user --name candig" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It will be nice to keep a `bash` shell running along with out `iPyKernel`..." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Populate Minio Object Store with Buckets and Data\n", - "\n", - "We will need minio server/client with some demo data before we continue. Normally this is something we can do easily when using CanDIGv2 in `Swarm` or `Kubernetes` mode, but for a local `Compose` demo we need to take some extra steps.\n", - "\n", - "First, we need to copy over the `minio_acess_key` and `minio-secret-key` from our host. Copy/paste into the corresponding files or just run the following docker commands from the **host machine** with the keys:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "jp_container=$(docker ps | grep jupyter | awk '{print $1}')\n", - "docker cp minio-access-key $jp_container:/notebooks/demo/CanDIGv2/\n", - "docker cp minio-secret-key $jp_container:/notebooks/demo/CanDIGv2/" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, lets get the latest minio client in our env:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/notebooks/demo/CanDIGv2\n" - ] - } - ], - "source": [ - "# check we are in the project root directory\n", - "!pwd" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "mkdir -p /notebooks/demo/CanDIGv2/bin\n", - "curl -Lo /notebooks/demo/CanDIGv2/bin/minio \\\n", - "\thttps://dl.minio.io/server/minio/release/linux-amd64/minio\n", - " % Total % Received % Xferd Average Speed Time Time Time Current\n", - " Dload Upload Total Spent Left Speed\n", - "100 40.7M 100 40.7M 0 0 2518k 0 0:00:16 0:00:16 --:--:-- 3223k\n", - "curl -Lo /notebooks/demo/CanDIGv2/bin/mc \\\n", - "\thttps://dl.minio.io/client/mc/release/linux-amd64/mc\n", - " % Total % Received % Xferd Average Speed Time Time Time Current\n", - " Dload Upload Total Spent Left Speed\n", - "100 16.0M 100 16.0M 0 0 1835k 0 0:00:08 0:00:08 --:--:-- 2046k\n", - "chmod 755 /notebooks/demo/CanDIGv2/bin/minio\n", - "chmod 755 /notebooks/demo/CanDIGv2/bin/mc\n" - ] - } - ], - "source": [ - "# copy over a site.env for compose testing\n", - "!cp $(pwd)/etc/site/compose.env $(pwd)/site.env\n", - "\n", - "!make minio" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we can run this script to populate our Minio Object Store:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[m\u001b[32mAdded `minio` successfully.\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "!bin/mc config host add minio http://minio:9000 $(cat minio-access-key) $(cat minio-secret-key)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "File ‘NA20276.mapped.ILLUMINA.bwa.ASW.low_coverage.20120522.bam’ already there; not retrieving.\n", - "\n", - "File ‘chr22.fa.gz’ already there; not retrieving.\n", - "\n", - "\u001b[m\u001b[32;1mBucket created successfully `minio/candig/hg38/chromosomes/`.\u001b[0m\n", - "\u001b[0m\u001b[m\u001b[32;1mBucket created successfully `minio/candig/1000genomes/phase3/data/NA20276/alignment/`.\u001b[0m\n", - "\u001b[0m\u001b[m\u001b[32;1mBucket created successfully `minio/candig/reads/BroadHiSeqX_b37/NA12878/`.\u001b[0m\n", - "chr22.fa.gz: 11.69 MiB / 11.69 MiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 100.00% 37.89 MiB/s 0s\u001b[0m\u001b[0m" - ] - } - ], - "source": [ - "!wget -nc --continue https://s3.amazonaws.com/1000genomes/phase3/data/NA20276/alignment/NA20276.mapped.ILLUMINA.bwa.ASW.low_coverage.20120522.bam\n", - "!wget -nc --continue http://hgdownload.cse.ucsc.edu/goldenPath/hg38/chromosomes/chr22.fa.gz\n", - "#!wget -nc --continue https://dl.dnanex.us/F/D/Pb1QjgQx9j2bZ8Q44x50xf4fQV3YZBgkvkz23FFB/NA12878_recompressed.bam\n", - "\n", - "!bin/mc mb minio/candig/hg38/chromosomes/\n", - "!bin/mc mb minio/candig/1000genomes/phase3/data/NA20276/alignment/\n", - "!bin/mc mb minio/candig/reads/BroadHiSeqX_b37/NA12878/\n", - "\n", - "!bin/mc cp NA20276.mapped.ILLUMINA.bwa.ASW.low_coverage.20120522.bam minio/candig/1000genomes/phase3/data/NA20276/alignment/\n", - "!bin/mc cp chr22.fa.gz minio/candig/hg38/chromosomes/\n", - "#!bin/mc cp NA12878_recompressed.bam minio/candig/reads/BroadHiSeqX_b37/NA12878/" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Working with DOS/DRS API\n", - "\n", - "A useful Data Object Service might present a list of available reference FASTAs for performing downstream alignment and analysis.\n", - "\n", - "We'll index the UCSC human reference FASTAs into DOS as an example.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import requests, json\n", - "import datetime\n", - "\n", - "d = datetime.datetime.utcnow()\n", - "\n", - "headers = {'content-type': 'application/json'}\n", - "url = 'http://ga4gh-dos:8080/ga4gh/dos/v1/dataobjects'\n", - "data = json.dumps({\"data_object\": {\"id\": \"hg38-chr22\",\n", - " \"name\": \"Human Reference Chromosome 22\",\n", - " \"created\": d.isoformat(\"T\") + \"Z\",\n", - " \"checksums\": [{\"checksum\": \"41b47ce1cc21b558409c19b892e1c0d1\", \"type\": \"md5\"}],\n", - " \"urls\": [{\"url\": \"http://minio:9000/candig/hg38/chromosomes/chr22.fa.gz\"}],\n", - " \"size\": \"12255678\"}})\n", - "\n", - "requests.post(url, data, headers=headers)" - ] - }, - { - "cell_type": "code", - "execution_count": 84, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"data_objects\": [\n", - " {\n", - " \"checksums\": [\n", - " {\n", - " \"checksum\": \"41b47ce1cc21b558409c19b892e1c0d1\",\n", - " \"type\": \"md5\"\n", - " }\n", - " ],\n", - " \"created\": \"2019-05-28T18:38:16.960271Z\",\n", - " \"id\": \"hg38-chr22\",\n", - " \"name\": \"Human Reference Chromosome 22\",\n", - " \"size\": \"12255678\",\n", - " \"updated\": \"2019-05-28T18:38:16.960285Z\",\n", - " \"urls\": [\n", - " {\n", - " \"url\": \"http://minio:9000/candig/hg38/chromosomes/chr22.fa.gz\"\n", - " }\n", - " ],\n", - " \"version\": \"2019-05-28T18:38:16.960291Z\"\n", - " }\n", - " ]\n", - "}\n", - "\n" - ] - } - ], - "source": [ - "r = requests.get(url)\n", - "\n", - "print r.text" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using the Client to Access the Demo Server\n", - "\n", - "We can now use the Python client to create a simple Data Object. The same could be done using cURL or wget but we want to be more programmatic.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from ga4gh.dos.client import Client\n", - "\n", - "client = Client(\"http://ga4gh-dos:8080/ga4gh/dos/v1\")\n", - "c = client.client\n", - "models = client.models" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Listing Data Objects\n", - "\n", - "To list the existing Data Objects, we send a `ListDataObjectsRequest` to the ListDataObjects method!\n", - "\n", - "**NOTE:** If you install ga4gh-dos-schema via pip, there is a dependency error with the `ga4gh.dos.client` library. Run the following command to fix:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Collecting jsonschema==2.6.0\n", - " Using cached https://files.pythonhosted.org/packages/77/de/47e35a97b2b05c2fadbec67d44cfcdcd09b8086951b331d82de90d2912da/jsonschema-2.6.0-py2.py3-none-any.whl\n", - "Installing collected packages: jsonschema\n", - " Found existing installation: jsonschema 3.0.1\n", - " Uninstalling jsonschema-3.0.1:\n", - " Successfully uninstalled jsonschema-3.0.1\n", - "Successfully installed jsonschema-2.6.0\n" - ] - } - ], - "source": [ - "# Dependency mapping issues - https://github.com/ga4gh/data-repository-service-schemas/issues/147\n", - "!pip install jsonschema==2.6.0" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of Data Objects: 3 \n" - ] - } - ], - "source": [ - "# FIXED :)\n", - "ListDataObjectsRequest = models.get_model('ListDataObjectsRequest')\n", - "list_request = c.ListDataObjects(page_size=10000000)\n", - "list_response = list_request.result()\n", - "print(\"Number of Data Objects: {} \".format(len(list_response.data_objects)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can filter the DataObject in order to retrieve just the URL:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "url: http://minio:9000/candig/hg38/chromosomes/chr22.fa.gz, file_size (B): 12255678\n" - ] - } - ], - "source": [ - "# FIXED :)\n", - "data_objects = list_response.data_objects\n", - "data_object = data_objects[1]\n", - "print('url: {}, file_size (B): {}'.format(data_object.urls[0].url, data_object.size))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are additional features of the DOS client, such as querying against public DOS/DRS instaces and working with DataBundles, but this is out of scope of our demo. You can review these topics [here](https://github.com/ga4gh/data-repository-service-schemas/blob/master/python/examples/gdc_notebook.ipynb)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using DOS with htsget\n", - "\n", - "Data Objects are meant to represent versioned artifacts and can represent an API resource. For example, we could use DOS as a way of exposing htsget resources.\n", - "\n", - "In the [htsget Quickstart documentation](https://htsget.readthedocs.io/en/stable/quickstart.html) a link is made to the following snippet, which will stream the BAM results to the client.\n", - "\n", - "In this example, we will take a subset of the Illumina Platinum Genomes NA12878 and create a DataObject with metadata corresponding to the genomic range of interest. Using `htsget` is useful in cases such as this, since BAM files can be VERY large..." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Spider mode enabled. Check if remote file exists.\n", - "--2019-05-29 07:27:25-- https://dl.dnanex.us/F/D/Pb1QjgQx9j2bZ8Q44x50xf4fQV3YZBgkvkz23FFB/NA12878_recompressed.bam\n", - "Resolving dl.dnanex.us (dl.dnanex.us)... 3.89.79.27, 54.164.161.183\n", - "Connecting to dl.dnanex.us (dl.dnanex.us)|3.89.79.27|:443... connected.\n", - "HTTP request sent, awaiting response... 200 OK\n", - "Length: 140786687556 (131G) [application/x-gzip]\n", - "Remote file exists.\n", - "\n" - ] - } - ], - "source": [ - "!wget --spider https://dl.dnanex.us/F/D/Pb1QjgQx9j2bZ8Q44x50xf4fQV3YZBgkvkz23FFB/NA12878_recompressed.bam" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This particular sequence file is **131GB *compressed***, way too large to download over hotel/conference wi-fi. Furthermore, we do not need the entire dataset for our use case. So let's slice it with `htsget` and create a DataObject that we can use for downstream analysis:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "GetDataObjectResponse(data_object=DataObject(aliases=[u'NA12878 chr 2 subset'], checksums=[Checksum(checksum=u'eaf80af5e9e54db5936578bed06ffcdc', type=u'md5')], created=datetime.datetime(2019, 5, 28, 22, 12, 4, 831951, tzinfo=tzlocal()), description=None, id=u'na12878_2', mime_type=None, name=u'NA12878_2.bam', size=555749, updated=datetime.datetime(2019, 5, 28, 22, 12, 4, 831960, tzinfo=tzlocal()), urls=[URL(system_metadata=SystemMetadata(end=20000, reference_name=2, start=1000), url=u'http://minio:9000/candig/reads/BroadHiSeqX_b37/NA12878', user_metadata=None)], version=u'2019-05-28T22:12:04.831964Z'))\n" - ] - } - ], - "source": [ - "from ga4gh.dos.client import Client\n", - "import htsget\n", - "import datetime\n", - "import pytz\n", - "\n", - "# https://stackoverflow.com/questions/8556398/generate-rfc-3339-timestamp-in-python#8556555\n", - "d = datetime.datetime.utcnow()\n", - "d_with_timezone = d.replace(tzinfo=pytz.UTC)\n", - "\n", - "# htsget docs - https://htsget.readthedocs.io/en/latest/quickstart.html\n", - "url = \"http://htsnexus.rnd.dnanex.us/v1/reads/BroadHiSeqX_b37/NA12878\"\n", - "with open(\"NA12878_2.bam\", \"wb\") as output:\n", - " htsget.get(url, output, reference_name=\"2\", start=1000, end=20000)\n", - "\n", - "# https://github.com/ga4gh/data-repository-service-schemas/blob/master/python/examples/object-type-examples.ipynb\n", - "client = Client(\"http://ga4gh-dos:8080/ga4gh/dos/v1/\")\n", - "c = client.client\n", - "models = client.models\n", - "\n", - "DataObject = models.get_model('DataObject')\n", - "Checksum = models.get_model('Checksum')\n", - "URL = models.get_model('URL')\n", - "\n", - "na12878_2 = DataObject()\n", - "na12878_2.id = 'na12878_2'\n", - "na12878_2.name = 'NA12878_2.bam'\n", - "na12878_2.checksums = [\n", - " Checksum(checksum='eaf80af5e9e54db5936578bed06ffcdc', type='md5')]\n", - "na12878_2.urls = [\n", - " URL(\n", - " url=\"http://minio:9000/candig/reads/BroadHiSeqX_b37/NA12878\",\n", - " system_metadata={'reference_name': 2, 'start': 1000, 'end': 20000})]\n", - "na12878_2.aliases = ['NA12878 chr 2 subset']\n", - "na12878_2.size = '555749'\n", - "na12878_2.created = d_with_timezone\n", - "\n", - "c.CreateDataObject(body={'data_object': na12878_2}).result()\n", - "\n", - "response = c.GetDataObject(data_object_id='na12878_2').result()\n", - "\n", - "print(response)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will place this file back into our minio-server to so that others can retrieve this file:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "...878_2.bam: 542.72 KiB / 542.72 KiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 100.00% 27.02 MiB/s 0s\u001b[0m\u001b[0m" - ] - } - ], - "source": [ - "!bin/mc cp NA12878_2.bam minio/candig/reads/BroadHiSeqX_b37/NA12878/" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using DOS with WES\n", - "\n", - "The Workflow Execution Service attempts to present the interface for workflow execution over HTTP methods. Simple JSON requests including the inputs and outputs for a workflow are sent to a service. This allows us to \"ship code to the data,\" since data privacy and egress costs require that data is not shared.\n", - "\n", - "UCSC Toil is software for executing workflows. It presents a Python native API, which will not be demonstrated here, as well as a CWL compliant CLI interface. For that reason, any CWLRunner can easily be exposed by the workflow-service, demonstrated here." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!wes-server --backend=wes_service.cwl_runner --opt runner=cwltoil --opt extra=--logLevel=CRITICAL" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Background processes aren't supported directly in notebooks, so we close it here. But you can paste this command in a terminal and it will bring up your very own Workflow Execution Service!\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using the Client\n", - "\n", - "The server is now running, but Toil hasn't started yet as we haven't issued any Workflow Execution requests. Here, using the provided CLI client, we demonstrate a simple workflow which calculates an md5sum.\n", - "\n", - "### Accessing a workflow via Dockstore Tool Registry Service\n", - "\n", - "We will start by accessing the metadata for a workflow from dockstore." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{u'descriptor_type': [u'CWL', u'WDL'], u'verified': True, u'name': u'master', u'url': u'https://dockstore.org/api/api/ga4gh/v2/tools/quay.io%2Fbriandoconnor%2Fdockstore-tool-md5sum/versions/master', u'image': u'7f82fc51fa35d36bbd61297ee0c05170ab4ba67c969a9a66b28e5ed3c100034b', u'verified_source': u'Phase 1 GA4GH Tool Execution Challenge', u'image_name': u'quay.io/briandoconnor/dockstore-tool-md5sum', u'meta_version': u'2017-07-23 15:45:37.0', u'id': u'quay.io/briandoconnor/dockstore-tool-md5sum:master', u'containerfile': True, u'registry_url': u'quay.io'}\n" - ] - } - ], - "source": [ - "import requests\n", - "response = requests.get('https://dockstore.org:8443/api/ga4gh/v2/tools/', params={\"name\": \"md5sum\"})\n", - "print(response.json()[0]['versions'][7])\n", - "md5sum_url = response.json()[0]['versions'][7]['url'] + '/plain-CWL/descriptor/%2FDockstore.cwl'" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now have a URL we can pass too WES for execution!" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://dockstore.org/api/api/ga4gh/v2/tools/quay.io%2Fbriandoconnor%2Fdockstore-tool-md5sum/versions/master/plain-CWL/descriptor/%2FDockstore.cwl\n" - ] - } - ], - "source": [ - "print(md5sum_url)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using the WES CLI client to Execute" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cloning into 'workflow-service'...\n", - "remote: Enumerating objects: 101, done.\u001b[K\n", - "remote: Counting objects: 100% (101/101), done.\u001b[K\n", - "remote: Compressing objects: 100% (68/68), done.\u001b[K\n", - "remote: Total 1105 (delta 55), reused 67 (delta 33), pack-reused 1004\u001b[K\n", - "Receiving objects: 100% (1105/1105), 260.72 KiB | 970.00 KiB/s, done.\n", - "Resolving deltas: 100% (629/629), done.\n" - ] - } - ], - "source": [ - "!git clone https://github.com/common-workflow-language/workflow-service.git" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!wes-client --host 172.16.47.156:8181 --proto http $md5sum_url workflow-service/testdata/md5sum.cwl.json" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As you can see, the wes-client routed a request and polled the service until the its state was `COMPLETE`. It then shows us the location of the outputs, so we can read them." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running Workflows Against Remote WES Systems" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "...kflow.cwl: 76.96 MiB / 76.96 MiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 100.00% 397.92 MiB/s 0s\u001b[0m\u001b[0m" - ] - } - ], - "source": [ - "!mkdir -p demo\n", - "!bin/mc cp --recursive minio/candig/wes/workflows/demo/ demo/" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO:cwltool:Resolved 'demo/workflow.cwl' to 'file:///notebooks/demo/CanDIGv2/demo/workflow.cwl'\n", - "WARNING:toil.batchSystems.singleMachine:Limiting maxMemory to physically available memory (16709406720).\n", - "WARNING:toil.batchSystems.singleMachine:Limiting maxDisk to physically available disk (40889761792).\n", - "ERROR:pymesos.process:Failed to process event\n", - "Traceback (most recent call last):\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 194, in read\n", - " self._callback.process_event(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 274, in process_event\n", - " self.on_event(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/scheduler.py\", line 641, in on_event\n", - " func(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/scheduler.py\", line 559, in on_offers\n", - " self, [self._dict_cls(offer) for offer in offers]\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/batchSystems/mesos/batchSystem.py\", line 423, in resourceOffers\n", - " self._trackOfferedNodes(offers)\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/batchSystems/mesos/batchSystem.py\", line 508, in _trackOfferedNodes\n", - " assert(offer.agent_id.has_key('value'))\n", - "TypeError: 'Dict' object is not callable\n", - "ERROR:pymesos.process:Thread abort:\n", - "Traceback (most recent call last):\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 372, in _run\n", - " if not conn.read():\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 194, in read\n", - " self._callback.process_event(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 274, in process_event\n", - " self.on_event(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/scheduler.py\", line 641, in on_event\n", - " func(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/scheduler.py\", line 559, in on_offers\n", - " self, [self._dict_cls(offer) for offer in offers]\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/batchSystems/mesos/batchSystem.py\", line 423, in resourceOffers\n", - " self._trackOfferedNodes(offers)\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/batchSystems/mesos/batchSystem.py\", line 508, in _trackOfferedNodes\n", - " assert(offer.agent_id.has_key('value'))\n", - "TypeError: 'Dict' object is not callable\n", - "INFO:toil.common:Successfully deleted the job store: FileJobStore(/tmp/tmpfxi323kx)\n", - "Traceback (most recent call last):\n", - " File \"/opt/conda/bin/toil-cwl-runner\", line 10, in \n", - " sys.exit(main())\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/cwl/cwltoil.py\", line 1276, in main\n", - " outobj = toil.start(wf1)\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/common.py\", line 781, in start\n", - " return self._runMainLoop(rootJobGraph)\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/common.py\", line 1043, in _runMainLoop\n", - " logProcessContext(self.config)\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/__init__.py\", line 417, in logProcessContext\n", - " log.info(\"Running Toil version %s.\", version)\n", - " File \"/opt/conda/lib/python3.6/logging/__init__.py\", line 1308, in info\n", - " self._log(INFO, msg, args, **kwargs)\n", - " File \"/opt/conda/lib/python3.6/logging/__init__.py\", line 1444, in _log\n", - " self.handle(record)\n", - " File \"/opt/conda/lib/python3.6/logging/__init__.py\", line 1454, in handle\n", - " self.callHandlers(record)\n", - " File \"/opt/conda/lib/python3.6/logging/__init__.py\", line 1516, in callHandlers\n", - " hdlr.handle(record)\n", - " File \"/opt/conda/lib/python3.6/logging/__init__.py\", line 863, in handle\n", - " self.acquire()\n", - " File \"/opt/conda/lib/python3.6/logging/__init__.py\", line 814, in acquire\n", - " self.lock.acquire()\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 37, in _handle_sigint\n", - " reraise(*exc_info)\n", - " File \"/opt/conda/lib/python3.6/site-packages/six.py\", line 693, in reraise\n", - " raise value\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 372, in _run\n", - " if not conn.read():\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 194, in read\n", - " self._callback.process_event(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/process.py\", line 274, in process_event\n", - " self.on_event(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/scheduler.py\", line 641, in on_event\n", - " func(event)\n", - " File \"/opt/conda/lib/python3.6/site-packages/pymesos/scheduler.py\", line 559, in on_offers\n", - " self, [self._dict_cls(offer) for offer in offers]\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/batchSystems/mesos/batchSystem.py\", line 423, in resourceOffers\n", - " self._trackOfferedNodes(offers)\n", - " File \"/opt/conda/lib/python3.6/site-packages/toil/batchSystems/mesos/batchSystem.py\", line 508, in _trackOfferedNodes\n", - " assert(offer.agent_id.has_key('value'))\n", - "TypeError: 'Dict' object is not callable\n" - ] - } - ], - "source": [ - "!toil-cwl-runner --clean onError --cleanWorkDir onError --batchSystem mesos --mesosMaster toil-master:5050 demo/workflow.cwl demo/inputs.yml" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "candig", - "language": "python", - "name": "candig" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.16" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/README.md b/README.md index f356180e1..78045247b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ CanDIGv2/ ├── Makefile - functions for repeatable testing/deployment (Docker/Kubernetes) ├── tox.ini - functions for repeatable testing/deployment (Python Venv/Screen) ├── bin/ - local binaries directory - ├── docs/ - documentation, installation instructions + ├── docs/ - documentation, installation instructions ├── etc/ - contains misc files/config/scripts │    ├── docker/ - docker configurations │    ├── env/ - sample .env file @@ -51,7 +51,7 @@ Some of the functionality that is controlled through `.env` are: * pre-defined defaults for turnkey deployment Environment variables defined in the `.env` file can be read in `docker-compose` scripts through the variable substitution operator -`${VAR}`. +`${VAR}`. ```yaml # example compose YAML using variable substitution with default option @@ -63,19 +63,19 @@ services: ``` ### Configuring CanDIG modules -Not all CanDIG modules are required for a minimal installation. The `CANDIG_MODULES` and `CANDIG_AUTH_MODULES` define which modules are included in the deployment. +Not all CanDIG modules are required for a minimal installation. The `CANDIG_MODULES` and `CANDIG_AUTH_MODULES` define which modules are included in the deployment. By default (if you copy the sample file from `etc/env/example.env`) the installation includes the minimal list of modules: CANDIG_MODULES=minio htsget-server chord-metadata candig-data-portal -Optional modules follow the `#` and include federation service, various monitoring components, workflow execution, and some older modules not generally installed. +Optional modules follow the `#` and include federation service, various monitoring components, workflow execution, and some older modules not generally installed. -For federated installations, you will need `federation-service`. +For federated installations, you will need `federation-service`. -For production deployments, you will probably want to include `federation-service weavescope logging monitoring`. Be aware that the last three require more resources, includeing storage. +For production deployments, you will probably want to include `federation-service weavescope logging monitoring`. Be aware that the last three require more resources, includeing storage. -Authorization and authentication modules defined in `CANDIG_AUTH_MODULES` are only installed if you run `make init-authx` during deployment. +Authorization and authentication modules defined in `CANDIG_AUTH_MODULES` are only installed if you run `make init-authx` during deployment. ## `make` Deployment @@ -85,9 +85,6 @@ To deploy CanDIGv2, follow the docker deployment guide in `docs/`: There are other deprecated deployment guides in `docs`, but there are no guarantees that these still function: -* [Vagrant Deployment Guide (with instructions for OpenStack)](./docs/install-vagrant.md) -* [Kubernetes Deployment Guide](./docs/install-kubernetes.md) -* [Tox Deployment Guide](./docs/install-tox.md) * [Authentication and Authorization Deployment Guide](./docs/authx-setup.md) View additional Makefile options with `make help`. @@ -106,14 +103,8 @@ The following table lists the details from the Data Flow Diagram in the "Overvie | Service/Component Name | Source | Notes | |------------------------|--------|------------------------------| | Katsu (CHORD Metadata) | links | DFD: `chord_metadata` | -| CNV Service | links | DFD: `cnv_service` | -| Authorization Service | links | DFD: `authorization_service` | | Federation Service | links | DFD: `federation_service` | -| Datasets Service | links | DFD: `datasets_service` | -| RNAGet | links | DFD: `rnaget` | -| CanDIGv1 Server | links | DFD: `candig_server` | | HTSGet | links | DFD: `htsget_app` | | CHORD DRS | links | DFD: `chord_drs` | -| IGV JS | links | DFD: `igv_js` | | WES Server | links | DFD: `wes_server` | -| CanDIG Data Portal | links | DFD: | \ No newline at end of file +| CanDIG Data Portal | links | DFD: `candig-data-portal` | From 6d6fc942c1be5cb543889f608853b5c07741b8f3 Mon Sep 17 00:00:00 2001 From: shaikh-rashid <44211165+shaikh-rashid@users.noreply.github.com> Date: Wed, 14 Dec 2022 13:36:37 -0500 Subject: [PATCH 128/236] Module cleanup (#184) * removed unused modules * remove swarm, kubernetes, and tox related make commands * remove modules from env * fixed make compose methods * Remove vargrant, tox, and traefik from modules * remove graphql module * cleanup of docs * change minio to bind to local address * start opa container even if exited * more specific grepping for container names * fix doc links * fix for network issues Co-authored-by: Shaikh Rashid Co-authored-by: Daisie Huang --- .gitmodules | 15 - Makefile | 485 +---- Makefile.authx | 25 +- Vagrantfile | 73 - docs/ingest-and-test.md | 9 +- etc/env/example.env | 126 +- etc/ssl/alt_names.txt | 1 - etc/ssl/root-ca.cnf | 4 - etc/ssl/site.cnf | 7 - lib/cancogen-dashboard/cancogen_dashboard | 1 - lib/cancogen-dashboard/docker-compose.yml | 34 - lib/candig-data-portal/docker-compose.yml | 18 - lib/candig-server/Dockerfile | 32 - lib/candig-server/config.py | 10 - lib/candig-server/docker-compose.yml | 38 - lib/{compose => candigv2}/docker-compose.yml | 23 - lib/chord-metadata/docker-compose.yml | 29 - lib/cnv-service/candig_cnv_service | 1 - lib/cnv-service/docker-compose.yml | 29 - lib/consul/docker-compose.yml | 36 - lib/datasets/datasets_service | 1 - lib/datasets/docker-compose.yml | 31 - lib/federation-service/docker-compose.yml | 17 - lib/graphql/GraphQL-interface | 1 - lib/graphql/docker-compose.yml | 35 - lib/htsget-server/docker-compose.yml | 23 - lib/igv-js/Dockerfile | 22 - lib/igv-js/app.js | 38 - lib/igv-js/docker-compose.yml | 27 - lib/igv-js/html/.index.html | 47 - .../html/assets/css/font-awesome.min.css | 4 - lib/igv-js/html/assets/css/ie9.css | 35 - lib/igv-js/html/assets/css/main.css | 1594 ----------------- lib/igv-js/html/assets/css/noscript.css | 12 - lib/igv-js/html/assets/fonts/FontAwesome.otf | Bin 124988 -> 0 bytes .../html/assets/fonts/fontawesome-webfont.eot | Bin 76518 -> 0 bytes .../html/assets/fonts/fontawesome-webfont.svg | 685 ------- .../html/assets/fonts/fontawesome-webfont.ttf | Bin 152796 -> 0 bytes .../assets/fonts/fontawesome-webfont.woff | Bin 90412 -> 0 bytes .../assets/fonts/fontawesome-webfont.woff2 | Bin 71896 -> 0 bytes lib/igv-js/html/assets/js/jquery.min.js | 5 - lib/igv-js/html/assets/js/main.js | 409 ----- lib/igv-js/html/assets/js/skel.min.js | 2 - lib/igv-js/html/assets/js/util.js | 587 ------ lib/igv-js/html/assets/sass/base/_page.scss | 35 - .../html/assets/sass/base/_typography.scss | 183 -- .../html/assets/sass/components/_box.scss | 26 - .../html/assets/sass/components/_button.scss | 80 - .../html/assets/sass/components/_form.scss | 253 --- .../html/assets/sass/components/_icon.scss | 17 - .../html/assets/sass/components/_image.scss | 87 - .../html/assets/sass/components/_list.scss | 198 -- .../html/assets/sass/components/_table.scss | 81 - lib/igv-js/html/assets/sass/ie9.scss | 48 - lib/igv-js/html/assets/sass/layout/_bg.scss | 68 - .../html/assets/sass/layout/_footer.scss | 37 - .../html/assets/sass/layout/_header.scss | 261 --- lib/igv-js/html/assets/sass/layout/_main.scss | 102 -- .../html/assets/sass/layout/_wrapper.scss | 36 - .../html/assets/sass/libs/_functions.scss | 34 - lib/igv-js/html/assets/sass/libs/_mixins.scss | 62 - lib/igv-js/html/assets/sass/libs/_skel.scss | 585 ------ lib/igv-js/html/assets/sass/libs/_vars.scss | 43 - lib/igv-js/html/assets/sass/main.scss | 49 - lib/igv-js/html/assets/sass/noscript.scss | 19 - lib/igv-js/html/images/bg.jpg | Bin 37864 -> 0 bytes lib/igv-js/html/images/overlay.png | Bin 4385 -> 0 bytes lib/igv-js/html/images/pic01.jpg | Bin 10064 -> 0 bytes lib/igv-js/html/images/pic02.jpg | Bin 8904 -> 0 bytes lib/igv-js/html/images/pic03.jpg | Bin 9697 -> 0 bytes lib/igv-js/html/index.pug | 37 - lib/igv-js/package.json | 11 - lib/jupyter/Dockerfile | 301 ---- lib/jupyter/demo/deepvariant-wdl.sh | 78 - lib/jupyter/demo/deepvariant-wes.sh | 143 -- lib/jupyter/demo/demo.ipynb | 151 -- lib/jupyter/demo/install-demos.sh | 85 - lib/jupyter/docker-compose.yml | 44 - lib/keycloak/docker-compose.yml | 17 - lib/kubernetes/docker-compose.yml | 48 - lib/logging/docker-compose.yml | 38 - lib/logging/fluentd/docker-compose.yml | 8 - lib/logging/json/docker-compose.yml | 5 - lib/mc/docker-compose.yml | 28 - lib/mc/mc-config.sh | 14 - lib/mc/sample.config.json | 35 - lib/minio/docker-compose.yml | 51 +- lib/minio/minio/Dockerfile | 19 - lib/minio/minio/setup.sh | 16 - lib/monitoring/docker-compose.yml | 61 - lib/opa/docker-compose.yml | 24 - lib/opa/opa_setup.sh | 6 +- lib/portainer/docker-compose.yml | 60 - lib/rnaget/Dockerfile | 18 - lib/rnaget/docker-compose.yml | 29 - lib/rnaget/rnaget_service | 1 - lib/swarm/docker-compose.yml | 77 - lib/templates/docker-compose.yml | 38 - lib/toil/docker-compose.yml | 28 - lib/traefik/docker-compose.yml | 92 - lib/tyk/docker-compose.yml | 31 +- lib/vault/docker-compose.yml | 35 +- lib/vault/vault_setup.sh | 6 +- lib/weavescope/docker-compose.yml | 48 - lib/wes-server/docker-compose.yml | 27 - setup_vagrant.sh | 21 - tox.ini | 123 -- 107 files changed, 48 insertions(+), 8681 deletions(-) delete mode 100644 Vagrantfile delete mode 100644 etc/ssl/alt_names.txt delete mode 100644 etc/ssl/root-ca.cnf delete mode 100644 etc/ssl/site.cnf delete mode 160000 lib/cancogen-dashboard/cancogen_dashboard delete mode 100644 lib/cancogen-dashboard/docker-compose.yml delete mode 100644 lib/candig-server/Dockerfile delete mode 100644 lib/candig-server/config.py delete mode 100644 lib/candig-server/docker-compose.yml rename lib/{compose => candigv2}/docker-compose.yml (82%) delete mode 160000 lib/cnv-service/candig_cnv_service delete mode 100644 lib/cnv-service/docker-compose.yml delete mode 100644 lib/consul/docker-compose.yml delete mode 160000 lib/datasets/datasets_service delete mode 100644 lib/datasets/docker-compose.yml delete mode 160000 lib/graphql/GraphQL-interface delete mode 100644 lib/graphql/docker-compose.yml delete mode 100644 lib/igv-js/Dockerfile delete mode 100644 lib/igv-js/app.js delete mode 100644 lib/igv-js/docker-compose.yml delete mode 100644 lib/igv-js/html/.index.html delete mode 100644 lib/igv-js/html/assets/css/font-awesome.min.css delete mode 100644 lib/igv-js/html/assets/css/ie9.css delete mode 100644 lib/igv-js/html/assets/css/main.css delete mode 100644 lib/igv-js/html/assets/css/noscript.css delete mode 100644 lib/igv-js/html/assets/fonts/FontAwesome.otf delete mode 100644 lib/igv-js/html/assets/fonts/fontawesome-webfont.eot delete mode 100644 lib/igv-js/html/assets/fonts/fontawesome-webfont.svg delete mode 100644 lib/igv-js/html/assets/fonts/fontawesome-webfont.ttf delete mode 100644 lib/igv-js/html/assets/fonts/fontawesome-webfont.woff delete mode 100644 lib/igv-js/html/assets/fonts/fontawesome-webfont.woff2 delete mode 100644 lib/igv-js/html/assets/js/jquery.min.js delete mode 100644 lib/igv-js/html/assets/js/main.js delete mode 100644 lib/igv-js/html/assets/js/skel.min.js delete mode 100644 lib/igv-js/html/assets/js/util.js delete mode 100644 lib/igv-js/html/assets/sass/base/_page.scss delete mode 100644 lib/igv-js/html/assets/sass/base/_typography.scss delete mode 100644 lib/igv-js/html/assets/sass/components/_box.scss delete mode 100644 lib/igv-js/html/assets/sass/components/_button.scss delete mode 100644 lib/igv-js/html/assets/sass/components/_form.scss delete mode 100644 lib/igv-js/html/assets/sass/components/_icon.scss delete mode 100644 lib/igv-js/html/assets/sass/components/_image.scss delete mode 100644 lib/igv-js/html/assets/sass/components/_list.scss delete mode 100644 lib/igv-js/html/assets/sass/components/_table.scss delete mode 100644 lib/igv-js/html/assets/sass/ie9.scss delete mode 100644 lib/igv-js/html/assets/sass/layout/_bg.scss delete mode 100644 lib/igv-js/html/assets/sass/layout/_footer.scss delete mode 100644 lib/igv-js/html/assets/sass/layout/_header.scss delete mode 100644 lib/igv-js/html/assets/sass/layout/_main.scss delete mode 100644 lib/igv-js/html/assets/sass/layout/_wrapper.scss delete mode 100644 lib/igv-js/html/assets/sass/libs/_functions.scss delete mode 100644 lib/igv-js/html/assets/sass/libs/_mixins.scss delete mode 100644 lib/igv-js/html/assets/sass/libs/_skel.scss delete mode 100644 lib/igv-js/html/assets/sass/libs/_vars.scss delete mode 100644 lib/igv-js/html/assets/sass/main.scss delete mode 100644 lib/igv-js/html/assets/sass/noscript.scss delete mode 100644 lib/igv-js/html/images/bg.jpg delete mode 100644 lib/igv-js/html/images/overlay.png delete mode 100644 lib/igv-js/html/images/pic01.jpg delete mode 100644 lib/igv-js/html/images/pic02.jpg delete mode 100644 lib/igv-js/html/images/pic03.jpg delete mode 100644 lib/igv-js/html/index.pug delete mode 100644 lib/igv-js/package.json delete mode 100644 lib/jupyter/Dockerfile delete mode 100644 lib/jupyter/demo/deepvariant-wdl.sh delete mode 100644 lib/jupyter/demo/deepvariant-wes.sh delete mode 100644 lib/jupyter/demo/demo.ipynb delete mode 100644 lib/jupyter/demo/install-demos.sh delete mode 100644 lib/jupyter/docker-compose.yml delete mode 100644 lib/kubernetes/docker-compose.yml delete mode 100644 lib/logging/fluentd/docker-compose.yml delete mode 100644 lib/logging/json/docker-compose.yml delete mode 100644 lib/mc/docker-compose.yml delete mode 100644 lib/mc/mc-config.sh delete mode 100644 lib/mc/sample.config.json delete mode 100644 lib/minio/minio/Dockerfile delete mode 100644 lib/minio/minio/setup.sh delete mode 100644 lib/portainer/docker-compose.yml delete mode 100644 lib/rnaget/Dockerfile delete mode 100644 lib/rnaget/docker-compose.yml delete mode 160000 lib/rnaget/rnaget_service delete mode 100644 lib/swarm/docker-compose.yml delete mode 100644 lib/traefik/docker-compose.yml delete mode 100644 lib/weavescope/docker-compose.yml delete mode 100644 setup_vagrant.sh delete mode 100644 tox.ini diff --git a/.gitmodules b/.gitmodules index 0fc3489d7..def6ecb6d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,27 +7,12 @@ [submodule "lib/federation-service/federation_service"] path = lib/federation-service/federation_service url = https://github.com/CanDIG/federation_service -[submodule "lib/cnv-service/candig_cnv_service"] - path = lib/cnv-service/candig_cnv_service - url = https://github.com/CanDIG/candig_cnv_service -[submodule "lib/rnaget/rnaget_service"] - path = lib/rnaget/rnaget_service - url = https://github.com/CanDIG/rnaget_service -[submodule "lib/datasets/datasets_service"] - path = lib/datasets/datasets_service - url = https://github.com/CanDIG/datasets_service [submodule "lib/chord-metadata/chord_metadata_service"] path = lib/chord-metadata/chord_metadata_service url = https://github.com/CanDIG/katsu.git -[submodule "lib/cancogen-dashboard/cancogen_dashboard"] - path = lib/cancogen-dashboard/cancogen_dashboard - url = https://github.com/CanDIG/cancogen_dashboard.git [submodule "lib/candig-data-portal/candig-data-portal"] path = lib/candig-data-portal/candig-data-portal url = https://github.com/CanDIG/candig-data-portal.git [submodule "lib/opa/opa"] path = lib/opa/opa url = https://github.com/CanDIG/candig-opa.git -[submodule "lib/graphql/GraphQL-interface"] - path = lib/graphql/GraphQL-interface - url = https://github.com/CanDIG/GraphQL-interface.git diff --git a/Makefile b/Makefile index 99906be43..beb6f5147 100644 --- a/Makefile +++ b/Makefile @@ -29,10 +29,7 @@ all: .PHONY: mkdir mkdir: mkdir -p $(DIR)/bin - mkdir -p $(DIR)/tmp/configs - mkdir -p $(DIR)/tmp/data - mkdir -p $(DIR)/tmp/secrets - mkdir -p $(DIR)/tmp/ssl + mkdir -p $(DIR)/tmp/{configs,data,secrets} mkdir -p $(DIR)/tmp/{keycloak,tyk,vault} mkdir -p ${DIR}/tmp/federation @@ -43,7 +40,7 @@ mkdir: #<<< .PHONY: bin-all -bin-all: bin-conda bin-docker-machine bin-minio bin-traefik bin-prometheus +bin-all: bin-conda #>>> @@ -72,81 +69,6 @@ endif echo " finished bin-conda" >> $(LOGFILE) -#>>> -# download docker-machine (for swarm deployment) -# make bin-docker-machine - -#<<< -bin-docker-machine: mkdir - echo " started bin-docker-machine" >> $(LOGFILE) - curl -Lo $(DIR)/bin/docker-machine \ - https://github.com/docker/machine/releases/download/v0.16.2/docker-machine-`uname -s`-`uname -m` - chmod 755 $(DIR)/bin/docker-machine - echo " finished bin-docker-machine" >> $(LOGFILE) - - -#>>> -# download latest minio server/client from Minio repo -# make bin-minio - -#<<< -bin-minio: mkdir - echo " started bin-minio" >> $(LOGFILE) -ifeq ($(VENV_OS), arm64mac) - curl -Lo $(DIR)/bin/minio \ - https://dl.minio.io/server/minio/release/darwin-arm64/minio - curl -Lo $(DIR)/bin/mc \ - https://dl.minio.io/client/mc/release/darwin-arm64/mc -else - curl -Lo $(DIR)/bin/minio \ - https://dl.minio.io/server/minio/release/$(VENV_OS)-amd64/minio - curl -Lo $(DIR)/bin/mc \ - https://dl.minio.io/client/mc/release/$(VENV_OS)-amd64/mc -endif - chmod 755 $(DIR)/bin/minio - chmod 755 $(DIR)/bin/mc - echo " finished bin-minio" >> $(LOGFILE) - - -#>>> -# download prometheus binaries from Github repo -# make bin-prometheus - -#<<< -bin-prometheus: mkdir - echo " started bin-prometheus" >> $(LOGFILE) - mkdir -p $(DIR)/bin/prometheus -ifeq ($(VENV_OS), arm64mac) - curl -Lo $(DIR)/bin/prometheus/prometheus.tar.gz \ - https://github.com/prometheus/prometheus/releases/download/v$(PROMETHEUS_VERSION)/prometheus-$(PROMETHEUS_VERSION).darwin-arm64.tar.gz -else - curl -Lo $(DIR)/bin/prometheus/prometheus.tar.gz \ - https://github.com/prometheus/prometheus/releases/download/v$(PROMETHEUS_VERSION)/prometheus-$(PROMETHEUS_VERSION).$(VENV_OS)-amd64.tar.gz -endif - tar --strip-components=1 -zxvf $(DIR)/bin/prometheus/prometheus.tar.gz -C $(DIR)/bin/prometheus - chmod 755 $(DIR)/bin/prometheus/prometheus - echo " finished bin-prometheus" >> $(LOGFILE) - - -#>>> -# download latest traefik binary from Github repo -# make bin-traefik - -#<<< -bin-traefik: mkdir - echo " started bin-traefik" >> $(LOGFILE) -ifeq ($(VENV_OS), arm64mac) - curl -Lo $(DIR)/bin/traefik.tar.gz \ - https://github.com/traefik/traefik/releases/download/v$(TRAEFIK_VERSION)/traefik_v$(TRAEFIK_VERSION)_darwin_arm64.tar.gz -else - curl -Lo $(DIR)/bin/traefik.tar.gz \ - https://github.com/traefik/traefik/releases/download/v$(TRAEFIK_VERSION)/traefik_v$(TRAEFIK_VERSION)_$(VENV_OS)_amd64.tar.gz -endif - tar -xvzf $(DIR)/bin/traefik.tar.gz -C bin/ - chmod 755 $(DIR)/bin/traefik - echo " finished bin-traefik" >> $(LOGFILE) - - #>>> # (re)build service image and deploy/test using docker-compose # $module is the name of the sub-folder in lib/ @@ -158,8 +80,7 @@ endif build-%: echo " started build-$*" >> $(LOGFILE) DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 \ - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml $(DIR)/lib/$*/docker-compose.yml \ - | docker-compose -f - build $(BUILD_OPTS) + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/$*/docker-compose.yml build $(BUILD_OPTS) echo " finished build-$*" >> $(LOGFILE) @@ -170,17 +91,8 @@ build-%: #<<< .PHONY: clean-all -clean-all: clean-stack clean-compose clean-containers clean-secrets clean-configs \ - clean-volumes clean-networks clean-images clean-swarm clean-machines \ - clean-certs clean-conda clean-bin - - -#>>> -# close all authentication and authorization services - -#<<< -clean-auth: - +clean-all: clean-compose clean-containers clean-secrets \ + clean-volumes clean-images clean-conda clean-bin #>>> @@ -194,16 +106,6 @@ clean-bin: rm -rf $(DIR)/bin -#>>> -# removed selfsigned-certs (including root-ca) -# make clean-certs - -#<<< -.PHONY: clean-certs -clean-certs: - rm -f $(DIR)/tmp/ssl/selfsigned-* - - #>>> # stops and removes docker-compose instances # make clean-compose @@ -212,8 +114,7 @@ clean-certs: .PHONY: clean-compose clean-compose: $(foreach MODULE, $(CANDIG_MODULES), \ - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml $(DIR)/lib/$(MODULE)/docker-compose.yml \ - | docker-compose -f - down;) + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/$(MODULE)/docker-compose.yml down || true;) #>>> @@ -228,17 +129,6 @@ clean-conda: $(CONDA) env remove -n $(VENV_NAME) -#>>> -# clear swarm configs and remove config files -# make clean-configs - -#<<< -.PHONY: clean-configs -clean-configs: - -docker config rm `docker config ls -q` - rm -rf $(DIR)/tmp/configs - - #>>> # stop all running containers and remove all stopped containers # make clean-containers @@ -246,7 +136,6 @@ clean-configs: #<<< .PHONY: clean-containers clean-containers: - -docker stop `docker ps -q` docker container prune -f @@ -260,48 +149,6 @@ clean-images: docker image prune -a -f -#>>> -# shutdown kubernetes services -# make clean-kubernetes - -#<<< -.PHONY: clean-kubernetes -clean-kubernetes: - $(DIR)/bin/kompose --file $(DIR)/lib/kubernetes/docker-compose.yml \ - $(foreach MODULE, $(CANDIG_MODULES), --file $(DIR)/lib/$(MODULE)/docker-compose.yml) \ - down - - -#>>> -# destroy docker-machine cluster -# make clean-machines - -#<<< -.PHONY: clean-machines -clean-machines: - $(DIR)/bin/docker-machine rm -f `$(DIR)/bin/docker-machine ls -q` - - -#>>> -# destroy minikube cluster -# make clean-minikube - -#<<< -.PHONY: clean-minikube -clean-minikube: - $(DIR)/bin/minikube delete - - -#>>> -# remove all unused networks -# make clean-networks - -#<<< -.PHONY: clean-networks -clean-networks: - docker network prune -f - - #>>> # clear swarm secrets and remove secret files # make clean-secrets @@ -313,36 +160,6 @@ clean-secrets: rm -rf $(DIR)/tmp/secrets -#>>> -# remove all stacks -# make clean-stack - -#<<< -.PHONY: clean-stack -clean-stack: - -docker stack rm `docker stack ls | awk '{print $$1}'` - - -#>>> -# leave docker-swarm -# make clean-swarm - -#<<< -.PHONY: clean-swarm -clean-swarm: - docker swarm leave --force - - -#>>> -# clear all tox screen sessions -# make clean-tox - -#<<< -.PHONY: clean-tox -clean-tox: - screen -ls | grep pts | cut -d. -f1 | awk '{print $$1}' | xargs kill - - #>>> # remove all peristant volumes and local data # make clean-volumes @@ -362,9 +179,6 @@ clean-volumes: .PHONY: compose compose: $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) compose-$(MODULE);) - # cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - # $(foreach MODULE, $(CANDIG_MODULES), $(DIR)/lib/$(MODULE)/docker-compose.yml) \ - # | docker-compose -f - up #>>> @@ -375,28 +189,10 @@ compose: #<<< compose-%: echo " started compose-$*" >> $(LOGFILE) - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/$*/docker-compose.yml \ - | docker-compose --compatibility -f - up -d + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/$*/docker-compose.yml --compatibility up -d echo " finished compose-$*" >> $(LOGFILE) -#>>> -# create docker bridge networks -# make docker-networks - -#<<< -.PHONY: docker-networks -docker-networks: - docker network create --driver bridge --subnet=$(DOCKER_BRIDGE_IP) --attachable \ - bridge-net || echo "bridge-net already exists..." - docker network create --driver bridge --subnet=$(DOCKER_GWBRIDGE_IP) --attachable \ - -o com.docker.network.bridge.enable_icc=false \ - -o com.docker.network.bridge.name=docker_gwbridge \ - -o com.docker.network.bridge.enable_ip_masquerade=true \ - docker_gwbridge || echo "docker_gwbridge already exists..." - - #>>> # pull images from $DOCKER_REGISTRY # make docker-pull @@ -405,7 +201,7 @@ docker-networks: .PHONY: docker-pull docker-pull: $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) pull-$(MODULE);) - #$(foreach MODULE, $(TOIL_MODULES), docker pull $(DOCKER_REGISTRY)/$(MODULE):latest;) +#$(foreach MODULE, $(TOIL_MODULES), docker pull $(DOCKER_REGISTRY)/$(MODULE):latest;) #>>> @@ -416,7 +212,7 @@ docker-pull: .PHONY: docker-push docker-push: $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) push-$(MODULE);) - #$(foreach MODULE, $(TOIL_MODULES), docker push $(DOCKER_REGISTRY)/$(MODULE):latest;) +#$(foreach MODULE, $(TOIL_MODULES), docker push $(DOCKER_REGISTRY)/$(MODULE):latest;) #>>> @@ -426,11 +222,8 @@ docker-push: #<<< .PHONY: docker-secrets docker-secrets: mkdir minio-secrets - @echo admin > $(DIR)/tmp/secrets/portainer-user - $(MAKE) secret-portainer-secret - $(MAKE) secret-metadata-app-secret - @echo admin > $(DIR)/tmp/secrets/metadata-db-user + $(MAKE) secret-metadata-app-secret $(MAKE) secret-metadata-db-secret @echo admin > $(DIR)/tmp/secrets/keycloak-admin-user @@ -447,7 +240,7 @@ docker-secrets: mkdir minio-secrets $(MAKE) secret-tyk-analytics-admin-key $(MAKE) secret-vault-s3-token - + $(MAKE) secret-opa-root-token $(MAKE) secret-opa-service-token @@ -459,17 +252,12 @@ docker-secrets: mkdir minio-secrets #<<< .PHONY: docker-volumes docker-volumes: - docker volume create consul-data - docker volume create datasets-data docker volume create grafana-data docker volume create jupyter-data - docker volume create mc-config docker volume create minio-config docker volume create minio-data $(MINIO_VOLUME_OPT) - docker volume create portainer-data docker volume create prometheus-data docker volume create toil-jobstore - docker volume create traefik-data docker volume create keycloak-data docker volume create tyk-data docker volume create tyk-redis-data @@ -506,9 +294,9 @@ init-conda: && conda activate $(VENV_NAME) \ && pip install -U -r $(DIR)/etc/venv/requirements.txt - #@echo "Load local conda: source $(DIR)/bin/miniconda3/etc/profile.d/conda.sh" - #@echo "Activate conda env: conda activate $(VENV_NAME)" - #@echo "Install requirements: pip install -U -r $(DIR)/etc/venv/requirements.txt" +#@echo "Load local conda: source $(DIR)/bin/miniconda3/etc/profile.d/conda.sh" +#@echo "Activate conda env: conda activate $(VENV_NAME)" +#@echo "Install requirements: pip install -U -r $(DIR)/etc/venv/requirements.txt" echo " finished init-conda" >> $(LOGFILE) @@ -518,78 +306,7 @@ init-conda: #<<< .PHONY: init-docker -init-docker: ssl-cert docker-networks docker-volumes docker-secrets - - -#>>> -# initialize kubernetes environment -# make init-kubernetes - -#<<< -.PHONY: init-kubernetes -init-kubernetes: ssl-cert docker-secrets docker-pull - $(DIR)/bin/kubectl create namespace $(DOCKER_NAMESPACE) - - -#>>> -# initialize docker-swarm environment and create swarm networks, configs, and secrets -# make init-swarm - -#<<< -.PHONY: init-swarm -init-swarm: swarm-init swarm-networks swarm-configs swarm-secrets - - -#>>> -# deploy/test all modules in $CANDIG_MODULES using Kubernetes -# make kubernetes - -#<<< -.PHONY: kubernetes -kubernetes: - $(DIR)/bin/kompose --file $(DIR)/lib/kubernetes/docker-compose.yml \ - $(foreach MODULE, $(CANDIG_MODULES), --file $(DIR)/lib/$(MODULE)/docker-compose.yml) \ - up - - -#>>> -# deploys individual module using kompose -# $module is the name of the sub-folder in lib -# make kube-$module - -#<<< -kube-%: - $(DIR)/bin/kompose --file $(DIR)/lib/kubernetes/docker-compose.yml \ - --file $(DIR)/lib/$*/docker-compose.yml up - - -#>>> -# create docker-machine instance(s) for Docker Compose/Swarm development -# NOTE: only virtualbox is supported at this time -# NOTE: use MINIKUBE_* to configure vm options -# $vm_name must be a unique name for the docker-machine instance (e.g. make machine-manager) -# make machine-$vm_name - -#<<< -machine-%: - $(DIR)/bin/docker-machine create --driver "$(MINIKUBE_DRIVER)" \ - --virtualbox-cpu-count "$(MINIKUBE_CPUS)" --virtualbox-memory "$(MINIKUBE_MEM)" \ - --virtualbox-disk-size "$(MINIKUBE_DISK)" --virtualbox-hostonly-cidr "192.168.56.1/24" \ - --virtualbox-hostonly-nicpromisc "deny" --virtualbox-hostonly-nictype "82540EM" \ - $* - - -#>>> -# create minikube environment for (kubernetes) integration testing -# make minikube - -#<<< -.PHONY: minikube -minikube: - $(DIR)/bin/minikube start --container-runtime $(MINIKUBE_CRI) \ - --cpus $(MINIKUBE_CPUS) --memory $(MINIKUBE_MEM) --disk-size $(MINIKUBE_DISK) \ - --network-plugin cni --cni $(MINIKUBE_CNI) --driver $(MINIKUBE_DRIVER) \ - --dns-domain $(CANDIG_DOMAIN) --nodes $(MINIKUBE_NODES) +init-docker: docker-volumes docker-secrets #>>> @@ -603,8 +320,6 @@ minio-secrets: @echo '[default]' > $(DIR)/tmp/secrets/aws-credentials @echo "aws_access_key_id=`cat tmp/secrets/minio-access-key`" >> $(DIR)/tmp/secrets/aws-credentials @echo "aws_secret_access_key=`cat tmp/secrets/minio-secret-key`" >> $(DIR)/tmp/secrets/aws-credentials - cp $(DIR)/tmp/ssl/selfsigned-site.crt $(DIR)/tmp/secrets/selfsigned-site-crt - cp $(DIR)/tmp/ssl/selfsigned-site.key $(DIR)/tmp/secrets/selfsigned-site-key #>>> @@ -614,9 +329,7 @@ minio-secrets: #<<< pull-%: - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/$*/docker-compose.yml \ - | docker-compose -f - pull + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/$*/docker-compose.yml pull #>>> @@ -626,9 +339,7 @@ pull-%: #<<< push-%: - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/$*/docker-compose.yml \ - | docker-compose -f - push + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/$*/docker-compose.yml push #>>> @@ -641,144 +352,6 @@ secret-%: | base64 | tr -d '\n\r+' | sed s/[^A-Za-z0-9]//g > $(DIR)/tmp/secrets/$* -#>>> -# generate root-ca and site ssl certs using openssl -# make ssl-cert - -#<<< -ssl-cert: - openssl genrsa -out $(DIR)/tmp/ssl/selfsigned-root-ca.key 4096 - openssl req -new -key $(DIR)/tmp/ssl/selfsigned-root-ca.key \ - -out $(DIR)/tmp/ssl/selfsigned-root-ca.csr -sha256 \ - -subj '/C=CA/ST=ON/L=Toronto/O=CanDIG/CN=CanDIG Self-Signed CA' - openssl x509 -req -days 3650 -in $(DIR)/tmp/ssl/selfsigned-root-ca.csr \ - -signkey $(DIR)/tmp/ssl/selfsigned-root-ca.key -sha256 \ - -out $(DIR)/tmp/ssl/selfsigned-root-ca.crt \ - -extfile $(DIR)/etc/ssl/root-ca.cnf -extensions root_ca - openssl genrsa -out $(DIR)/tmp/ssl/selfsigned-site.key 4096 - openssl req -new -key $(DIR)/tmp/ssl/selfsigned-site.key \ - -out $(DIR)/tmp/ssl/selfsigned-site.csr -sha256 \ - -subj '/C=CA/ST=ON/L=Toronto/O=CanDIG/CN=CanDIG Self-Signed Cert' - - cp $(DIR)/etc/ssl/site.cnf $(DIR)/tmp/ssl/site.cnf - sed -i.bak s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/tmp/ssl/site.cnf - openssl x509 -req -days 750 -in $(DIR)/tmp/ssl/selfsigned-site.csr -sha256 \ - -CA $(DIR)/tmp/ssl/selfsigned-root-ca.crt \ - -CAkey $(DIR)/tmp/ssl/selfsigned-root-ca.key \ - -CAcreateserial -out $(DIR)/tmp/ssl/selfsigned-site.crt \ - -extfile $(DIR)/tmp/ssl/site.cnf -extensions server - - cp $(DIR)/etc/ssl/alt_names.txt $(DIR)/tmp/ssl/alt_names.txt - sed -i.bak s/CANDIG_DOMAIN/$(CANDIG_DOMAIN)/ $(DIR)/tmp/ssl/alt_names.txt - openssl x509 -req -days 365 -in $(DIR)/tmp/ssl/selfsigned-root-ca.csr \ - -sha256 \ - -signkey $(DIR)/tmp/ssl/selfsigned-root-ca.key \ - -extfile $(DIR)/tmp/ssl/alt_names.txt \ - -out $(DIR)/tmp/ssl/public.crt - openssl x509 -in $(DIR)/tmp/ssl/public.crt -out $(DIR)/tmp/ssl/cert.pem - - - -#>>> -# deploy/test all modules in $CANDIG_MODULES using docker stack -# make stack - -#<<< -.PHONY: stack -stack: - $(foreach MODULE, $(CANDIG_MODULES), $(MAKE) stack-$(MODULE);) - - -#>>> -# deploy/test individual modules using docker stack -# $module is the name of the sub-folder in lib/ -# make stack-$module - -#<<< -stack-%: - cat $(DIR)/lib/swarm/docker-compose.yml \ - $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/$*/docker-compose.yml > $(DIR)/tmp/data/docker-compose.yml - docker stack deploy --compose-file $(DIR)/tmp/data/docker-compose.yml $(DOCKER_NAMESPACE) - - -#>>> -# initialize primary docker-swarm master node -# make swarm-init - - -#>>> -# create docker configs for CanDIG services (swarm only) -# configs are distributed to all swarm nodes -# make swarm-configs - -#<<< -.PHONY: swarm-configs -swarm-configs: - docker config create chord-metadata-settings $(DIR)/lib/chord-metadata/settings.py - docker config create wes-dependency-resolver $(DIR)/etc/yml/$(WES_DEPENDENCY_RESOLVER).yml - -#<<< -.PHONY: swarm-init -swarm-init: - docker swarm init --advertise-addr $(SWARM_ADVERTISE_IP) --listen-addr $(SWARM_LISTEN_IP) - @docker swarm join-token manager -q > $(DIR)/tmp/secrets/swarm-manager-token - @docker swarm join-token worker -q > $(DIR)/tmp/secrets/swarm-worker-token - - -#>>> -# join a docker swarm cluster using manager/worker token -# make swarm-join - -#<<< -.PHONY: swarm-join -swarm-join: - @docker swarm join --advertise-addr $(SWARM_ADVERTISE_IP) --listen-addr $(SWARM_LISTEN_IP) \ - --token `cat $(DIR)/tmp/secrets/swarm-$(SWARM_MODE)-token` $(SWARM_MANAGER_IP) - - -#>>> -# create docker swarm overlay networks -# make-swarm-networks - -#<<< -.PHONY: swarm-networks -swarm-networks: - docker network create --driver overlay --opt encrypted=true traefik-net - docker network create --driver overlay --internal --opt encrypted=true agent-net - -#>>> -# create docker swarm compatbile secrets -# make swarm-secrets - -#<<< -.PHONY: swarm-secrets -swarm-secrets: - docker secret create aws-credentials $(DIR)/tmp/secrets/aws-credentials - docker secret create minio-access-key $(DIR)/tmp/secrets/minio-access-key - docker secret create minio-secret-key $(DIR)/tmp/secrets/minio-secret-key - - docker secret create portainer-user $(DIR)/tmp/secrets/portainer-user - docker secret create portainer-secret $(DIR)/tmp/secrets/portainer-secret - - docker secret create traefik-ssl-key $(DIR)/tmp/ssl/$(TRAEFIK_SSL_CERT).key - docker secret create traefik-ssl-crt $(DIR)/tmp/ssl/$(TRAEFIK_SSL_CERT).crt - - docker secret create metadata-app-secret $(DIR)/tmp/secrets/metadata-app-secret - docker secret create metadata-db-user $(DIR)/tmp/secrets/metadata-db-user - docker secret create metadata-db-secret $(DIR)/tmp/secrets/metadata-db-secret - - docker secret create keycloak-admin-user $(DIR)/tmp/secrets/keycloak-admin-user - docker secret create keycloak-admin-password $(DIR)/tmp/secrets/keycloak-admin-password - - docker secret create tyk-secret-key $(DIR)/tmp/secrets/tyk-secret-key - docker secret create tyk-node-secret-key $(DIR)/tmp/secrets/tyk-node-secret-key - - # TODO: review - #docker secret create keycloak-test-password-1 $(DIR)/tmp/secrets/keycloak-test-password-1 - #docker secret create keycloak-test-password-2 $(DIR)/tmp/secrets/keycloak-test-password-2 - - #>>> # create toil images using upstream CanDIG Toil repo # make toil-docker @@ -787,7 +360,8 @@ swarm-secrets: .PHONY: toil-docker toil-docker: echo " started toil-docker" >> $(LOGFILE) - VIRTUAL_ENV=1 DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 TOIL_DOCKER_REGISTRY=$(DOCKER_REGISTRY) $(MAKE) -C $(DIR)/lib/toil/toil-docker docker + VIRTUAL_ENV=1 DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 TOIL_DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ + $(MAKE) -C $(DIR)/lib/toil/toil-docker docker $(foreach MODULE,$(TOIL_MODULES), \ docker tag $(DOCKER_REGISTRY)/$(MODULE):$(TOIL_VERSION)-$(TOIL_BUILD_HASH) \ $(DOCKER_REGISTRY)/$(MODULE):$(TOIL_VERSION);) @@ -798,26 +372,6 @@ toil-docker: echo " finished toil-docker" >> $(LOGFILE) -#>>> -# deploys all modules using Tox -# make tox - -#<<< -.PHONY: tox -tox: - dotenv -f .env run tox - - -#>>> -# deploys individual module using tox -# $module is the name of the sub-folder in lib/ -# make tox-$module - -#<<< -tox-%: - dotenv -f .env run tox -e $* - - #>>> # view available options # make help @@ -837,3 +391,4 @@ help: #<<< print-%: @echo '$*=$($*)' + diff --git a/Makefile.authx b/Makefile.authx index 767895fb9..c3c60586d 100644 --- a/Makefile.authx +++ b/Makefile.authx @@ -4,8 +4,7 @@ #<<< clean-keycloak: - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/keycloak/docker-compose.yml | docker-compose -f - down + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/keycloak/docker-compose.yml down || true # - remove intermittent docker images @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'keycloak' | xargs -I{} docker rmi --force {} @@ -18,8 +17,7 @@ clean-keycloak: #<<< clean-vault: - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/vault/docker-compose.yml | docker-compose -f - down + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/vault/docker-compose.yml down || true # - remove intermittent docker images @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'vault' | xargs -I{} docker rmi --force {} @@ -33,8 +31,7 @@ clean-vault: #<<< clean-opa: - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/opa/docker-compose.yml | docker-compose -f - down + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/opa/docker-compose.yml down || true # - remove intermittent docker images @eval docker images --format '{{.Repository}}:{{.Tag}}' | grep 'openpolicyagent/opa' | xargs -I{} docker rmi --force {} @@ -49,8 +46,7 @@ clean-opa: #<<< clean-tyk: - cat $(DIR)/lib/compose/docker-compose.yml $(DIR)/lib/logging/$(DOCKER_LOG_DRIVER)/docker-compose.yml \ - $(DIR)/lib/tyk/docker-compose.yml | docker-compose -f - down + docker-compose -f $(DIR)/lib/candigv2/docker-compose.yml -f $(DIR)/lib/tyk/docker-compose.yml down || true # - remove intermittent docker images docker images --format '{{.Repository}}:{{.Tag}}' | grep -E 'tyk|redis' | xargs -I{} docker rmi --force {} @@ -76,12 +72,6 @@ clean-authx: clean-keycloak clean-tyk clean-vault clean-opa #<<< init-authx: mkdir - # Generate dynamic environment variables - # ========== HACK ALERT ============== - # This setup with backslashes (\) is required because Make runs each command - # in its own shell. So to pass the environment from previous commands, one has - # to do this backslash dance. This could have been resolved but we decided to - # move to a better solution so Aman made a decision to keep this hack as-is. $(MAKE) docker-volumes echo "Setting up Keycloak"; \ source ${PWD}/lib/keycloak/keycloak_setup.sh; \ @@ -107,12 +97,6 @@ init-authx: mkdir redeploy-tyk: mkdir $(MAKE) clean-tyk $(MAKE) docker-volumes - # Generate dynamic environment variables - # ========== HACK ALERT ============== - # This setup with backslashes (\) is required because Make runs each command - # in its own shell. So to pass the environment from previous commands, one has - # to do this backslash dance. This could have been resolved but we decided to - # move to a better solution so Aman made a decision to keep this hack as-is. source ${PWD}/lib/tyk/tyk_setup.sh; \ echo ; \ $(MAKE) compose-tyk; \ @@ -129,4 +113,3 @@ redeploy-tyk: mkdir setup-%: echo "Setting up $*" source ${PWD}/lib/$*/$*_setup.sh - diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index b6e660bfe..000000000 --- a/Vagrantfile +++ /dev/null @@ -1,73 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Install vagrant-disksize to allow resizing the vagrant box disk. -unless Vagrant.has_plugin?("vagrant-disksize") - raise Vagrant::Errors::VagrantError.new, "vagrant-disksize plugin is missing. Please install it using 'vagrant plugin install vagrant-disksize' and rerun 'vagrant up'" -end - -# Install vagrant-reload to allow reloading during provisioning. -unless Vagrant.has_plugin?("vagrant-reload") - raise Vagrant::Errors::VagrantError.new, "vagrant-reload plugin is missing. Please install it using 'vagrant plugin install vagrant-reload' and rerun 'vagrant up'" -end - -Vagrant.configure('2') do |config| - config.vm.hostname = 'candig-dev' - ip_address = ENV['CUSTOM_IP'] || "192.168.33.33" - config.vm.network "private_network", ip: ip_address - - config.vm.provider 'virtualbox' do |vb, override| - override.vm.synced_folder '.', '/home/vagrant/candig', type: 'virtualbox' - override.vm.box = 'generic/ubuntu2010' - override.vm.hostname = 'candig.local' - override.disksize.size = '50GB' -# override.vm.network "forwarded_port", guest: 80, host: 80 -# override.vm.network "forwarded_port", guest: 443, host: 443 - vb.name = 'candig-dev' - vb.gui = false - vb.customize ['modifyvm', :id, '--cpus', 4] - vb.customize ['modifyvm', :id, '--memory', '8096'] - # run custom shell on provision - override.vm.provision 'shell', privileged: false, path: "provision.sh", args: ["/home/vagrant/candig"] - override.vm.provision :reload - override.vm.provision 'shell', privileged: false, path: "setup_containers.sh", args: ["/home/vagrant/candig"] - override.vm.provision 'shell', privileged: false, path: "setup_auth.sh", args: ["/home/vagrant/candig"] - end - - config.vm.provider :libvirt do |libvirt, override| - override.vm.synced_folder '.', '/home/vagrant/candig', type: 'nfs' - override.vm.box = 'generic/ubuntu2010' - override.vm.hostname = 'candig.local' - override.disksize.size = '50GB' - libvirt.memory = 4096 - libvirt.nested = true - libvirt.cpus = 4 - override.vm.provision 'shell', privileged: false, path: "provision.sh", args: ["/home/vagrant/candig"] - override.vm.provision :reload - override.vm.provision 'shell', privileged: false, path: "setup_containers.sh", args: ["/home/vagrant/candig"] - end - - - config.vm.provider :openstack do |os, override| - override.vm.synced_folder '.', '/home/vagrant/candig', type: 'virtualbox', disabled: true - override.ssh.username = 'ubuntu' - override.ssh.private_key_path = ENV["OS_PRIVATEKEY_PATH"] - os.username = ENV["OS_USERNAME"] - os.password = ENV["OS_PASSWORD"] - os.user_domain_name = ENV["OS_USER_DOMAIN_NAME"] - os.project_name = ENV["OS_PROJECT_NAME"] - os.project_domain_name = ENV["OS_PROJECT_DOMAIN_ID"] - os.identity_api_version = ENV["OS_IDENTITY_API_VERSION"] - os.region = ENV["OS_REGION_NAME"] - os.openstack_auth_url = ENV["OS_AUTH_URL"] - os.interface_type = ENV["OS_INTERFACE"] - os.keypair_name = ENV["OS_KEYPAIR"] - - os.flavor = 'm1.large' - os.image = 'UbuntuServer-1804-2019Nov20' - os.server_name = 'candig-vagrant' - override.vm.provision 'shell', privileged: false, path: "provision.sh", args: ["."] - override.vm.provision :reload - override.vm.provision 'shell', privileged: false, path: "setup_containers.sh", args: ["/home/ubuntu/CanDIGv2"] - end -end diff --git a/docs/ingest-and-test.md b/docs/ingest-and-test.md index d8df80336..d07226df7 100644 --- a/docs/ingest-and-test.md +++ b/docs/ingest-and-test.md @@ -78,13 +78,12 @@ If you are seeing the above directory not found error in WSL it is a issue with Current directory: **../CanDIGv2** ```bash #Copy the servers.json and services.json into the config folder instead: -cp tmp/federation/* lib/federation-service/federation_service/configs +cp tmp/federation/* lib/federation-service/federation_service/configs ``` -You will need to comment out the 'secrets' section in the lib/federation-service/docker-compose.yml file. It will look like the code chunk below: +You will need to comment out the 'secrets' section in the lib/federation-service/docker-compose.yml file. It will look like the code chunk below: ```yml ... - logging: *default-logging # secrets: # - source: federation-servers # target: /app/federation_service/configs/servers.json @@ -149,7 +148,7 @@ python settings.py ../CanDIGv2/ source env.sh ``` -Create opa dataset policy based on the dataset name and the email +Create opa dataset policy based on the dataset name and the email address of the user (for example, dataset_name=SYNTHETIC_1 and user_name=user2@test.ca) ```bash @@ -171,7 +170,7 @@ docker cp '/local_path_to_file/Synthetic_Clinical_Data.json' candigv2_chord-meta Then run ingest command (katsu and federation should be running): ```bash -python katsu_ingest.py --dataset --input /Synthetic_Clinical_Data.json +python katsu_ingest.py --dataset --input /Synthetic_Clinical_Data.json ``` ## Test data services diff --git a/etc/env/example.env b/etc/env/example.env index c04e01f96..312381e5a 100644 --- a/etc/env/example.env +++ b/etc/env/example.env @@ -2,7 +2,7 @@ # - - - # site options -CANDIG_MODULES=minio htsget-server chord-metadata candig-data-portal #drs-server wes-server jupyter igv-js traefik portainer consul logging monitoring datasets cnv-service rnaget federation-service +CANDIG_MODULES=minio htsget-server chord-metadata candig-data-portal #drs-server wes-server logging monitoring federation-service CANDIG_AUTH_MODULES=keycloak tyk opa vault # options are [, , host.docker.internal, docker.localhost] @@ -11,6 +11,7 @@ CANDIG_AUTH_DOMAIN=docker.localhost CANDIG_SITE_LOCATION=UHN CANDIG_DEBUG_MODE=1 + # miniconda venv # options are [linux, darwin, arm64mac] VENV_OS=linux @@ -18,45 +19,12 @@ VENV_NAME=candig VENV_PYTHON=3.10 VENV_PIP=22.2.2 + # docker -# options are [bridge, bridge-net, ingress, traefik-net] -DOCKER_NET=bridge-net -DOCKER_BRIDGE_IP=10.10.1.0/24 -DOCKER_GWBRIDGE_IP=10.10.2.0/24 -# options are [compose, swarm, kubernetes] -DOCKER_MODE=compose DOCKER_NAMESPACE=candig DOCKER_REGISTRY=candig -# options are [json, fluentd] -DOCKER_LOG_DRIVER=json ALPINE_VERSION=3.14 -# docker swarm -# options are [manager, worker] -SWARM_MODE=manager -# options are [, , :, :] -SWARM_ADVERTISE_IP=eth0 -SWARM_LISTEN_IP=eth0 -SWARM_MANAGER_IP=eth0 - -# minikube deploy -MINIKUBE_NODES=1 -# options are [containerd, cri-o, docker] -MINIKUBE_CRI=docker -# options are [auto, bridge, calico, cilium, flannel, kindnet] -MINIKUBE_CNI=auto -MINIKUBE_CPUS=2 -# options are [] -MINIKUBE_MEM=4096 -# options are [] -MINIKUBE_DISK=80000 -# options are [virtualbox, vmwarefusion, kvm2, vmware, none, docker, podman] -MINIKUBE_DRIVER=virtualbox - -# weavescope app -#TODO: update weave version -WEAVE_VERSION=1.13.1 -WEAVE_UI_PORT=4040 # logging services #TODO: test monitoring version updates @@ -78,40 +46,10 @@ CADVISOR_PORT=9080 GRAFANA_VERSION=8.2.4 GRAFANA_PORT=9888 -# portainer controller -PORTAINER_VERSION=2.9.3-alpine -PORTAINER_UI_PORT=9010 -#options are [unix:///var/run/docker.sock, tcp://tasks.portainer-agent:9001] -PORTAINER_SOCKET=unix:///var/run/docker.sock - -# consul server -CONSUL_VERSION=1.9 -CONSUL_RPC_PORT=8502 -CONSUL_HTTP_PORT=8500 -CONSUL_DNS_PORT=8600 -CONSUL_LAN_PORT=8301 -CONSUL_WAN_PORT=8302 - -# traefik controller, need at least 2.5.1 for mac M1 support -TRAEFIK_VERSION=2.5.1 -# enable swarm operations -# options are [true, false] -TRAEFIK_SWARM=false -# expose containers by default -# options are [true, false] -TRAEFIK_EXPOSE=true -TRAEFIK_HTTP_PORT=80 -TRAEFIK_HTTPS_PORT=443 -TRAEFIK_UI_PORT=8000 -# options are [http, https] -TRAEFIK_ENTRYPOINT=http -TRAEFIK_SSL_CERT=selfsigned-site -# options are [unix:///var/run/docker.sock, tcp://127.0.0.1:2377] -TRAEFIK_SOCKET=unix:///var/run/docker.sock # minio server MINIO_VERSION=latest -MINIO_UI_PORT=9090 +MINIO_UI_PORT=9001 MINIO_PORT=9000 MINIO_PUBLIC_URL=http://${CANDIG_DOMAIN}:${MINIO_PORT} MINIO_PRIVATE_URL=http://minio:9000 @@ -127,12 +65,14 @@ MINIO_SELF_CERT=0 #MINIO_VOLUME_OPT+=--opt=type=ext4 #MINIO_VOLUME_OPT+=--opt=device=/dev/sdb1 + # htsget-app HTSGET_APP_VERSION=v1.0.1 HTSGET_PRIVATE_URL=http://htsget-app:3000 HTSGET_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} HTSGET_APP_PORT=3333 + # wes server WES_VERSION=3.3 WES_PORT=5000 @@ -160,6 +100,7 @@ WES_OPT+=--opt=extra=--metrics #WES_OPT+=--opt=extra=--metrics #--- + # toil executor TOIL_VERSION=5.5.0 TOIL_BUILD_HASH=b0ff5be051f2fd55352e00450b7848dcf8354a3b-py3.7 @@ -169,18 +110,6 @@ TOIL_PORT=5050 TOIL_UI_PORT=3000 TOIL_WORKER_PORT=5051 -# igv.js -IGVJS_VERSION=2.0 -IGVJS_PORT=9091 - -# jupyter-lab -JUPYTER_VERSION=v0.2.1 -JUPYTER_UI_PORT=8888 -JUPYTER_R_PORT=8787 -JUPYTER_NOTEBOOK_DIR=/notebooks -JUPYTER_USER=jovyan -JUPYTER_ENABLE_LAB=yes -JUPYTER_ENABLE_SUDO=yes # federation_service FEDERATION_VERSION=v0.5.5 @@ -189,6 +118,7 @@ FEDERATION_PORT=4232 FEDERATION_SERVICE_URL=http://federation-service:${FEDERATION_PORT} FEDERATION_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_FEDERATION_API_LISTEN_PATH} + # chord metadata service CHORD_METADATA_VERSION=v1.5.1 CHORD_METADATA_PORT=8008 @@ -207,33 +137,6 @@ CANDIG_AUTHZ_SERVICE_PORT=8182 CACHE_TIME=0 -# cnv service -CNV_SERVICE_HOST=0.0.0.0 -CNV_SERVICE_PORT=8870 - -# candig server -CANDIG_SERVER_VERSION=1.6.0 -CANDIG_SERVER_HOST=0.0.0.0 -CANDIG_SERVER_PORT=3001 -CANDIG_INGEST_VERSION=1.5.0 -CANDIG_PUBLIC_URL=${TYK_LOGIN_TARGET_URL}/${TYK_CANDIG_API_LISTEN_PATH} -CANDIG_PRIVATE_URL=http://candig-server:3000 - -# rnaget service -RNAGET_VERSION=v0.9.5 -RNAGET_HOST=0.0.0.0 -RNAGET_PORT=3005 - -# datasets service -DATASETS_VERSION=latest -DATASETS_HOST=0.0.0.0 -DATASETS_PORT=8880 - -# authorization service -AUTHORIZATION_SERVICE_VERSION=v0.0.1-alpha -AUTHORIZATION_SERVICE_HOST=0.0.0.0 -AUTHORIZATION_SERVICE_PORT=7000 - # keycloak service KEYCLOAK_VERSION=15.0.0 KEYCLOAK_REALM=candig @@ -252,6 +155,7 @@ KEYCLOAK_REALM_URL=${KEYCLOAK_PUBLIC_URL}/auth/realms/${KEYCLOAK_REALM} KEYCLOAK_GENERATE_TEST_USER=1 + # tyk service TYK_VERSION=v3.2 TYK_REDIS_VERSION=5.0-alpine @@ -324,6 +228,7 @@ TYK_FEDERATION_API_LISTEN_PATH=federation #TYK_EXAMPLE_API_TARGET=http://example.org #TYK_EXAMPLE_API_LISTEN_PATH=example + # vault service VAULT_VERSION=1.10.4 VAULT_FILE_PATH="/vault/data" @@ -352,17 +257,6 @@ CANDIG_DATA_PORTAL_PORT=2543 CANDIG_DATA_PORTAL_URL=http://${CANDIG_DOMAIN}:${CANDIG_DATA_PORTAL_PORT}/data-portal CANDIG_DATA_PORTAL_PRIVATE_URL=http://candig-data-portal:3000 -# graphql-interface -GRAPHQL_PYTHON_VERSION=3.8 -GRAPHQL_INTERFACE_VERSION=v1.0.0 -GRAPHQL_INTERFACE_PORT=7999 -GRAPHQL_PRIVATE_URL=http://graphql:7999 - -GRAPHQL_KATSU_API=http://${TYK_LOGIN_TARGET_URL}/${TYK_KATSU_API_LISTEN_PATH}/api -GRAPHQL_CANDIG_SERVER=http://${TYK_LOGIN_TARGET_URL}/${TYK_CANDIG_API_LISTEN_PATH} -GRAPHQL_BEACON_ID=com.candig.graphql -GRAPHQL_KATSU_TOKEN_KEY=authorization -GRAPHQL_CANDIG_TOKEN_KEY=authorization # vault helper tool TOKEN_PATH = ${PWD}/Vault-Helper-Tool/token.txt diff --git a/etc/ssl/alt_names.txt b/etc/ssl/alt_names.txt deleted file mode 100644 index 12a00cb92..000000000 --- a/etc/ssl/alt_names.txt +++ /dev/null @@ -1 +0,0 @@ -subjectAltName = DNS:CANDIG_DOMAIN diff --git a/etc/ssl/root-ca.cnf b/etc/ssl/root-ca.cnf deleted file mode 100644 index b87de32c7..000000000 --- a/etc/ssl/root-ca.cnf +++ /dev/null @@ -1,4 +0,0 @@ -[root_ca] -basicConstraints = critical,CA:TRUE,pathlen:1 -keyUsage = critical, nonRepudiation, cRLSign, keyCertSign -subjectKeyIdentifier=hash diff --git a/etc/ssl/site.cnf b/etc/ssl/site.cnf deleted file mode 100644 index d4004c29e..000000000 --- a/etc/ssl/site.cnf +++ /dev/null @@ -1,7 +0,0 @@ -[server] -authorityKeyIdentifier=keyid,issuer -basicConstraints = critical,CA:FALSE -extendedKeyUsage=serverAuth -keyUsage = critical, digitalSignature, keyEncipherment -subjectAltName = DNS:CANDIG_DOMAIN, IP:127.0.0.1 -subjectKeyIdentifier=hash diff --git a/lib/cancogen-dashboard/cancogen_dashboard b/lib/cancogen-dashboard/cancogen_dashboard deleted file mode 160000 index f5d70b17c..000000000 --- a/lib/cancogen-dashboard/cancogen_dashboard +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f5d70b17cdf92d38dc795b5309ab5433d083449e diff --git a/lib/cancogen-dashboard/docker-compose.yml b/lib/cancogen-dashboard/docker-compose.yml deleted file mode 100644 index fd5cbacb9..000000000 --- a/lib/cancogen-dashboard/docker-compose.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: '3.7' - -services: - cancogen-dashboard: - build: - context: $PWD/lib/cancogen-dashboard/cancogen_dashboard - image: ${DOCKER_REGISTRY}/cancogen-dashboard:${CANCOGEN_DASHBOARD_VERSION:-latest} - networks: - - ${DOCKER_NET} - ports: - - "${CANCOGEN_DASHBOARD_PORT}:3000" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.cancogen-dashboard.rule=Host(`cancogen-dashboard.${CANDIG_DOMAIN}`)" - - "traefik.http.services.cancogen-dashboard.loadbalancer.server.port=${CANCOGEN_DASHBOARD_PORT}" - logging: *default-logging - tty: true - environment: - - CHOKIDAR_USEPOLLING=true - - REACT_APP_BASE_URL=${CANCOGEN_BASE_URL} - - REACT_APP_METADATA_URL=${CANCOGEN_METADATA_URL} - - REACT_APP_HTSGET_URL=${CANCOGEN_HTSGET_URL} - - REACT_APP_DRS_URL=${CANCOGEN_DRS_URL} - - REACT_APP_FEDERATION_URL=${CANCOGEN_FEDERATION_URL} diff --git a/lib/candig-data-portal/docker-compose.yml b/lib/candig-data-portal/docker-compose.yml index 2a180204c..e124c3185 100644 --- a/lib/candig-data-portal/docker-compose.yml +++ b/lib/candig-data-portal/docker-compose.yml @@ -7,25 +7,8 @@ services: args: katsu_api_target_url: "${TYK_KATSU_API_TARGET}" image: ${DOCKER_REGISTRY}/candig-data-portal:${CANDIG_DATA_PORTAL_VERSION:-latest} - networks: - - ${DOCKER_NET} ports: - "${CANDIG_DATA_PORTAL_PORT}:2543" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.candig-data-portal.rule=Host(`candig-data-portal.${CANDIG_DOMAIN}`)" - - "traefik.http.services.candig-data-portal.loadbalancer.server.port=${CANDIG_DATA_PORTAL_PORT}" - logging: *default-logging secrets: - source: vault-s3-token target: vault-s3-token @@ -33,7 +16,6 @@ services: target: opa-service-token environment: - REACT_APP_KATSU_API_SERVER=${CHORD_METADATA_PUBLIC_URL} - - REACT_APP_CANDIG_SERVER=${CANDIG_PUBLIC_URL} - REACT_APP_HTSGET_SERVER=${HTSGET_PUBLIC_URL} - REACT_APP_FEDERATION_API_SERVER=${FEDERATION_PUBLIC_URL} - REACT_APP_BASE_NAME= diff --git a/lib/candig-server/Dockerfile b/lib/candig-server/Dockerfile deleted file mode 100644 index 5e08ac685..000000000 --- a/lib/candig-server/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -ARG venv_python - -FROM python:${venv_python}-slim - -ARG candig_version -ARG candig_ingest - -LABEL Maintainer="CanDIG Project" - -COPY candig-server /app/candig-server - -WORKDIR /app/candig-server - -COPY config.py /app/candig-server - -RUN apt-get update && \ - apt-get install -y build-essential zlib1g-dev wget git && \ - apt-get autoclean && \ - apt-get autoremove -y - -RUN pip install --no-cache-dir candig-ingest==${candig_ingest} -RUN pip install -r requirements.txt -RUN pip install /app/candig-server - -# Uncomment below lines if you want to ingest some mock data -RUN mkdir candig-example-data && \ - wget https://raw.githubusercontent.com/CanDIG/candig-ingest/master/candig/ingest/mock_data/clinical_metadata_tier1.json && \ - wget https://raw.githubusercontent.com/CanDIG/candig-ingest/master/candig/ingest/mock_data/clinical_metadata_tier2.json && \ - ingest candig-example-data/registry.db mock1 clinical_metadata_tier1.json && \ - ingest candig-example-data/registry.db mock2 clinical_metadata_tier2.json - -ENTRYPOINT ["candig_server"] diff --git a/lib/candig-server/config.py b/lib/candig-server/config.py deleted file mode 100644 index 0706961a1..000000000 --- a/lib/candig-server/config.py +++ /dev/null @@ -1,10 +0,0 @@ -import os - - -OPA_SERVER = os.environ['OPA_SERVER']+"/v1/data/permissions/datasets" -OPA_SERVER_TOKEN = os.environ['OPA_SERVER_TOKEN'] - -TYK_ENABLED = True -TYK_SERVER = os.environ['TYK_SERVER'] -TYK_LISTEN_PATH = os.environ['TYK_LISTEN_PATH'] -ACCESS_LIST = "access_list.txt" diff --git a/lib/candig-server/docker-compose.yml b/lib/candig-server/docker-compose.yml deleted file mode 100644 index de4842d62..000000000 --- a/lib/candig-server/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: '3.7' - -services: - candig-server: - #build: - #context: $PWD/lib/candig-server - #args: - #venv_python: '3.6' - #candig_version: ${CANDIG_SERVER_VERSION} - #candig_ingest: ${CANDIG_INGEST_VERSION} - image: ${DOCKER_REGISTRY}/candig-server:${CANDIG_SERVER_VERSION} - volumes: - - minio-data:/data - networks: - - ${DOCKER_NET} - ports: - - "${CANDIG_SERVER_PORT}:3000" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.candig-server.rule=Host(`candig-server.${CANDIG_DOMAIN}`)" - - "traefik.http.services.candig-server.loadbalancer.server.port=${CANDIG_SERVER_PORT}" - logging: *default-logging - environment: - - OPA_SERVER=http://opa:${OPA_PORT} - - OPA_SERVER_TOKEN=${CANDIG_OPA_SECRET} - - TYK_SERVER=${TYK_CANDIG_API_TARGET} - - TYK_LISTEN_PATH=${TYK_CANDIG_API_LISTEN_PATH} - command: ["--host", "0.0.0.0", "--port", "3000", "--config-file", "/app/candig-server/config.py"] diff --git a/lib/compose/docker-compose.yml b/lib/candigv2/docker-compose.yml similarity index 82% rename from lib/compose/docker-compose.yml rename to lib/candigv2/docker-compose.yml index e625d45b2..1e57ed1fe 100644 --- a/lib/compose/docker-compose.yml +++ b/lib/candigv2/docker-compose.yml @@ -1,35 +1,16 @@ version: '3.7' -networks: - bridge: - external: true - bridge-net: - external: true - ingress: - traefik-net: - agent-net: - volumes: - datasets-data: - external: true minio-data: external: true minio-config: external: true - mc-config: - external: true toil-jobstore: external: true - portainer-data: - external: true prometheus-data: external: true - consul-data: - external: true grafana-data: external: true - traefik-data: - external: true keycloak-data: external: true opa-data: @@ -66,10 +47,6 @@ secrets: file: $PWD/tmp/secrets/portainer-user portainer-secret: file: $PWD/tmp/secrets/portainer-secret - traefik-ssl-key: - file: $PWD/tmp/ssl/${TRAEFIK_SSL_CERT}.key - traefik-ssl-crt: - file: $PWD/tmp/ssl/${TRAEFIK_SSL_CERT}.crt wes-dependency-resolver: file: $PWD/etc/yml/${WES_DEPENDENCY_RESOLVER}.yml keycloak-admin-user: diff --git a/lib/chord-metadata/docker-compose.yml b/lib/chord-metadata/docker-compose.yml index b3ef5fdb7..2ef776c64 100644 --- a/lib/chord-metadata/docker-compose.yml +++ b/lib/chord-metadata/docker-compose.yml @@ -8,27 +8,10 @@ services: venv_python: "3.10" alpine_version: "3.16" image: ${DOCKER_REGISTRY}/chord-metadata:${CHORD_METADATA_VERSION:-latest} - networks: - - ${DOCKER_NET} ports: - "${CHORD_METADATA_PORT}:8000" depends_on: - metadata-db - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.chord-metadata.rule=Host(`chord-metadata.${CANDIG_DOMAIN}`)" - - "traefik.http.services.chord-metadata.loadbalancer.server.port=${CHORD_METADATA_PORT}" - logging: *default-logging environment: - CHORD_URL=${CHORD_METADATA_HOST} - CHORD_PERMISSIONS=${CHORD_METADATA_AUTH} @@ -70,20 +53,8 @@ services: #- chord-metadata-db:/var/lib/postgresql/data #add volume name to lib/{compose,swarm,kubernetes} #add volume name to docker-volumes in Makefile - networks: - - ${DOCKER_NET} # ports: # - "${CHORD_METADATA_PORT}:8000" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - logging: *default-logging environment: - POSTGRES_USER=admin - POSTGRES_DB=metadata diff --git a/lib/cnv-service/candig_cnv_service b/lib/cnv-service/candig_cnv_service deleted file mode 160000 index 29cff73a9..000000000 --- a/lib/cnv-service/candig_cnv_service +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 29cff73a941d6d4f439e4b27076f9922f5881d36 diff --git a/lib/cnv-service/docker-compose.yml b/lib/cnv-service/docker-compose.yml deleted file mode 100644 index 4cb2a6ce4..000000000 --- a/lib/cnv-service/docker-compose.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: '3.7' - -services: - cnv-service: - build: - context: $PWD/lib/cnv-service/candig_cnv_service - args: - venv_python: "${VENV_PYTHON}" - image: ${DOCKER_REGISTRY}/cnv-service:${CNV_SERVICE_VERSION:-latest} - networks: - - ${DOCKER_NET} - ports: - - "${CNV_SERVICE_PORT}:8870" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.cnv-service.rule=Host(`cnv-service.${CANDIG_DOMAIN}`)" - - "traefik.http.services.cnv-service.loadbalancer.server.port=${CNV_SERVICE_PORT}" - logging: *default-logging - command: ["--host", "0.0.0.0", "--port", "8870"] diff --git a/lib/consul/docker-compose.yml b/lib/consul/docker-compose.yml deleted file mode 100644 index 02d5eac44..000000000 --- a/lib/consul/docker-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: '3.7' - -services: - consul: - image: consul:${CONSUL_VERSION:-lateset} - volumes: - - consul-data:/consul/data - networks: - - ${DOCKER_NET} - ports: - # RPC PORT 8400 in -server mode - - "${CONSUL_RPC_PORT}:8502" - - "${CONSUL_HTTP_PORT}:8500" - - "${CONSUL_DNS_PORT}:8600/udp" - - "${CONSUL_LAN_PORT}:8301/tcp" - - "${CONSUL_LAN_PORT}:8301/udp" - - "${CONSUL_WAN_PORT}:8302/tcp" - - "${CONSUL_WAN_PORT}:8302/udp" - deploy: - placement: - constraints: - - node.role == manager - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.consul.rule=Host(`consul.${CANDIG_DOMAIN}`)" - - "traefik.http.services.consul.loadbalancer.server.port=${CONSUL_HTTP_PORT}" - logging: *default-logging - -# TODO: update consul config for server/agent mode -# TODO: test consul DNS resolution diff --git a/lib/datasets/datasets_service b/lib/datasets/datasets_service deleted file mode 160000 index 8f0403fff..000000000 --- a/lib/datasets/datasets_service +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8f0403fffeb4974a90e00eb6add7c20bbe301f8c diff --git a/lib/datasets/docker-compose.yml b/lib/datasets/docker-compose.yml deleted file mode 100644 index dcb57180b..000000000 --- a/lib/datasets/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3.7' - -services: - datasets: - build: - context: $PWD/lib/datasets/datasets_service - args: - venv_python: "${VENV_PYTHON}" - image: ${DOCKER_REGISTRY}/datasets:${DATASETS_VERSION:-latest} - volumes: - - datasets-data:/app/datasets_service/tmp - networks: - - ${DOCKER_NET} - ports: - - "${DATASETS_PORT}:8880" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.datasets.rule=Host(`datasets.${CANDIG_DOMAIN}`)" - - "traefik.http.services.datasets.loadbalancer.server.port=${DATSETS_PORT}" - logging: *default-logging - command: ["--host", "0.0.0.0", "--port", "8880", "--database", "datasets.db", "--logfile", "datasets.log"] diff --git a/lib/federation-service/docker-compose.yml b/lib/federation-service/docker-compose.yml index fa5b00fac..e518288a6 100644 --- a/lib/federation-service/docker-compose.yml +++ b/lib/federation-service/docker-compose.yml @@ -8,25 +8,8 @@ services: venv_python: "${VENV_PYTHON}" alpine_version: "${ALPINE_VERSION}" image: ${DOCKER_REGISTRY}/federation-service:${FEDERATION_VERSION:-latest} - networks: - - ${DOCKER_NET} ports: - "${FEDERATION_PORT}:4232" - deploy: - placement: - constraints: - - node.role == manager - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.federation-service.rule=Host(`federation-service.${CANDIG_DOMAIN}`)" - - "traefik.http.services.federation-service.loadbalancer.server.port=${FEDERATION_PORT}" - logging: *default-logging secrets: - source: federation-servers target: /app/federation_service/configs/servers.json diff --git a/lib/graphql/GraphQL-interface b/lib/graphql/GraphQL-interface deleted file mode 160000 index 75fab0b73..000000000 --- a/lib/graphql/GraphQL-interface +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 75fab0b73b69173c97dfaccc6f8645d989ca373a diff --git a/lib/graphql/docker-compose.yml b/lib/graphql/docker-compose.yml deleted file mode 100644 index 2d15b96c1..000000000 --- a/lib/graphql/docker-compose.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: '3.7' - -services: - graphql: - build: - context: $PWD/lib/graphql/GraphQL-interface - args: - GRAPHQL_PYTHON_VERSION: "${GRAPHQL_PYTHON_VERSION}" - image: ${DOCKER_REGISTRY}/graphql:${GRAPHQL_INTERFACE_VERSION:-latest} - networks: - - ${DOCKER_NET} - ports: - - "${GRAPHQL_INTERFACE_PORT}:7999" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.graphql.rule=Host(`graphql.${CANDIG_DOMAIN}`)" - - "traefik.http.routers.graphql.entrypoints=${TRAEFIK_ENTRYPOINT}" - - "traefik.http.services.graphql.loadbalancer.server.port=${GRAPHQL_INTERFACE_PORT}" - logging: *default-logging - environment: - - GRAPHQL_KATSU_API=${GRAPHQL_KATSU_API} - - GRAPHQL_CANDIG_SERVER=${GRAPHQL_CANDIG_SERVER} - - GRAPHQL_BEACON_ID=${GRAPHQL_BEACON_ID} - - GRAPHQL_KATSU_TOKEN_KEY=${GRAPHQL_KATSU_TOKEN_KEY} - - GRAPHQL_CANDIG_TOKEN_KEY=${GRAPHQL_CANDIG_TOKEN_KEY} diff --git a/lib/htsget-server/docker-compose.yml b/lib/htsget-server/docker-compose.yml index 46c14eb84..474871934 100644 --- a/lib/htsget-server/docker-compose.yml +++ b/lib/htsget-server/docker-compose.yml @@ -10,36 +10,13 @@ services: image: ${DOCKER_REGISTRY}/htsget-app:${HTSGET_APP_VERSION:-latest} volumes: - htsget-data:/data - networks: - - ${DOCKER_NET} ports: - "${HTSGET_APP_PORT}:3000" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.htsget-app.rule=Host(`htsget-app.${CANDIG_DOMAIN}`)" - - "traefik.http.services.htsget-app.loadbalancer.server.port=${HTSGET_APP_PORT}" secrets: - - source: minio-access-key - target: access - - source: minio-secret-key - target: secret - - source: selfsigned-site-pem - target: cert.pem - source: vault-s3-token target: vault-s3-token - source: opa-service-token target: opa-service-token - logging: *default-logging environment: HTSGET_TEST_KEY: "hoodlebug" HTSGET_URL: ${TYK_LOGIN_TARGET_URL}/${TYK_HTSGET_API_LISTEN_PATH} diff --git a/lib/igv-js/Dockerfile b/lib/igv-js/Dockerfile deleted file mode 100644 index a6d7ce6ab..000000000 --- a/lib/igv-js/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM alpine:latest -LABEL maintainer=CanDIG -ENV NODE_ENV=production - -WORKDIR /var/www - -RUN apk update -RUN apk add nodejs yarn - -ADD package.json . - -RUN yarn add express --save -RUN yarn add pug --save -RUN yarn add helmet --save -RUN yarn add compression --save -RUN yarn add igv --save - -ADD app.js . -COPY html /var/www/html -RUN cp node_modules/igv/dist/igv.min.js html/assets/js/ - -ENTRYPOINT ["/usr/bin/node", "app.js"] diff --git a/lib/igv-js/app.js b/lib/igv-js/app.js deleted file mode 100644 index cbe034815..000000000 --- a/lib/igv-js/app.js +++ /dev/null @@ -1,38 +0,0 @@ -var express = require('express') -var helmet = require('helmet') -var os = require("os") -var path = require('path') -var compression = require('compression') - -var app = express() -var hostname = os.hostname() -var p = path.join(__dirname, '/html') - -app.use(helmet()) -app.use(express.static(p)) -app.use(compression()) -app.set('views', p) -app.set('view engine', 'pug') - -app.get('/', function (req, res) { - res.render('index', { - title: 'igv.js Demo App v1', - message1: 'IGV.js Web App', - message2: 'App Version: v1', - message3: "Hostname: " + hostname, - genome: "hg19", - locus: "chr8:128,747,267-128,754,546", - tracks: [ - { - type: 'alignment', - sourceType: 'htsget', - endpoint: 'https://htsnexus.rnd.dnanex.us/v1', - id: 'BroadHiSeqX_b37/NA12878', - name: 'NA12878' - }] - }) -}) - -app.listen(80, function () { - console.log('app listening on port 80!') -}) diff --git a/lib/igv-js/docker-compose.yml b/lib/igv-js/docker-compose.yml deleted file mode 100644 index 29e8cf892..000000000 --- a/lib/igv-js/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: '3.7' - -services: - igv-js: - build: - context: $PWD/lib/igv-js - image: ${DOCKER_REGISTRY}/igv-js:${IGVJS_VERSION:-latest} - networks: - - ${DOCKER_NET} - ports: - - "${IGVJS_PORT}:80" - deploy: - placement: - constraints: - - node.role == worker - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - labels: - - "traefik.enable=true" - - "traefik.docker.lbswarm=true" - - "traefik.http.routers.igv.rule=Host(`igv.${CANDIG_DOMAIN}`)" - - "traefik.http.routers.igv.entrypoints=${TRAEFIK_ENTRYPOINT}" - - "traefik.http.services.igv.loadbalancer.server.port=${IGVJS_PORT}" - logging: *default-logging diff --git a/lib/igv-js/html/.index.html b/lib/igv-js/html/.index.html deleted file mode 100644 index 164391529..000000000 --- a/lib/igv-js/html/.index.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - Kubernetes Demo v1 - - - - - - - - - -
- - - -
- - -
- - - - - - - - - diff --git a/lib/igv-js/html/assets/css/font-awesome.min.css b/lib/igv-js/html/assets/css/font-awesome.min.css deleted file mode 100644 index 9b27f8ea8..000000000 --- a/lib/igv-js/html/assets/css/font-awesome.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/*! - * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.3') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/lib/igv-js/html/assets/css/ie9.css b/lib/igv-js/html/assets/css/ie9.css deleted file mode 100644 index 92031cc80..000000000 --- a/lib/igv-js/html/assets/css/ie9.css +++ /dev/null @@ -1,35 +0,0 @@ -/* - Dimension by HTML5 UP - html5up.net | @ajlkn - Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) -*/ - -/* BG */ - - #bg:before { - background: rgba(19, 21, 25, 0.5); - } - -/* Header */ - - #header .logo { - margin: 0 auto; - } - - #header .content { - display: inline-block; - } - - #header nav ul { - display: inline-block; - } - - #header nav ul li { - display: inline-block; - } - -/* Main */ - - #main article { - margin: 0 auto; - } \ No newline at end of file diff --git a/lib/igv-js/html/assets/css/main.css b/lib/igv-js/html/assets/css/main.css deleted file mode 100644 index f9943f8a3..000000000 --- a/lib/igv-js/html/assets/css/main.css +++ /dev/null @@ -1,1594 +0,0 @@ -@import url(font-awesome.min.css); -@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300italic,600italic,300,600"); - -/* - Dimension by HTML5 UP - html5up.net | @ajlkn - Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) -*/ - -/* Reset */ - - html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; - } - - article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { - display: block; - } - - body { - line-height: 1; - } - - ol, ul { - list-style: none; - } - - blockquote, q { - quotes: none; - } - - blockquote:before, blockquote:after, q:before, q:after { - content: ''; - content: none; - } - - table { - border-collapse: collapse; - border-spacing: 0; - } - - body { - -webkit-text-size-adjust: none; - } - -/* Box Model */ - - *, *:before, *:after { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - } - -/* Basic */ - - @-ms-viewport { - width: device-width; - } - - @media screen and (max-width: 480px) { - - html, body { - min-width: 320px; - } - - } - - body { - background: #1b1f22; - } - - body.is-loading *, body.is-loading *:before, body.is-loading *:after, body.is-switching *, body.is-switching *:before, body.is-switching *:after { - -moz-animation: none !important; - -webkit-animation: none !important; - -ms-animation: none !important; - animation: none !important; - -moz-transition: none !important; - -webkit-transition: none !important; - -ms-transition: none !important; - transition: none !important; - -moz-transition-delay: none !important; - -webkit-transition-delay: none !important; - -ms-transition-delay: none !important; - transition-delay: none !important; - } - -/* Type */ - - html { - font-size: 16pt; - } - - @media screen and (max-width: 1680px) { - - html { - font-size: 12pt; - } - - } - - @media screen and (max-width: 736px) { - - html { - font-size: 11pt; - } - - } - - @media screen and (max-width: 360px) { - - html { - font-size: 10pt; - } - - } - - body, input, select, textarea { - color: #ffffff; - font-family: "Source Sans Pro", sans-serif; - font-weight: 300; - font-size: 1rem; - line-height: 1.65; - } - - a { - -moz-transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; - -webkit-transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; - -ms-transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; - transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; - border-bottom: dotted 1px rgba(255, 255, 255, 0.5); - text-decoration: none; - color: inherit; - } - - a:hover { - border-bottom-color: transparent; - } - - strong, b { - color: #ffffff; - font-weight: 600; - } - - em, i { - font-style: italic; - } - - p { - margin: 0 0 2rem 0; - } - - h1, h2, h3, h4, h5, h6 { - color: #ffffff; - font-weight: 600; - line-height: 1.5; - margin: 0 0 1rem 0; - text-transform: uppercase; - letter-spacing: 0.2rem; - } - - h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { - color: inherit; - text-decoration: none; - } - - h1.major, h2.major, h3.major, h4.major, h5.major, h6.major { - border-bottom: solid 1px #ffffff; - width: -moz-max-content; - width: -webkit-max-content; - width: -ms-max-content; - width: max-content; - padding-bottom: 0.5rem; - margin: 0 0 2rem 0; - } - - h1 { - font-size: 2.25rem; - line-height: 1.3; - letter-spacing: 0.5rem; - } - - h2 { - font-size: 1.5rem; - line-height: 1.4; - letter-spacing: 0.5rem; - } - - h3 { - font-size: 1rem; - } - - h4 { - font-size: 0.8rem; - } - - h5 { - font-size: 0.7rem; - } - - h6 { - font-size: 0.6rem; - } - - @media screen and (max-width: 736px) { - - h1 { - font-size: 1.75rem; - line-height: 1.4; - } - - h2 { - font-size: 1.25em; - line-height: 1.5; - } - - } - - sub { - font-size: 0.8rem; - position: relative; - top: 0.5rem; - } - - sup { - font-size: 0.8rem; - position: relative; - top: -0.5rem; - } - - blockquote { - border-left: solid 4px #ffffff; - font-style: italic; - margin: 0 0 2rem 0; - padding: 0.5rem 0 0.5rem 2rem; - } - - code { - background: rgba(255, 255, 255, 0.075); - border-radius: 4px; - font-family: "Courier New", monospace; - font-size: 0.9rem; - margin: 0 0.25rem; - padding: 0.25rem 0.65rem; - } - - pre { - -webkit-overflow-scrolling: touch; - font-family: "Courier New", monospace; - font-size: 0.9rem; - margin: 0 0 2rem 0; - } - - pre code { - display: block; - line-height: 1.75; - padding: 1rem 1.5rem; - overflow-x: auto; - } - - hr { - border: 0; - border-bottom: solid 1px #ffffff; - margin: 2.75rem 0; - } - - .align-left { - text-align: left; - } - - .align-center { - text-align: center; - } - - .align-right { - text-align: right; - } - -/* Form */ - - form { - margin: 0 0 2.5rem 0; - } - - form .field { - margin: 0 0 1.5rem 0; - } - - form .field.half { - width: 50%; - float: left; - padding: 0 0 0 0.75rem; - } - - form .field.half.first { - padding: 0 0.75rem 0 0; - } - - form > .actions { - margin: 1.875rem 0 0 0 !important; - } - - @media screen and (max-width: 736px) { - - form .field { - margin: 0 0 1.125rem 0; - } - - form .field.half { - padding: 0 0 0 0.5625rem; - } - - form .field.half.first { - padding: 0 0.5625rem 0 0; - } - - form > .actions { - margin: 1.5rem 0 0 0 !important; - } - - } - - @media screen and (max-width: 480px) { - - form .field.half { - width: 100%; - float: none; - padding: 0; - } - - form .field.half.first { - padding: 0; - } - - } - - label { - color: #ffffff; - display: block; - font-size: 0.8rem; - font-weight: 300; - letter-spacing: 0.2rem; - line-height: 1.5; - margin: 0 0 1rem 0; - text-transform: uppercase; - } - - input[type="text"], - input[type="password"], - input[type="email"], - input[type="tel"], - select, - textarea { - -moz-appearance: none; - -webkit-appearance: none; - -ms-appearance: none; - appearance: none; - -moz-transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - -webkit-transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - -ms-transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - background: transparent; - border-radius: 4px; - border: solid 1px #ffffff; - color: inherit; - display: block; - outline: 0; - padding: 0 1rem; - text-decoration: none; - width: 100%; - } - - input[type="text"]:invalid, - input[type="password"]:invalid, - input[type="email"]:invalid, - input[type="tel"]:invalid, - select:invalid, - textarea:invalid { - box-shadow: none; - } - - input[type="text"]:focus, - input[type="password"]:focus, - input[type="email"]:focus, - input[type="tel"]:focus, - select:focus, - textarea:focus { - background: rgba(255, 255, 255, 0.075); - border-color: #ffffff; - box-shadow: 0 0 0 1px #ffffff; - } - - select option { - background: #1b1f22; - color: #ffffff; - } - - .select-wrapper { - text-decoration: none; - display: block; - position: relative; - } - - .select-wrapper:before { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - font-family: FontAwesome; - font-style: normal; - font-weight: normal; - text-transform: none !important; - } - - .select-wrapper:before { - color: #ffffff; - content: '\f107'; - display: block; - height: 2.75rem; - line-height: calc(2.75rem + 0em); - pointer-events: none; - position: absolute; - right: 0; - text-align: center; - top: 0; - width: 2.75rem; - } - - .select-wrapper select::-ms-expand { - display: none; - } - - input[type="text"], - input[type="password"], - input[type="email"], - select { - height: 2.75rem; - } - - textarea { - padding: 0.75rem 1rem; - } - - input[type="checkbox"], - input[type="radio"] { - -moz-appearance: none; - -webkit-appearance: none; - -ms-appearance: none; - appearance: none; - display: block; - float: left; - margin-right: -2rem; - opacity: 0; - width: 1rem; - z-index: -1; - } - - input[type="checkbox"] + label, - input[type="radio"] + label { - text-decoration: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - color: #ffffff; - cursor: pointer; - display: inline-block; - font-size: 0.8rem; - font-weight: 300; - margin: 0 0 0.5rem 0; - padding-left: 2.65rem; - padding-right: 0.75rem; - position: relative; - } - - input[type="checkbox"] + label:before, - input[type="radio"] + label:before { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - font-family: FontAwesome; - font-style: normal; - font-weight: normal; - text-transform: none !important; - } - - input[type="checkbox"] + label:before, - input[type="radio"] + label:before { - -moz-transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - -webkit-transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - -ms-transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - border-radius: 4px; - border: solid 1px #ffffff; - content: ''; - display: inline-block; - height: 1.65rem; - left: 0; - line-height: calc(1.58125rem + 0em); - position: absolute; - text-align: center; - top: -0.125rem; - width: 1.65rem; - } - - input[type="checkbox"]:checked + label:before, - input[type="radio"]:checked + label:before { - background: #ffffff !important; - border-color: #ffffff !important; - color: #1b1f22; - content: '\f00c'; - } - - input[type="checkbox"]:focus + label:before, - input[type="radio"]:focus + label:before { - background: rgba(255, 255, 255, 0.075); - border-color: #ffffff; - box-shadow: 0 0 0 1px #ffffff; - } - - input[type="checkbox"] + label:before { - border-radius: 4px; - } - - input[type="radio"] + label:before { - border-radius: 100%; - } - - ::-webkit-input-placeholder { - color: rgba(255, 255, 255, 0.5) !important; - opacity: 1.0; - } - - :-moz-placeholder { - color: rgba(255, 255, 255, 0.5) !important; - opacity: 1.0; - } - - ::-moz-placeholder { - color: rgba(255, 255, 255, 0.5) !important; - opacity: 1.0; - } - - :-ms-input-placeholder { - color: rgba(255, 255, 255, 0.5) !important; - opacity: 1.0; - } - - .formerize-placeholder { - color: rgba(255, 255, 255, 0.5) !important; - opacity: 1.0; - } - -/* Box */ - - .box { - border-radius: 4px; - border: solid 1px #ffffff; - margin-bottom: 2rem; - padding: 1.5em; - } - - .box > :last-child, - .box > :last-child > :last-child, - .box > :last-child > :last-child > :last-child { - margin-bottom: 0; - } - - .box.alt { - border: 0; - border-radius: 0; - padding: 0; - } - -/* Icon */ - - .icon { - text-decoration: none; - border-bottom: none; - position: relative; - } - - .icon:before { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - font-family: FontAwesome; - font-style: normal; - font-weight: normal; - text-transform: none !important; - } - - .icon > .label { - display: none; - } - -/* Image */ - - .image { - border-radius: 4px; - border: 0; - display: inline-block; - position: relative; - } - - .image:before { - -moz-pointer-events: none; - -webkit-pointer-events: none; - -ms-pointer-events: none; - pointer-events: none; - background-image: url("../../images/overlay.png"); - background-color: rgba(19, 21, 25, 0.5); - border-radius: 4px; - content: ''; - display: block; - height: 100%; - left: 0; - opacity: 0.5; - position: absolute; - top: 0; - width: 100%; - } - - .image img { - border-radius: 4px; - display: block; - } - - .image.left, .image.right { - max-width: 40%; - } - - .image.left img, .image.right img { - width: 100%; - } - - .image.left { - float: left; - padding: 0 1.5em 1em 0; - top: 0.25em; - } - - .image.right { - float: right; - padding: 0 0 1em 1.5em; - top: 0.25em; - } - - .image.fit { - display: block; - margin: 0 0 2rem 0; - width: 100%; - } - - .image.fit img { - width: 100%; - } - - .image.main { - display: block; - margin: 2.5rem 0; - width: 100%; - } - - .image.main img { - width: 100%; - } - - @media screen and (max-width: 736px) { - - .image.main { - margin: 2rem 0; - } - - } - - @media screen and (max-width: 480px) { - - .image.main { - margin: 1.5rem 0; - } - - } - -/* List */ - - ol { - list-style: decimal; - margin: 0 0 2rem 0; - padding-left: 1.25em; - } - - ol li { - padding-left: 0.25em; - } - - ul { - list-style: disc; - margin: 0 0 2rem 0; - padding-left: 1em; - } - - ul li { - padding-left: 0.5em; - } - - ul.alt { - list-style: none; - padding-left: 0; - } - - ul.alt li { - border-top: solid 1px #ffffff; - padding: 0.5em 0; - } - - ul.alt li:first-child { - border-top: 0; - padding-top: 0; - } - - ul.icons { - cursor: default; - list-style: none; - padding-left: 0; - } - - ul.icons li { - display: inline-block; - padding: 0 0.75em 0 0; - } - - ul.icons li:last-child { - padding-right: 0; - } - - ul.icons li a { - border-radius: 100%; - box-shadow: inset 0 0 0 1px #ffffff; - display: inline-block; - height: 2.25rem; - line-height: 2.25rem; - text-align: center; - width: 2.25rem; - } - - ul.icons li a:hover { - background-color: rgba(255, 255, 255, 0.075); - } - - ul.icons li a:active { - background-color: rgba(255, 255, 255, 0.175); - } - - ul.actions { - cursor: default; - list-style: none; - padding-left: 0; - } - - ul.actions li { - display: inline-block; - padding: 0 1rem 0 0; - vertical-align: middle; - } - - ul.actions li:last-child { - padding-right: 0; - } - - ul.actions.small li { - padding: 0 0.5rem 0 0; - } - - ul.actions.vertical li { - display: block; - padding: 1rem 0 0 0; - } - - ul.actions.vertical li:first-child { - padding-top: 0; - } - - ul.actions.vertical li > * { - margin-bottom: 0; - } - - ul.actions.vertical.small li { - padding: 0.5rem 0 0 0; - } - - ul.actions.vertical.small li:first-child { - padding-top: 0; - } - - ul.actions.fit { - display: table; - margin-left: -1rem; - padding: 0; - table-layout: fixed; - width: calc(100% + 1rem); - } - - ul.actions.fit li { - display: table-cell; - padding: 0 0 0 1rem; - } - - ul.actions.fit li > * { - margin-bottom: 0; - } - - ul.actions.fit.small { - margin-left: -0.5rem; - width: calc(100% + 0.5rem); - } - - ul.actions.fit.small li { - padding: 0 0 0 0.5rem; - } - - @media screen and (max-width: 480px) { - - ul.actions { - margin: 0 0 2rem 0; - } - - ul.actions li { - padding: 1rem 0 0 0; - display: block; - text-align: center; - width: 100%; - } - - ul.actions li:first-child { - padding-top: 0; - } - - ul.actions li > * { - width: 100%; - margin: 0 !important; - } - - ul.actions li > *.icon:before { - margin-left: -2em; - } - - ul.actions.small li { - padding: 0.5rem 0 0 0; - } - - ul.actions.small li:first-child { - padding-top: 0; - } - - } - - dl { - margin: 0 0 2rem 0; - } - - dl dt { - display: block; - font-weight: 600; - margin: 0 0 1rem 0; - } - - dl dd { - margin-left: 2rem; - } - -/* Table */ - - .table-wrapper { - -webkit-overflow-scrolling: touch; - overflow-x: auto; - } - - table { - margin: 0 0 2rem 0; - width: 100%; - } - - table tbody tr { - border: solid 1px #ffffff; - border-left: 0; - border-right: 0; - } - - table tbody tr:nth-child(2n + 1) { - background-color: rgba(255, 255, 255, 0.075); - } - - table td { - padding: 0.75em 0.75em; - } - - table th { - color: #ffffff; - font-size: 0.9em; - font-weight: 600; - padding: 0 0.75em 0.75em 0.75em; - text-align: left; - } - - table thead { - border-bottom: solid 2px #ffffff; - } - - table tfoot { - border-top: solid 2px #ffffff; - } - - table.alt { - border-collapse: separate; - } - - table.alt tbody tr td { - border: solid 1px #ffffff; - border-left-width: 0; - border-top-width: 0; - } - - table.alt tbody tr td:first-child { - border-left-width: 1px; - } - - table.alt tbody tr:first-child td { - border-top-width: 1px; - } - - table.alt thead { - border-bottom: 0; - } - - table.alt tfoot { - border-top: 0; - } - -/* Button */ - - input[type="submit"], - input[type="reset"], - input[type="button"], - button, - .button { - -moz-appearance: none; - -webkit-appearance: none; - -ms-appearance: none; - appearance: none; - -moz-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; - -webkit-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; - -ms-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; - transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; - background-color: transparent; - border-radius: 4px; - border: 0; - box-shadow: inset 0 0 0 1px #ffffff; - color: #ffffff !important; - cursor: pointer; - display: inline-block; - font-size: 0.8rem; - font-weight: 300; - height: 2.75rem; - letter-spacing: 0.2rem; - line-height: 2.75rem; - outline: 0; - padding: 0 1.25rem 0 1.35rem; - text-align: center; - text-decoration: none; - text-transform: uppercase; - white-space: nowrap; - } - - input[type="submit"]:hover, - input[type="reset"]:hover, - input[type="button"]:hover, - button:hover, - .button:hover { - background-color: rgba(255, 255, 255, 0.075); - } - - input[type="submit"]:active, - input[type="reset"]:active, - input[type="button"]:active, - button:active, - .button:active { - background-color: rgba(255, 255, 255, 0.175); - } - - input[type="submit"].icon:before, - input[type="reset"].icon:before, - input[type="button"].icon:before, - button.icon:before, - .button.icon:before { - margin-right: 0.5em; - } - - input[type="submit"].fit, - input[type="reset"].fit, - input[type="button"].fit, - button.fit, - .button.fit { - display: block; - margin: 0 0 1rem 0; - width: 100%; - } - - input[type="submit"].special, - input[type="reset"].special, - input[type="button"].special, - button.special, - .button.special { - background-color: #ffffff; - color: #1b1f22 !important; - font-weight: 600; - } - - input[type="submit"].disabled, input[type="submit"]:disabled, - input[type="reset"].disabled, - input[type="reset"]:disabled, - input[type="button"].disabled, - input[type="button"]:disabled, - button.disabled, - button:disabled, - .button.disabled, - .button:disabled { - -moz-pointer-events: none; - -webkit-pointer-events: none; - -ms-pointer-events: none; - pointer-events: none; - cursor: default; - opacity: 0.25; - } - - input[type="submit"], - input[type="reset"], - input[type="button"], - button { - line-height: calc(2.75rem - 2px); - } - -/* BG */ - - #bg { - -moz-transform: scale(1.0); - -webkit-transform: scale(1.0); - -ms-transform: scale(1.0); - transform: scale(1.0); - -webkit-backface-visibility: hidden; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100vh; - z-index: 1; - } - - #bg:before, #bg:after { - content: ''; - display: block; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - #bg:before { - -moz-transition: background-color 2.5s ease-in-out; - -webkit-transition: background-color 2.5s ease-in-out; - -ms-transition: background-color 2.5s ease-in-out; - transition: background-color 2.5s ease-in-out; - -moz-transition-delay: 0.75s; - -webkit-transition-delay: 0.75s; - -ms-transition-delay: 0.75s; - transition-delay: 0.75s; - background-image: linear-gradient(to top, rgba(19, 21, 25, 0.5), rgba(19, 21, 25, 0.5)), url("../../images/overlay.png"); - background-size: auto, 256px 256px; - background-position: center, center; - background-repeat: no-repeat, repeat; - z-index: 2; - } - - #bg:after { - -moz-transform: scale(1.125); - -webkit-transform: scale(1.125); - -ms-transform: scale(1.125); - transform: scale(1.125); - -moz-transition: -moz-transform 0.325s ease-in-out, -moz-filter 0.325s ease-in-out; - -webkit-transition: -webkit-transform 0.325s ease-in-out, -webkit-filter 0.325s ease-in-out; - -ms-transition: -ms-transform 0.325s ease-in-out, -ms-filter 0.325s ease-in-out; - transition: transform 0.325s ease-in-out, filter 0.325s ease-in-out; - background-image: url("../../images/bg.jpg"); - background-position: center; - background-size: cover; - background-repeat: no-repeat; - z-index: 1; - } - - body.is-article-visible #bg:after { - -moz-transform: scale(1.0825); - -webkit-transform: scale(1.0825); - -ms-transform: scale(1.0825); - transform: scale(1.0825); - -moz-filter: blur(0.2rem); - -webkit-filter: blur(0.2rem); - -ms-filter: blur(0.2rem); - filter: blur(0.2rem); - } - - body.is-loading #bg:before { - background-color: #000000; - } - -/* Wrapper */ - - #wrapper { - display: -moz-flex; - display: -webkit-flex; - display: -ms-flex; - display: flex; - -moz-flex-direction: column; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -moz-align-items: center; - -webkit-align-items: center; - -ms-align-items: center; - align-items: center; - -moz-justify-content: space-between; - -webkit-justify-content: space-between; - -ms-justify-content: space-between; - justify-content: space-between; - position: relative; - min-height: 100vh; - width: 100%; - padding: 4rem 2rem; - z-index: 3; - } - - #wrapper:before { - content: ''; - display: block; - } - - @media screen and (max-width: 1680px) { - - #wrapper { - padding: 3rem 2rem; - } - - } - - @media screen and (max-width: 736px) { - - #wrapper { - padding: 2rem 1rem; - } - - } - - @media screen and (max-width: 480px) { - - #wrapper { - padding: 1rem; - } - - } - -/* Header */ - - #header { - display: -moz-flex; - display: -webkit-flex; - display: -ms-flex; - display: flex; - -moz-flex-direction: column; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -moz-align-items: center; - -webkit-align-items: center; - -ms-align-items: center; - align-items: center; - -moz-transition: -moz-transform 0.325s ease-in-out, -moz-filter 0.325s ease-in-out, opacity 0.325s ease-in-out; - -webkit-transition: -webkit-transform 0.325s ease-in-out, -webkit-filter 0.325s ease-in-out, opacity 0.325s ease-in-out; - -ms-transition: -ms-transform 0.325s ease-in-out, -ms-filter 0.325s ease-in-out, opacity 0.325s ease-in-out; - transition: transform 0.325s ease-in-out, filter 0.325s ease-in-out, opacity 0.325s ease-in-out; - background-image: -moz-radial-gradient(rgba(0, 0, 0, 0.25) 25%, transparent 55%); - background-image: -webkit-radial-gradient(rgba(0, 0, 0, 0.25) 25%, transparent 55%); - background-image: -ms-radial-gradient(rgba(0, 0, 0, 0.25) 25%, transparent 55%); - background-image: radial-gradient(rgba(0, 0, 0, 0.25) 25%, transparent 55%); - max-width: 100%; - text-align: center; - } - - #header > * { - -moz-transition: opacity 0.325s ease-in-out; - -webkit-transition: opacity 0.325s ease-in-out; - -ms-transition: opacity 0.325s ease-in-out; - transition: opacity 0.325s ease-in-out; - position: relative; - margin-top: 3.5rem; - } - - #header > *:before { - content: ''; - display: block; - position: absolute; - top: calc(-3.5rem - 1px); - left: calc(50% - 1px); - width: 1px; - height: calc(3.5rem + 1px); - background: #ffffff; - } - - #header > :first-child { - margin-top: 0; - } - - #header > :first-child:before { - display: none; - } - - #header .logo { - width: 5.5rem; - height: 5.5rem; - line-height: 5.5rem; - border: solid 1px #ffffff; - border-radius: 100%; - } - - #header .logo .icon:before { - font-size: 2rem; - } - - #header .content { - border-style: solid; - border-color: #ffffff; - border-top-width: 1px; - border-bottom-width: 1px; - max-width: 100%; - } - - #header .content .inner { - -moz-transition: max-height 0.75s ease, padding 0.75s ease, opacity 0.325s ease-in-out; - -webkit-transition: max-height 0.75s ease, padding 0.75s ease, opacity 0.325s ease-in-out; - -ms-transition: max-height 0.75s ease, padding 0.75s ease, opacity 0.325s ease-in-out; - transition: max-height 0.75s ease, padding 0.75s ease, opacity 0.325s ease-in-out; - -moz-transition-delay: 0.25s; - -webkit-transition-delay: 0.25s; - -ms-transition-delay: 0.25s; - transition-delay: 0.25s; - padding: 3rem 2rem; - max-height: 40rem; - overflow: hidden; - } - - #header .content .inner > :last-child { - margin-bottom: 0; - } - - #header .content p { - text-transform: uppercase; - letter-spacing: 0.2rem; - font-size: 0.8rem; - line-height: 2; - } - - #header nav ul { - display: -moz-flex; - display: -webkit-flex; - display: -ms-flex; - display: flex; - margin-bottom: 0; - list-style: none; - padding-left: 0; - border: solid 1px #ffffff; - border-radius: 4px; - } - - #header nav ul li { - padding-left: 0; - border-left: solid 1px #ffffff; - } - - #header nav ul li:first-child { - border-left: 0; - } - - #header nav ul li a { - display: block; - min-width: 7.5rem; - height: 2.75rem; - line-height: 2.75rem; - padding: 0 1.25rem 0 1.45rem; - text-transform: uppercase; - letter-spacing: 0.2rem; - font-size: 0.8rem; - border-bottom: 0; - } - - #header nav ul li a:hover { - background-color: rgba(255, 255, 255, 0.075); - } - - #header nav ul li a:active { - background-color: rgba(255, 255, 255, 0.175); - } - - #header nav.use-middle:after { - content: ''; - display: block; - position: absolute; - top: 0; - left: calc(50% - 1px); - width: 1px; - height: 100%; - background: #ffffff; - } - - #header nav.use-middle ul li.is-middle { - border-left: 0; - } - - body.is-article-visible #header { - -moz-transform: scale(0.95); - -webkit-transform: scale(0.95); - -ms-transform: scale(0.95); - transform: scale(0.95); - -moz-filter: blur(0.1rem); - -webkit-filter: blur(0.1rem); - -ms-filter: blur(0.1rem); - filter: blur(0.1rem); - opacity: 0; - } - - body.is-loading #header { - -moz-filter: blur(0.125rem); - -webkit-filter: blur(0.125rem); - -ms-filter: blur(0.125rem); - filter: blur(0.125rem); - } - - body.is-loading #header > * { - opacity: 0; - } - - body.is-loading #header .content .inner { - max-height: 0; - padding-top: 0; - padding-bottom: 0; - opacity: 0; - } - - @media screen and (max-width: 980px) { - - #header .content p br { - display: none; - } - - } - - @media screen and (max-width: 736px) { - - #header > * { - margin-top: 2rem; - } - - #header > *:before { - top: calc(-2rem - 1px); - height: calc(2rem + 1px); - } - - #header .logo { - width: 4.75rem; - height: 4.75rem; - line-height: 4.75rem; - } - - #header .logo .icon:before { - font-size: 1.75rem; - } - - #header .content .inner { - padding: 2.5rem 1rem; - } - - #header .content p { - line-height: 1.875; - } - - } - - @media screen and (max-width: 480px) { - - #header { - padding: 1.5rem 0; - } - - #header .content .inner { - padding: 2.5rem 0; - } - - #header nav ul { - -moz-flex-direction: column; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - min-width: 10rem; - max-width: 100%; - } - - #header nav ul li { - border-left: 0; - border-top: solid 1px #ffffff; - } - - #header nav ul li:first-child { - border-top: 0; - } - - #header nav ul li a { - height: 3rem; - line-height: 3rem; - min-width: 0; - width: 100%; - } - - #header nav.use-middle:after { - display: none; - } - - } - -/* Main */ - - #main { - -moz-flex-grow: 1; - -webkit-flex-grow: 1; - -ms-flex-grow: 1; - flex-grow: 1; - -moz-flex-shrink: 1; - -webkit-flex-shrink: 1; - -ms-flex-shrink: 1; - flex-shrink: 1; - display: -moz-flex; - display: -webkit-flex; - display: -ms-flex; - display: flex; - -moz-align-items: center; - -webkit-align-items: center; - -ms-align-items: center; - align-items: center; - -moz-justify-content: center; - -webkit-justify-content: center; - -ms-justify-content: center; - justify-content: center; - -moz-flex-direction: column; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - position: relative; - max-width: 100%; - z-index: 3; - } - - #main article { - -moz-transform: translateY(0.25rem); - -webkit-transform: translateY(0.25rem); - -ms-transform: translateY(0.25rem); - transform: translateY(0.25rem); - -moz-transition: opacity 0.325s ease-in-out, -moz-transform 0.325s ease-in-out; - -webkit-transition: opacity 0.325s ease-in-out, -webkit-transform 0.325s ease-in-out; - -ms-transition: opacity 0.325s ease-in-out, -ms-transform 0.325s ease-in-out; - transition: opacity 0.325s ease-in-out, transform 0.325s ease-in-out; - padding: 4.5rem 2.5rem 1.5rem 2.5rem ; - position: relative; - width: 40rem; - max-width: 100%; - background-color: rgba(27, 31, 34, 0.85); - border-radius: 4px; - opacity: 0; - } - - #main article.active { - -moz-transform: translateY(0); - -webkit-transform: translateY(0); - -ms-transform: translateY(0); - transform: translateY(0); - opacity: 1; - } - - #main article .close { - display: block; - position: absolute; - top: 0; - right: 0; - width: 4rem; - height: 4rem; - cursor: pointer; - text-indent: 4rem; - overflow: hidden; - white-space: nowrap; - } - - #main article .close:before { - -moz-transition: background-color 0.2s ease-in-out; - -webkit-transition: background-color 0.2s ease-in-out; - -ms-transition: background-color 0.2s ease-in-out; - transition: background-color 0.2s ease-in-out; - content: ''; - display: block; - position: absolute; - top: 0.75rem; - left: 0.75rem; - width: 2.5rem; - height: 2.5rem; - border-radius: 100%; - background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='20px' height='20px' viewBox='0 0 20 20' zoomAndPan='disable'%3E%3Cstyle%3Eline %7B stroke: %23ffffff%3B stroke-width: 1%3B %7D%3C/style%3E%3Cline x1='2' y1='2' x2='18' y2='18' /%3E%3Cline x1='18' y1='2' x2='2' y2='18' /%3E%3C/svg%3E"); - background-size: 20px 20px; - background-repeat: no-repeat; - } - - #main article .close:hover:before { - background-color: rgba(255, 255, 255, 0.075); - } - - #main article .close:active:before { - background-color: rgba(255, 255, 255, 0.175); - } - - @media screen and (max-width: 736px) { - - #main article { - padding: 3.5rem 2rem 0.5rem 2rem ; - } - - #main article .close:before { - top: 0.875rem; - left: 0.875rem; - width: 2.25rem; - height: 2.25rem; - background-size: 14px 14px; - } - - } - - @media screen and (max-width: 480px) { - - #main article { - padding: 3rem 1.5rem 0.5rem 1.5rem ; - } - - } - -/* Footer */ - - #footer { - -moz-transition: -moz-transform 0.325s ease-in-out, -moz-filter 0.325s ease-in-out, opacity 0.325s ease-in-out; - -webkit-transition: -webkit-transform 0.325s ease-in-out, -webkit-filter 0.325s ease-in-out, opacity 0.325s ease-in-out; - -ms-transition: -ms-transform 0.325s ease-in-out, -ms-filter 0.325s ease-in-out, opacity 0.325s ease-in-out; - transition: transform 0.325s ease-in-out, filter 0.325s ease-in-out, opacity 0.325s ease-in-out; - width: 100%; - max-width: 100%; - margin-top: 2rem; - text-align: center; - } - - #footer .copyright { - letter-spacing: 0.2rem; - font-size: 0.6rem; - opacity: 0.75; - margin-bottom: 0; - text-transform: uppercase; - } - - body.is-article-visible #footer { - -moz-transform: scale(0.95); - -webkit-transform: scale(0.95); - -ms-transform: scale(0.95); - transform: scale(0.95); - -moz-filter: blur(0.1rem); - -webkit-filter: blur(0.1rem); - -ms-filter: blur(0.1rem); - filter: blur(0.1rem); - opacity: 0; - } - - body.is-loading #footer { - opacity: 0; - } diff --git a/lib/igv-js/html/assets/css/noscript.css b/lib/igv-js/html/assets/css/noscript.css deleted file mode 100644 index 61ee6e1ea..000000000 --- a/lib/igv-js/html/assets/css/noscript.css +++ /dev/null @@ -1,12 +0,0 @@ -/* - Dimension by HTML5 UP - html5up.net | @ajlkn - Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) -*/ - -/* Main */ - - #main article { - opacity: 1; - margin: 4rem 0 0 0; - } \ No newline at end of file diff --git a/lib/igv-js/html/assets/fonts/FontAwesome.otf b/lib/igv-js/html/assets/fonts/FontAwesome.otf deleted file mode 100644 index d4de13e832d567ff29c5b4e9561b8c370348cc9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 124988 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ÐT?>-5aYhU|+1c>&YMpPK5x#-mbV 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'NS^o{z1ZKlHsAybrl=JE7AP369ntV%vB zEi(fm!=g{8NmLbAS6W|murNN}LRi&xxgDs@zBGi1(M#zXCC}RO%K1FKJ}+Ng`Z1Xj zzLz5o-@`Tk33-pKR1}nFgY#-ne05|w2XzHodXG5LH=3nO5+ac&+n0nexe3wwXidnt z_0d6mOhSBglse>BI;9Ai@r!4G5NH5~M4u|WTy{FzsUf~l7{uMh{(Hgv^qQ_nO;1lv z*BEdAizUo=*O0S3NKN`|B)vX;%#k%S(Hj_ytdp5QAx#ICcpzOlQz7;+u48K|D{BDn z1B)J=<`WR$;}cL>b3UNWfqg=8}T(hny6j8{Nj+%E{dst=9U zXTjV-tbXd**&xhe??iU&zRvCNcMRPYtEgO`yLOG5O7yKuX)r!hkSpO2(8pAOErL#+ ziVZ%_WwjI@D?g&BSiL-VxrUxY;Uwt2d~}-je=(S+J%1f(aUCwpZlI2qaY{ORsIk_ly zy811(S34DKuiV8Eu8xfO*XLwFN?4Rne8j4cC~Grxt-|rxdABPFy-9%fZ-i~%r{wKD z1@eYRs++hdaV+8_pU}^!6Q}waeTPrb@;TxILq?pBHOEIB98QnmiT(XU7&$LjSLv!#syUt6&et3Nwf5IXw&&@ntWgT`^fl{|xHig@ zTkdPOWRAvRd)0m~Ue)n=9U1vT|4^a&*?HWs`5y-;!mDEU?p6PCtMNf4cfRQOvFbC5 zLmP^guTUeQYce}h`NmeJx;aey1|m=63Gg&zJVc&`YNclJROfn9i@bNUe+w4umO-H& ziJX6IrlYHmZ;tmMzmGvN)qD4^gS~xYeKk8Xo2z4akE}r@C&&S9Mowmq;W+IVcSXIg zws;R$Q5s&Q$;#5DXYwhBlMnx&-o6B^sVn{aCLs&=gd~u}k%XJXy=vXH9hb3a)lRE* zVW^{Z1CdQxBt#Ju2nd3rtU(q*5V0;*Y{hCfJJe~XGo{;9+G?kJr?sux20HczFY5ny zF1gm}*O}-4e9!lM#>3g(^PYXV=e*0)mbN`F)E|SQ_4D67hgF$ZeY0@L3-|6>{u6|s!it^3$a#~52#xFUqM46b=9e!hTVUIf=icdFmvkt}ny|{b&OVl3 zz3+a@w!-YfY}?AgV~07`q`A`yQwuejMR_@ymWTLRsm~SIHZ*6=`t!*%_0L+Hn|Bp8 z+cS90v-G?Gg1>g&)Wj2eSyS`w=Ch}BPFp*+bscN6KUc}mI{db!F|RhW%$8b_R*|+> zV4fZCUGXnXVO?Qe{ipn@%qy1m#}6jj;*&SVTl3weW!vp#o#n50T3#;t!1ban9C}p1@ph&B6WeMccTLe(WwGiDxC^1^-bK6D8UhdU41KvPazd$t3xQ> zlBg-kNSq2D(1#$JaxX~tKq>7O=JxFo8tp=Mq+ml((axPk(7(ECm!&1?@@PJ{R8t0K zudQG3PeG_VFW;8`f_;C{evhXCi_o=!RdqWWva2+^i;MRZStr&A2?vDVZWIy|=B^ML zhHFZeI|n7$_;^0#atG)HBcPxx^<_RS|HW@#<7LkrK698K^VTz;J@aWJKkMMx)-%T- z9{DT}(Gb43dAqFSwU@elH9Y65?Rx1o+|i8!kt;u6J4AIOWYw^=rL9m?0J^@}>tOQ6UZoAM&_E^+A8L zLkR5(Q(7*_N-l&cy%!Yx5OZ;Y_WezQgmy>W2!i)_zcP_19Tvl169hC>r@ zx(l)N)=>Q>T3kbqu=S2n9l8MB77%{fXSj7*SmgUyDQtm$&YNk<+QiEEcuRaiLQbMB zcIcdif+ABR{87!nSOt3j(y#aeALM=L2J#tw=gs_(2@u2(_MFZ6d*x5D7lsKc%dM81 z)6}_XS+1?=n%Mn;a2RVTAb3CTs?ZV<0E_4?{glSXAwrfJ&vDv?nO`aIa_Hxl>~9_ zfOEOMZ(;8U^ZYdr&zraA&Cb`)zWL5OXCGc*>kSkxLlN9!!9ILEpm*=9x4fOuJ})z? zxH!|6w{m6v3UIA2y~u}t*>bh@6Z;46e&2E3YBZo})<6yCBf?@k8mI$Yrpe}SviP9c zZ`clQjBkyzrf<&8+-6Vbhb(~hHu+UeMP+qWwWTp;|F&|Qr~2fHS_^c@6_?sh#xHGN zW{q6xS_^eU8Jl-+wqk{wbr1G!r9EMFPIBs+9eFu<1^L^uvNLnGXxg@=G{jkjt#kSY z2(y=$EofS4e`W*$|lP0lP!_0mu-}7 zmAPfrvN~C#?5fNwyACBox8*9iNggU6As;V~kS~`<$>Zc1@~v{Wyii^xuaP&(TjVF? z?FyA*pn_8jQjAsHriY~?Himw#kDy{`g z44578P{8tlRRQY)QUcNgwg%(}>$VCsEN~T(iCgz zG*4)bYffqYrs>jL(tI4K3JeMy9{51uoWS{k8G)sN)qyVtz8v^k;2VJ#S%%fHI@ZYk zhUM5{>}Ylo8wn{3(QF#K8Ap^-wvs)_KFL1Eo@ZZU-()Y~!scW4M`9ww$#^o8EFeos z6p1Ap$tIFZ3dk-}Mm(gQyi2ZVS*=m)&<@v*(N54ls9ma!);hHr+G6br^_eE?8&LIdsExV|9~s({&H%mg?5)QgqvOb-I(fS9Sl;eWtsn z>(=$^ZtInLtv*Em8~tGYJ^J72BlPk5B)wZ-s&CdG(jV8K*1xEKMgKSbTl!D+JqD$L zH5d&84TB8B4WkW{4bu$|80HurHmoot8*&X5h8DvSL%ZPx!(R+<87>$u8oo931S^6~ z!NY>@3!WYv9y~YrvEWs~vB8^yvx5tQcLkRR*9SKT9|}Ged@A_);Fp8{8vJH(SMbH) zkAlAp{x+Y{-U?tPpodVaTqK+K?wgo(y>= zF`w9nLRI%GQb8V5y{SE=Yun{F06pUSYGznbmbz!EcM zfVkyR-||4Nx_KLJ^apGM8F%OV|Cz($b*`R@SdS7VFG33uPWxBD0tSNhKSSA-3Wum> zD54o)19$oW0iHgUWwa_YJ3G@ppD$)R)l(rfRON%7E$}=p12q}Wlml>nYxb!uL_Lfp z<2V}`!P!X{7!IRSAfm5Xh*am~QUy6Xa!YeOd0=$u8u%?s1=B<4@&)K7CBR8|up;2& zX_AeohNEI&ycbpV@Kn({RQB*IxyHJ>Mlu8FRS&Qvk`=nfaD3NHmP}-mIg*f)l0fcd z?Iav|tEO|r%*V5M%*ErW&@qq2n`GbVzp^EKItO`D5~rh|I+fDL|u9syIBIE z|3&%RE!)fTp$ULchCae`?C5@Ruvgw=39alm>~W5Ca(!`qar^+zC;s|4r~>d5Rar%m z9_P*#umpOZBS)bP(SCzDlEFbT6e-~N1`G!A>$SIQ7)P)~Oj0OjUr~r}N61Sp zg1Q1?9>=1wZJjEz^h{I-?TkotMkG3;BkIgj8$mNXz}+wnNQ4};C!l!#M%QX61X!ec zA#c||9s(sBv~}@#Ih=d5Q<1@_5<7 z!%u>zNFr(z5~!jUK7llBtIr}UxDsM6WBZ9z8FUCJIkB4#A{}SWbPy1TL3Axq&ErTi zn)0{s2K<&Kb=i#s-=HSq#$fp)Ro?eHr{2I3^-935Wl5OQOPWY%WY@swL1);ETM`^&ofSPkp%LPv&l}>0PaS zY1yduIo7_(pKyJX)pOcMy|mocyXIz;($&Jt*cM2j%JsSVa26vdJJU8FvlhHloijNy zft|#WNe@2AH*k|4yzr&Hw~GqBBIfZNakj>kIGx1Wmsrx5*q3M_?c50zZ;&X&3=={y zVMc)Ei1?`58N6JX@Qg_9yX61YlQCF7+*857buT7f(yf$|Bc%7J+d^c!3IbWPF4!SnVJXIEg>$LMP zgWGnhU6@Iw#$CnTdNo@K3VV1FES!H#c-cxG0(HCbsu13);kR&9(r9*RPC4{KHm<|> zWSkIb>B|7ux(kn9!M60uO*I)doLQ&jxOZjj%?|x^^xo{|UA22d!_#vkAy%{g*3Wt4+(vI86>>wSi)>04joOY|jJ z$bcPK1@D&3%qQdh5&}02HXStYVc_1w&Zoqsv(U8{9{kcqgjGTbq3b0vf65L>;0WlM zn^{Z+bqP!G9%LtffA!VB6Tf;!GIz<6xtPg&(PKVvrU*I0G5lcnO^#YqS{`MOsECT4 z`rx>T;nAUoo@+h7|J^6944ixp#C+FAVBH#N;g5lzCC4|}cEi-?F_TBdwnr7z_p;>r z=bv9E)9~aRFb#QoKuAkvyMZ?W0^D!~`Z~8kjmO;iQ>oIvHa?603X5Gr3a>z+_1K;; z=Dn!?46XyiG`D77L4%b}84tN$2Xt1pcbd8_@3;6vC>paS$r8~OW^SLfQuL%{s*D_HpOnQ0=n1#tqmD|VqKU!Aol zb9a`_>uvN}LEYroRK^dspV+P319jsbOG9zP&V9DuwN0pa;FM<5do>?jv=Gcz55J#P z5YXe82z@puRHNlH==qwON+=U0RL2Qjv8EwU-(YC(8ptm~s;PL{-|ht30Ld-98 z&jhSTr3Gb$6@|SZ^c>jgZ1S{ONCV%9snf-GQC&84egW1a8FGTdc0{0O!G#gPYe7TM z=Nb4h>!TJWY4|h78D!|FA*qi+FMH0hX*>H8N=@(t+W!zLK8WIx)Bns# z%%jlilOW&;8-Q7u+KZ=P72X~m{dmgXUahUIsxAjS;Lmgyo5>gHDf4Sp5Mo zH~C}`1FPzj;p(7CSp9%ahmOWq-~%jHCa1F`rqxNnNgEDPWG0;Nc+(=Br8u95KoY(;qV91(Q1O%Xy0UY8}T!V_Op0O;{$zj8mA3@ zBF7h#!b)f=A`J!ma%+gmvuIT(JmIYfbU9~C~Ix%W;LWzt|VM#n|CUjv6Cl#D71P$cz z{RTW(kKtCL7M#p+e30>Gcv!$;anN}91sop%WPF!`@Kd?3#}i69j+1O*aX~0uFUvVH zn(8&cIi#e$wUyM?mNezmqDoJ)B&%|3O|E!S!v5re`GGTiwuL1}_E)uM90ezA?ZEn{ z+dXHjxM3NJ8>BhdFg|)5ZKkvEx)*u*hacHKoCa1UL5DAs2o`TLIG?KCz(^D+lToU@ z7>OX}sv(}rN)HhdaIC~KjbAuFfQ42nayVP*dafh)FO)BI^v5SU?UiK}rDc0GWFCvp z3o=pNlv$Iu(LyKn(x@{XPt+W0YP9_m^I;CBo5p>IfwGXrrkqooQIeB`WxON?XRR`5 z>B2c7Tt(DRP)+q-s;PdXoovb6ky`BvO|Nd*ddNzsmTCS?4oax)MEw#=#2*U<}L&?Q45jhZN1TIxVFM83n413Cx16!J@U+E1Np?(DQs zYMeR*(_`G-KP)2ZG?rABRP3s>Pw^G=xA~uxwY9YkjTYUKcjlzH3f&k`N7Z?aEjx}_ z-+lER(gli@2@^mDG<){b0{QBzr+Lz4Bb8NEo(fMWsMnuR{*Mx=%IZe>Fw z`B`JF(&NE<>?*#%LO>=|LUvXbR~A+l)E3m|*MZb_%Ru1>p#sPwsQ~gw2plJLJkR6p z@9^At-ho$**#!G3N?AcA*%Ea^Q+{hJ*7>#!kJCw8SwP%3IlHBAJ=4 zB=H5*rWAkJ9t98SR5jLNs&%M{$9B53eBs(1n&=&?ic2h{oGzsm9Ht9wG-j%5u(a-_HlG+tCo0QJXV`TUOV3f1@!1?^!M z<|L6_ypg52@fs9;hpFT;Y)y1?LPx}MB&(3>oG0pbEJZj2fL=o#hlOC!=Token_YN7 zCX}_xMs8qbC0kfEp(iHtul}~f4mI~7LAB-a>Xaf|CfQyH%0&AckPp0fcXbt^98q#C zIaL*VEA4w5JdHICn)hEOptw3vZ~Z`3Rk3~hE;}g!r_qEWmelO4Zm8O?Y1+d>P7Tg- zuObT)_oy%+Twty1lPhY|?BrgKe;(_L3A+Il7g~vRnQKdOqCH{lAJ>e;fjsIKxT(H? z%K96R;tG~09PSJ1co~PgB2w1?%>!ymNnT;A01AAy;gC;Hc?7IRIAEFuy-x{Qp=8vI z53gcRC4C|X0LE^w%RGw?Tb#0R0W}1_x#T4O5r?CX-s^?HLBw_T&nYYqVS*bw5=K`4 zdd{zB#kNdyZFS|@^U}*x_h%9u(0l0$6M;ft#G1&YNkx;an}zv%z~?~t$vE0fCTe7g z&oFdfGgT6Z0R)WL6`=8g(2W6^2quzWP>?TXhb-}t*XvQym6yi_gBkbrMA)s$%`d@6 z^!$tvf-~bW%CARk_YPewaKdQ3)u0l%JE7aIo!n@WLDlFKO^}+T^lo<>CkoNxX!4U+lW>nYy1=ch|mtk zA=&NM*w1yDNC zmP%$K<6w^B@#i%27v|!i!Gmpsqr5t=%{$Y((7W0j=S}pccyqj^-u>P-?8+dZ>;VfX6plv`!q9Tg1nJo1Y?8Lz=R$Q$lm?2Yoqd)Iq6dfnb~Z?pF)Z-@6C?`K|b zH`8tCb|BA@-Q&B%yBBvyb#Lr;cb9iJcR$tL(fv;MXWiZ&rpM4Ts3*KClQ7qgBDK^fd-8MW7H5}P#EL}=oQr8)?e5E zOaG(3N8hdY>VMFEr2k%jO@CE?MgN`tTm3is%lfbNU+Mp;|5E>j{&W3jc+B`%|B?P5 z`VaM&^cVFX=-=1BM^205YU6+DxMlw(?*BGQ#b`0o2}7UtFui0uN51iJ#~S^N>x@F2 zhD_(88PEa=b&-z`9&-Ekt=oYz6@^qFMgKqe`sY?4Z8OCGa(>856mDjttOIavX0y^w zRR%I^DE417G~OS+icUPFk^G{Y`b8MncgG=3Cc;x_m}r>(bQA}LN0>##N5kFBLsg zFXb(zf&I8>+;^27ZnR%Gg@H=KJ7uDN(IB4MQeFzl51WPsz`hd?x4*2SP0J)2=1%y% zbZkj5v_GV_67xbiFjjB--Tko^HnA@AEh*4)6hD{(gM^ za6Pr9av_}+ew4)@20IXj;-X`K8o8SZhoO1g38Ue6@btUy@#7c66@V4v{k8;8@k(tW z)dz6lQWz}<4Hx5(uZRRYD8S7;N)5_32Ih`m$^&tLEIDxmgX(y51WSlCnLeZ9;3)d8U^y|$3^Ym-bv?X z>?pGxd58w#cG0+jo0|SStYRJf=>Z##aN>^>VWfEEH;0BJJJC>B7_pudpC3*orA1lp zrEXd_sa}8|MZ?5=+>Aqwb&lc?;oKsuSxSG$MmTi`z@~Xcel#}2%0zb|5`gyT(tjbdZ~<3 zdGAgq;r*p8l}jpzl&|`kpUaH6a4CP}7c1g;wm&~9{oOXj1^)oR1;GxxJ5F-bGQ&O0 z4=dJ>#`(h|q;mS<=Sp>f-$BD9zrW5yC3wlEW7Ut_fv^q0dk>AH;WT}}yT6Wb_m6is zGlJri=J?(DPxIir(p@VgbvU|FW$9dH+nk{K%7=MPui6|QDl zWPkF9YnXxZXums9NkuvFpapOHFaj%7{%*O^%(nXkp}nS+l1tikvrh+2TbJkxOHp z>4~W+j>%*18#_tFu{bFV4fZO46Mz4!a zj&^QvM7vzh#I>75kd#zcVoYp0vUj>1{``L~Z-UFUaoV_XG<|e`*~TWO{%YVj5begM zq{oh99%NFPjZ7v!OcR-SCV_D=4(Mo)W;Qdia6JT93bPLJ4t%tZXC^TBF=OGL#U$b9 zxGPn<=!<32kv>cP*mP)l4*kXZ?=17NciKB!g*I3>Zi4!Se(dX9pID2vTs0GG(u^3BC{5C`WZ_KQc+(Fpws=Ak)L)tuv6blL#|-$d|NaLw CDx5C> diff --git a/lib/igv-js/html/assets/fonts/fontawesome-webfont.eot b/lib/igv-js/html/assets/fonts/fontawesome-webfont.eot deleted file mode 100644 index c7b00d2ba8896fd29de846b19f89fcf0d56ad152..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76518 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%0DyT{kunD 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⋙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 z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lib/igv-js/html/assets/fonts/fontawesome-webfont.ttf b/lib/igv-js/html/assets/fonts/fontawesome-webfont.ttf deleted file mode 100644 index f221e50a2ef60738ba30932d834530cdfe55cb3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 152796 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

=v_rA!CPE#mM^4@)}X7qf;At+v)G*FZd&; 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 ALjV8( diff --git a/lib/igv-js/html/assets/fonts/fontawesome-webfont.woff b/lib/igv-js/html/assets/fonts/fontawesome-webfont.woff deleted file mode 100644 index 6e7483cf61b490c08ed644d6ef802c69472eb247..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90412 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%0Ded40&XED0GYFoIh+AV*?9!MR5pBW?X_8Bp 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 IH>08b4`|GD*#H0l diff --git a/lib/igv-js/html/assets/fonts/fontawesome-webfont.woff2 b/lib/igv-js/html/assets/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 7eb74fd127ee5eddf3b95fee6a20dc1684b0963b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71896 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≠dR0E9ICQ{-tcR;@{R^I5P#1GUH!p=-{c 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 diff --git a/lib/igv-js/html/assets/js/jquery.min.js b/lib/igv-js/html/assets/js/jquery.min.js deleted file mode 100644 index 0f60b7bd0..000000000 --- a/lib/igv-js/html/assets/js/jquery.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! jQuery v1.11.3 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; - -return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/\s*$/g,ra={option:[1,""],legend:[1,"

","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m("