From 385306945f6ffa5d18bfcdcc9abfa71639dbcb6d Mon Sep 17 00:00:00 2001 From: jkleinkauff Date: Mon, 24 Jan 2022 20:23:11 -0300 Subject: [PATCH] first commit --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 10 ++ .vscode/settings.json | 3 + Makefile | 8 ++ README.md | 32 +++++ dags/etl_create_dm_dll.py | 48 +++++++ dags/etl_ingest.py | 50 +++++++ dags/etl_load_dm.py | 123 ++++++++++++++++ dags/etl_process.py | 47 +++++++ dags/sql/d_datasource.sql | 6 + dags/sql/d_date.sql | 9 ++ dags/sql/d_region.sql | 6 + dags/sql/f_trips.sql | 15 ++ dags/sql/f_trips_staging.sql | 15 ++ dags/sql/pet_schema.sql | 7 + docker-compose-superset.yaml | 90 ++++++++++++ docker-compose.yaml | 119 ++++++++++++++++ docker/.DS_Store | Bin 0 -> 6148 bytes docker/.env | 10 ++ docker/Dockerfile | 25 ++++ docker/superset/.env | 46 ++++++ docker/superset/.env-non-dev | 46 ++++++ docker/superset/README.md | 75 ++++++++++ docker/superset/docker-bootstrap.sh | 51 +++++++ docker/superset/docker-ci.sh | 26 ++++ docker/superset/docker-frontend.sh | 26 ++++ docker/superset/docker-init.sh | 79 +++++++++++ docker/superset/frontend-mem-nag.sh | 49 +++++++ docker/superset/pythonpath_dev/.gitignore | 23 +++ .../pythonpath_dev/superset_config.py | 30 ++++ .../superset_config_local.example | 27 ++++ docker/superset/requirements-local.txt | 1 + docker/superset/run-server.sh | 32 +++++ readme_pdf.pdf | Bin 0 -> 301989 bytes requirements.txt | 6 + src/config.py | 26 ++++ src/etl_data/landing/trips.csv | 101 ++++++++++++++ src/etl_data/landing/trips___.csv | 15 ++ src/fill-dm/generate-import.py | 76 ++++++++++ src/process/spark_process.py | 131 ++++++++++++++++++ src/stream-csv-read-csv/read.py | 29 ++++ src/stream-csv-read-csv/read_convert.py | 53 +++++++ terraform/config.tf | 9 ++ terraform/kinesis.tf | 9 ++ terraform/providers.tf | 4 + terraform/s3.tf | 29 ++++ 46 files changed, 1622 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 Makefile create mode 100644 README.md create mode 100644 dags/etl_create_dm_dll.py create mode 100644 dags/etl_ingest.py create mode 100644 dags/etl_load_dm.py create mode 100644 dags/etl_process.py create mode 100644 dags/sql/d_datasource.sql create mode 100644 dags/sql/d_date.sql create mode 100644 dags/sql/d_region.sql create mode 100644 dags/sql/f_trips.sql create mode 100644 dags/sql/f_trips_staging.sql create mode 100644 dags/sql/pet_schema.sql create mode 100644 docker-compose-superset.yaml create mode 100644 docker-compose.yaml create mode 100644 docker/.DS_Store create mode 100644 docker/.env create mode 100644 docker/Dockerfile create mode 100644 docker/superset/.env create mode 100644 docker/superset/.env-non-dev create mode 100644 docker/superset/README.md create mode 100755 docker/superset/docker-bootstrap.sh create mode 100755 docker/superset/docker-ci.sh create mode 100755 docker/superset/docker-frontend.sh create mode 100755 docker/superset/docker-init.sh create mode 100755 docker/superset/frontend-mem-nag.sh create mode 100644 docker/superset/pythonpath_dev/.gitignore create mode 100644 docker/superset/pythonpath_dev/superset_config.py create mode 100644 docker/superset/pythonpath_dev/superset_config_local.example create mode 100644 docker/superset/requirements-local.txt create mode 100644 docker/superset/run-server.sh create mode 100644 readme_pdf.pdf create mode 100644 requirements.txt create mode 100644 src/config.py create mode 100644 src/etl_data/landing/trips.csv create mode 100644 src/etl_data/landing/trips___.csv create mode 100644 src/fill-dm/generate-import.py create mode 100644 src/process/spark_process.py create mode 100644 src/stream-csv-read-csv/read.py create mode 100644 src/stream-csv-read-csv/read_convert.py create mode 100644 terraform/config.tf create mode 100644 terraform/kinesis.tf create mode 100644 terraform/providers.tf create mode 100644 terraform/s3.tf diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d3a5f53d2b9a9a8bd4cba42434f7f836085e547d GIT binary patch literal 6148 zcmeH~L2uJA6vv;tvZhQ(6=KpjAVuO@jkfEC&@Q2L2M$~o1P4GRSqfB3zGajQiQw ztYjips8DnCDGjIDD2!#eHrfU(0ZZUtBf#J8CPgIagifjW{eBBa@gU0^jqjqgQNFfW zaVpN1^U8lGr+yyf?%r?j&Fk*$L90=B+XwCWyy|SxZ=O&FPpp@Rz7)#D6qaGrug3w1RLo|9E%M-Ja@$UE# zzWY-dY8RYiX*2wK=`eR@v;EdS^K1QqDuN05uF?{)1pa~m?+-4V)ib)#sIwiYBdiQ{J}o)NHR>5%Xhjc9=v1gq73PW| zbUNa$8Rr>YXw~T?%;iIvCkt~!5$frf-&Mm&cv@{~30ML}0voznE;w MFxbKpxKska06tRLcK`qY literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93515a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/dags/__pycache__ +/dags/data/* +/logs +/plugins +/venv +/terraform/.terraform +/terraform/.terraform.lock.hcl +/src/etl_data/parquets/* +/src/etl_data/parquets_processed/* +/src/etl_data/dags/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de288e1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e18db94 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +psql-connect: + psql --host=localhost --port=5433 --username=postgres + +build: + docker build -t etl-csv -f docker/Dockerfile . + +build-up: + docker-compose -f docker-compose.yaml -f docker-compose-superset.yaml up diff --git a/README.md b/README.md new file mode 100644 index 0000000..edc15e2 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +## How to start the project + +In your /docker/.env file, add the following variables: + +``` +DATA_DIR="/...//src/etl_data" +DAGS_DIR="/...//dags/data" +``` + +``` +make build +make build-up +``` + +The following services should start: +``` +➜ project git:(master) ✗ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +357b712341f4 apache/airflow:2.2.3 "/usr/bin/dumb-init …" 5 hours ago Up 5 hours 8080/tcp xxx_airflow-scheduler_1 +372d440f1b6a apache/airflow:2.2.3 "/usr/bin/dumb-init …" 5 hours ago Up 5 hours (healthy) 0.0.0.0:5555->5555/tcp, 8080/tcp xxx_flower_1 +77eadeb3c0aa apache/airflow:2.2.3 "/usr/bin/dumb-init …" 5 hours ago Up 5 hours (healthy) 0.0.0.0:8080->8080/tcp xxx_airflow-webserver_1 +7f68845efbc3 apache/airflow:2.2.3 "/usr/bin/dumb-init …" 5 hours ago Up 5 hours 8080/tcp xxx_airflow-worker_1 +af9fb24ad6ff postgres:13 "docker-entrypoint.s…" 5 hours ago Up 5 hours (healthy) 5432/tcp xxx_postgres_1 +4dadbd44de13 postgres "docker-entrypoint.s…" 5 hours ago Up 5 hours 0.0.0.0:5433->5432/tcp xxx_db_app_1 +432837c58485 redis:latest "docker-entrypoint.s…" 5 hours ago Up 5 hours (healthy) 0.0.0.0:6379->6379/tcp superset_cache +04ee1864109a apache/superset:latest-dev "/app/docker/docker-…" 14 hours ago Up 7 hours (unhealthy) 8088/tcp superset_worker +8254b5f3d984 apache/superset:latest-dev "/app/docker/docker-…" 14 hours ago Up 7 hours (unhealthy) 8088/tcp superset_worker_beat +b35323ed9f27 apache/superset:latest-dev "/app/docker/docker-…" 14 hours ago Up 7 hours (healthy) 0.0.0.0:8088->8088/tcp superset_app +aeefa7a99c4e postgres:10 "docker-entrypoint.s…" 14 hours ago Up 7 hours 5432/tcp superset_db + +``` + diff --git a/dags/etl_create_dm_dll.py b/dags/etl_create_dm_dll.py new file mode 100644 index 0000000..8ae0d68 --- /dev/null +++ b/dags/etl_create_dm_dll.py @@ -0,0 +1,48 @@ +import datetime + +from airflow import DAG +from airflow.providers.postgres.operators.postgres import PostgresOperator + +# create_pet_table, populate_pet_table, get_all_pets, and get_birth_date are examples of tasks created by +# instantiating the Postgres Operator + +with DAG( + dag_id="etl-dm-dll", + start_date=datetime.datetime(2020, 2, 2), + schedule_interval="@once", + catchup=False, +) as dag: + # create_pet_table = PostgresOperator( + # task_id="create_pet_table", + # sql="sql/pet_schema.sql", + # ) + + d_region = PostgresOperator( + task_id="create_d_region", + sql="sql/d_region.sql", + ) + + d_datasource = PostgresOperator( + task_id="create_d_datasource", + sql="sql/d_datasource.sql", + ) + + d_date = PostgresOperator( + task_id="create_d_date", + sql="sql/d_date.sql", + ) + + f_trips_staging = PostgresOperator( + task_id="create_f_trips_staging", + sql="sql/f_trips_staging.sql", + ) + + f_trips = PostgresOperator( + task_id="create_f_trips", + sql="sql/f_trips.sql", + ) + + + d_region >> f_trips_staging >> f_trips + d_datasource >> f_trips_staging >> f_trips + d_date >> f_trips_staging >> f_trips \ No newline at end of file diff --git a/dags/etl_ingest.py b/dags/etl_ingest.py new file mode 100644 index 0000000..0e409ab --- /dev/null +++ b/dags/etl_ingest.py @@ -0,0 +1,50 @@ +from datetime import datetime, timedelta +import os +from airflow import DAG +from airflow.operators.dummy_operator import DummyOperator +from airflow.operators.docker_operator import DockerOperator +from docker.types import Mount + + +default_args = { + "owner": "airflow", + "description": "Job to ingest CSV data and convert to parquet", + "depend_on_past": False, + "start_date": datetime(2021, 5, 1), + "email_on_failure": False, + "email_on_retry": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +abc = os.getenv("LOCAL_ETL_DATA") + +with DAG( + "etl-ingestion-dag", + default_args=default_args, + schedule_interval="*/15 * * * *", + catchup=False, +) as dag: + start_dag = DummyOperator(task_id="start_dag") + + t_ingest = DockerOperator( + task_id="task-ingest-convert-csv", + image="etl-csv", + # container_name="task_ingest_convert_csv", + api_version="auto", + auto_remove=True, + # command="/bin/sleep 30", + command=["python", "src/stream-csv-read-csv/read_convert.py"], + # docker_url="tcp://docker-proxy:2375", + docker_url="unix://var/run/docker.sock", + network_mode="bridge", + mounts=[ + Mount( + source=os.getenv("DATA_DIR"), + target="/app/src/etl_data", + type="bind", + ) + ], + ) + + start_dag >> t_ingest diff --git a/dags/etl_load_dm.py b/dags/etl_load_dm.py new file mode 100644 index 0000000..a095120 --- /dev/null +++ b/dags/etl_load_dm.py @@ -0,0 +1,123 @@ +from datetime import datetime, timedelta +import os +from airflow import DAG +from airflow.operators.dummy_operator import DummyOperator +from airflow.operators.bash_operator import BashOperator +from airflow.operators.docker_operator import DockerOperator +from airflow.providers.postgres.operators.postgres import PostgresOperator +from docker.types import Mount + +default_args = { + "owner": "airflow", + "description": "Job to ingest CSV data and convert to parquet", + "depend_on_past": False, + "start_date": datetime(2021, 5, 1), + "email_on_failure": False, + "email_on_retry": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +with DAG( + "etl-load-dm-dag", + default_args=default_args, + schedule_interval="*/60 * * * *", + catchup=False, +) as dag: + start_dag = DummyOperator(task_id="start_dag") + + t_generate_data = DockerOperator( + task_id="task-generate-dm-csv-data", + image="etl-csv", + # container_name="task_ingest_convert_csv", + api_version="auto", + auto_remove=True, + command=["spark-submit", "src/fill-dm/generate-import.py"], + # docker_url="tcp://docker-proxy:2375", + docker_url="unix://var/run/docker.sock", + network_mode="bridge", + mounts=[ + Mount( + source=os.getenv("DATA_DIR"), + target="/app/src/etl_data", + type="bind", + ), + Mount( + source=os.getenv("DAGS_DIR"), + target="/app/src/etl_data/dags", + type="bind", + ), + ], + ) + + # truncate all - certainly not for production, would do a SCD for dim and facts + + t_truncate_all = PostgresOperator( + task_id="truncate_all_tables_data", + sql=""" + TRUNCATE f_trips_staging, f_trips, d_datasource, d_date, d_region RESTART IDENTITY; + """, + ) + + t_load_d_datasource = BashOperator( + task_id="load_d_datasource", + bash_command="cd $AIRFLOW_HOME'/dags'" + "&& ls" + "&& f=$(ls data/d_datasource.csv/*.csv| head -1)" + "&& PGPASSWORD=password psql --host host.docker.internal --port 5433 --username postgres -d postgres -c '''\copy d_datasource(datasource) FROM '$f' '' WITH csv; ' ", + ) + + t_load_d_region = BashOperator( + task_id="load_d_region", + bash_command="cd $AIRFLOW_HOME'/dags'" + "&& ls" + "&& f=$(ls data/d_region.csv/*.csv| head -1)" + "&& PGPASSWORD=password psql --host host.docker.internal --port 5433 --username postgres -d postgres -c '''\copy d_region(region_name) FROM '$f' '' WITH csv; ' ", + ) + + t_load_d_date = BashOperator( + task_id="load_d_date", + bash_command="cd $AIRFLOW_HOME'/dags'" + "&& ls" + "&& f=$(ls data/d_date.csv/*.csv| head -1)" + "&& PGPASSWORD=password psql --host host.docker.internal --port 5433 --username postgres -d postgres -c '''\copy d_date(date,year,month,day) FROM '$f' '' WITH csv; ' ", + ) + + t_load_f_trips_staging = BashOperator( + task_id="load_f_trips_staging", + bash_command="cd $AIRFLOW_HOME'/dags'" + "&& ls" + "&& f=$(ls data/f_trips_staging.csv/*.csv| head -1)" + "&& PGPASSWORD=password psql --host host.docker.internal --port 5433 --username postgres -d postgres " + " -c '''\copy f_trips_staging(origin_coord_x,origin_coord_y,destination_coord_x,destination_coord_y,date,region,datasource, business_key) FROM '$f' '' WITH csv; ' ", + ) + + t_merge_f_trips = PostgresOperator( + task_id="merge_f_trips", + sql=""" + insert into f_trips(origin, destination, sk_region, sk_datasource, sk_date, sk_business) + select + stg.origin_coord_point + , stg.destination_coord_point + , region.id + , datasource.id + , date.id + , stg.business_key + from f_trips_staging as stg + join d_region as region + on stg.region = region.region_name + join d_datasource as datasource + on stg.datasource = datasource.datasource + join d_date as date + on stg."date" = date.date + where stg.business_key not in ( + select sk_business from f_trips + ) + """, + ) + + start_dag >> t_truncate_all >> t_generate_data + t_generate_data >> t_load_d_datasource >> t_load_f_trips_staging + t_generate_data >> t_load_d_region >> t_load_f_trips_staging + t_generate_data >> t_load_d_date >> t_load_f_trips_staging + t_load_f_trips_staging >> t_merge_f_trips \ No newline at end of file diff --git a/dags/etl_process.py b/dags/etl_process.py new file mode 100644 index 0000000..329ec3d --- /dev/null +++ b/dags/etl_process.py @@ -0,0 +1,47 @@ +from datetime import datetime, timedelta +import os +from airflow import DAG +from airflow.operators.dummy_operator import DummyOperator +from airflow.operators.docker_operator import DockerOperator +from docker.types import Mount + +default_args = { + "owner": "airflow", + "description": "Job to ingest CSV data and convert to parquet", + "depend_on_past": False, + "start_date": datetime(2021, 5, 1), + "email_on_failure": False, + "email_on_retry": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +with DAG( + "etl-process-ingested-dag", + default_args=default_args, + schedule_interval="*/30 * * * *", + catchup=False, +) as dag: + start_dag = DummyOperator(task_id="start_dag") + + t_data_process = DockerOperator( + task_id="task-process-data-csv", + image="etl-csv", + # container_name="task_ingest_convert_csv", + api_version="auto", + auto_remove=True, + # command="/bin/sleep 30", + command=["spark-submit", "src/process/spark_process.py"], + # docker_url="tcp://docker-proxy:2375", + docker_url="unix://var/run/docker.sock", + network_mode="bridge", + mounts=[ + Mount( + source=os.getenv("DATA_DIR"), + target='/app/src/etl_data', + type='bind' + ) + ], + ) + + start_dag >> t_data_process diff --git a/dags/sql/d_datasource.sql b/dags/sql/d_datasource.sql new file mode 100644 index 0000000..94b837e --- /dev/null +++ b/dags/sql/d_datasource.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS d_datasource CASCADE; +CREATE TABLE d_datasource ( + id INT GENERATED ALWAYS AS IDENTITY, + datasource VARCHAR(51), + PRIMARY KEY(id) +); \ No newline at end of file diff --git a/dags/sql/d_date.sql b/dags/sql/d_date.sql new file mode 100644 index 0000000..03d55b0 --- /dev/null +++ b/dags/sql/d_date.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS d_date; +CREATE TABLE d_date ( + id INT GENERATED ALWAYS AS IDENTITY, + date date, + year int, + month int, + day int, + PRIMARY KEY(id) +) \ No newline at end of file diff --git a/dags/sql/d_region.sql b/dags/sql/d_region.sql new file mode 100644 index 0000000..9609a4e --- /dev/null +++ b/dags/sql/d_region.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS d_region CASCADE; +CREATE TABLE d_region ( + id INT GENERATED ALWAYS AS IDENTITY, + region_name VARCHAR(50), + PRIMARY KEY(id) +) \ No newline at end of file diff --git a/dags/sql/f_trips.sql b/dags/sql/f_trips.sql new file mode 100644 index 0000000..0b86dc3 --- /dev/null +++ b/dags/sql/f_trips.sql @@ -0,0 +1,15 @@ +DROP TABLE IF EXISTS f_trips; + +CREATE TABLE f_trips ( + id INT GENERATED ALWAYS AS IDENTITY, + origin point, + destination point, + sk_region INT, + sk_datasource INT, + sk_date INT, + sk_business UUID, + PRIMARY KEY(id), + FOREIGN KEY(sk_region) REFERENCES d_region(id), + FOREIGN KEY(sk_datasource) REFERENCES d_datasource(id), + FOREIGN KEY(sk_date) REFERENCES d_date +) \ No newline at end of file diff --git a/dags/sql/f_trips_staging.sql b/dags/sql/f_trips_staging.sql new file mode 100644 index 0000000..46db1a8 --- /dev/null +++ b/dags/sql/f_trips_staging.sql @@ -0,0 +1,15 @@ +DROP TABLE IF EXISTS f_trips_staging; +CREATE TABLE f_trips_staging ( + id INT GENERATED ALWAYS AS IDENTITY, + origin_coord_x float, + origin_coord_y float, + origin_coord_point POINT GENERATED ALWAYS AS (point(origin_coord_x,origin_coord_y)) STORED, + destination_coord_x float, + destination_coord_y float, + destination_coord_point POINT GENERATED ALWAYS AS (point(destination_coord_x,destination_coord_y)) STORED, + date date, + region VARCHAR(50), + datasource VARCHAR(50), + business_key UUID, + PRIMARY KEY(id) +) \ No newline at end of file diff --git a/dags/sql/pet_schema.sql b/dags/sql/pet_schema.sql new file mode 100644 index 0000000..d0f1858 --- /dev/null +++ b/dags/sql/pet_schema.sql @@ -0,0 +1,7 @@ +-- create pet table +CREATE TABLE IF NOT EXISTS pet ( + pet_id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + pet_type VARCHAR NOT NULL, + birth_date DATE NOT NULL, + OWNER VARCHAR NOT NULL); \ No newline at end of file diff --git a/docker-compose-superset.yaml b/docker-compose-superset.yaml new file mode 100644 index 0000000..e42151d --- /dev/null +++ b/docker-compose-superset.yaml @@ -0,0 +1,90 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +x-superset-image: &superset-image apache/superset:latest-dev +x-superset-depends-on: &superset-depends-on + - db + - redis +x-superset-volumes: &superset-volumes + # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container + - ./docker/superset:/app/docker + - superset_home:/app/superset_home + +version: "3.7" +services: + redis: + image: redis:latest + container_name: superset_cache + restart: unless-stopped + volumes: + - redis:/data + + db: + env_file: docker/superset/.env-non-dev + image: postgres:10 + container_name: superset_db + restart: unless-stopped + volumes: + - db_home:/var/lib/postgresql/data + + superset: + env_file: docker/superset/.env-non-dev + image: *superset-image + container_name: superset_app + command: ["/app/docker/docker-bootstrap.sh", "app-gunicorn"] + user: "root" + restart: unless-stopped + ports: + - 8088:8088 + depends_on: *superset-depends-on + volumes: *superset-volumes + + superset-init: + image: *superset-image + container_name: superset_init + command: ["/app/docker/docker-init.sh"] + env_file: docker/superset/.env-non-dev + depends_on: *superset-depends-on + user: "root" + volumes: *superset-volumes + + superset-worker: + image: *superset-image + container_name: superset_worker + command: ["/app/docker/docker-bootstrap.sh", "worker"] + env_file: docker/superset/.env-non-dev + restart: unless-stopped + depends_on: *superset-depends-on + user: "root" + volumes: *superset-volumes + + superset-worker-beat: + image: *superset-image + container_name: superset_worker_beat + command: ["/app/docker/docker-bootstrap.sh", "beat"] + env_file: docker/superset/.env-non-dev + restart: unless-stopped + depends_on: *superset-depends-on + user: "root" + volumes: *superset-volumes + +volumes: + superset_home: + external: false + db_home: + external: false + redis: + external: false \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c4775ed --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,119 @@ +version: '3' +x-airflow-common: + &airflow-common + image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:2.2.3} + env_file: + - docker/.env + environment: + &airflow-common-env + AIRFLOW__CORE__EXECUTOR: CeleryExecutor + AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow + AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://airflow:airflow@postgres/airflow + AIRFLOW__CELERY__BROKER_URL: redis://:@redis:6379/0 + AIRFLOW__CORE__FERNET_KEY: '' + AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true' + AIRFLOW__CORE__LOAD_EXAMPLES: 'false' + AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: 5 # Just to have a fast load in the front-end. Do not use it in production with those configurations. + AIRFLOW__API__AUTH_BACKEND: 'airflow.api.auth.backend.basic_auth' + AIRFLOW__CORE__ENABLE_XCOM_PICKLING: 'true' # "_run_image of the DockerOperator returns now a python string, not a byte string" Ref: https://github.com/apache/airflow/issues/13487 + AIRFLOW_CONN_POSTGRES_DEFAULT: 'postgresql://postgres:password@host.docker.internal:5433/postgres' + volumes: + - ./dags:/opt/airflow/dags + - ./logs:/opt/airflow/logs + - ./plugins:/opt/airflow/plugins + - "/var/run/docker.sock:/var/run/docker.sock" # We will pass the Docker Deamon as a volume to allow the webserver containers start docker images. Ref: https://stackoverflow.com/q/51342810/7024760 + user: "${AIRFLOW_UID:-50000}:${AIRFLOW_GID:-50000}" + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + +services: + db_app: + image: postgres + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: postgres + environment: + POSTGRES_PASSWORD: password + ports: + - 5433:5432 + postgres: + image: postgres:13 + environment: + POSTGRES_USER: airflow + POSTGRES_PASSWORD: airflow + POSTGRES_DB: airflow + volumes: + - postgres-db-volume:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "airflow"] + interval: 5s + retries: 5 + restart: always + + redis: + image: redis:latest + ports: + - 6379:6379 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 30s + retries: 50 + restart: always + + airflow-webserver: + <<: *airflow-common + command: webserver + user: root + ports: + - 8080:8080 + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost:8080/health"] + interval: 10s + timeout: 10s + retries: 5 + restart: always + + airflow-scheduler: + <<: *airflow-common + command: scheduler + restart: always + user: root + + airflow-worker: + <<: *airflow-common + command: celery worker + restart: always + user: root + + airflow-init: + <<: *airflow-common + command: version + environment: + <<: *airflow-common-env + _AIRFLOW_DB_UPGRADE: 'true' + _AIRFLOW_WWW_USER_CREATE: 'true' + _AIRFLOW_WWW_USER_USERNAME: ${_AIRFLOW_WWW_USER_USERNAME:-airflow} + _AIRFLOW_WWW_USER_PASSWORD: ${_AIRFLOW_WWW_USER_PASSWORD:-airflow} + user: root + + flower: + <<: *airflow-common + command: celery flower + ports: + - 5555:5555 + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost:5555/"] + interval: 10s + timeout: 10s + retries: 5 + restart: always + user: root + +volumes: + postgres-db-volume: diff --git a/docker/.DS_Store b/docker/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f802d2a65c07aa4bba3539a2c25aaa7744e120d3 GIT binary patch literal 6148 zcmeHK%}N6?5Kh`^vlL+u3VI88E!f(kh?ixpFW`zERBD$NU0gS1f9#Jz~oEjXG6X$nS?RM{bJZ=%w>!TP{fJ@%@=}x)Fr7{3nJIwNLj{4EDtBy zcqy6;e~|&)I{~{^urci1r~M0K6@u?I9LG_Xw_0z!Qmw76*F{}4#7%IoCP6;Pi_9OS zS2#LXDh}uUL3k03CVhAFL?!tkN=B(ph=wBwxxS2&p_=%rNQRlt4U9uL!s)v^(`oyl z-D%3hZfDk%)1&=XQ+5x!vza5dcK43Y`VaAAqMi+#9DY148y4sA0-dFWJq2kJtK<%R z^SpUHAu&J<5Cbd3fIR|5V};gCOC<(~fuAve=Ys@A^emPJ_0a(hULVn4Lqvf-z9kT) zMbBbs5Ii8;LKZ@BhUl8W97;z`tUE*Lp$Ehb7tCy0kc~wG#9e6b0i-gC8l-(WMw-u@rBE aY5}{%4xneTGzb + +# Getting Started with Superset using Docker + +Docker is an easy way to get started with Superset. + +## Prerequisites + +1. Docker! [link](https://www.docker.com/get-started) +2. Docker-compose [link](https://docs.docker.com/compose/install/) + +## Configuration + +The `/app/pythonpath` folder is mounted from [`./docker/pythonpath_dev`](./pythonpath_dev) +which contains a base configuration [`./docker/pythonpath_dev/superset_config.py`](./pythonpath_dev/superset_config.py) +intended for use with local development. + +### Local overrides + +In order to override configuration settings locally, simply make a copy of [`./docker/pythonpath_dev/superset_config_local.example`](./pythonpath_dev/superset_config_local.example) +into `./docker/pythonpath_dev/superset_config_docker.py` (git ignored) and fill in your overrides. + +### Local packages + +If you want to add Python packages in order to test things like databases locally, you can simply add a local requirements.txt (`./docker/requirements-local.txt`) +and rebuild your Docker stack. + +Steps: + +1. Create `./docker/requirements-local.txt` +2. Add your new packages +3. Rebuild docker-compose + 1. `docker-compose down -v` + 2. `docker-compose up` + +## Initializing Database + +The database will initialize itself upon startup via the init container ([`superset-init`](./docker-init.sh)). This may take a minute. + +## Normal Operation + +To run the container, simply run: `docker-compose up` + +After waiting several minutes for Superset initialization to finish, you can open a browser and view [`http://localhost:8088`](http://localhost:8088) +to start your journey. + +## Developing + +While running, the container server will reload on modification of the Superset Python and JavaScript source code. +Don't forget to reload the page to take the new frontend into account though. + +## Production + +It is possible to run Superset in non-development mode by using [`docker-compose-non-dev.yml`](../docker-compose-non-dev.yml). This file excludes the volumes needed for development and uses [`./docker/.env-non-dev`](./.env-non-dev) which sets the variable `SUPERSET_ENV` to `production`. + +## Resource Constraints + +If you are attempting to build on macOS and it exits with 137 you need to increase your Docker resources. See instructions [here](https://docs.docker.com/docker-for-mac/#advanced) (search for memory) diff --git a/docker/superset/docker-bootstrap.sh b/docker/superset/docker-bootstrap.sh new file mode 100755 index 0000000..67e5294 --- /dev/null +++ b/docker/superset/docker-bootstrap.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -eo pipefail + +REQUIREMENTS_LOCAL="/app/docker/requirements-local.txt" +# If Cypress run – overwrite the password for admin and export env variables +if [ "$CYPRESS_CONFIG" == "true" ]; then + export SUPERSET_CONFIG=tests.integration_tests.superset_test_config + export SUPERSET_TESTENV=true + export ENABLE_REACT_CRUD_VIEWS=true + export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset +fi +# +# Make sure we have dev requirements installed +# +if [ -f "${REQUIREMENTS_LOCAL}" ]; then + echo "Installing local overrides at ${REQUIREMENTS_LOCAL}" + pip install -r "${REQUIREMENTS_LOCAL}" +else + echo "Skipping local overrides" +fi + +if [[ "${1}" == "worker" ]]; then + echo "Starting Celery worker..." + celery --app=superset.tasks.celery_app:app worker -Ofair -l INFO +elif [[ "${1}" == "beat" ]]; then + echo "Starting Celery beat..." + celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid -l INFO -s "${SUPERSET_HOME}"/celerybeat-schedule +elif [[ "${1}" == "app" ]]; then + echo "Starting web app..." + flask run -p 8088 --with-threads --reload --debugger --host=0.0.0.0 +elif [[ "${1}" == "app-gunicorn" ]]; then + echo "Starting web app..." + /usr/bin/run-server.sh +fi diff --git a/docker/superset/docker-ci.sh b/docker/superset/docker-ci.sh new file mode 100755 index 0000000..9e97cbb --- /dev/null +++ b/docker/superset/docker-ci.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +/app/docker/docker-init.sh + +# TODO: copy config overrides from ENV vars + +# TODO: run celery in detached state +export SERVER_THREADS_AMOUNT=8 +# start up the web server + +/usr/bin/run-server.sh diff --git a/docker/superset/docker-frontend.sh b/docker/superset/docker-frontend.sh new file mode 100755 index 0000000..4c0d01e --- /dev/null +++ b/docker/superset/docker-frontend.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -e + +cd /app/superset-frontend +npm install -g npm@7 +npm install -f --no-optional --global webpack webpack-cli +npm install -f --no-optional + +echo "Running frontend" +npm run dev diff --git a/docker/superset/docker-init.sh b/docker/superset/docker-init.sh new file mode 100755 index 0000000..d5ead50 --- /dev/null +++ b/docker/superset/docker-init.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -e + +# +# Always install local overrides first +# +/app/docker/docker-bootstrap.sh + +STEP_CNT=4 + +echo_step() { +cat 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�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