diff --git a/.env.docker.example b/.env.docker.example index 37dc8f99..4bf3efda 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -1,8 +1,18 @@ +MB_PORT=4300 CLIENT_PORT=4400 API_PORT=4500 -MB_INSTANCE_URL="https://shoppy.metabaseapp.com" -METABASE_JWT_SHARED_SECRET="your_secret_here" +PREMIUM_EMBEDDING_TOKEN="" +METABASE_JWT_SHARED_SECRET="ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + +METABASE_ADMIN_EMAIL: "admin@example.com" +METABASE_ADMIN_PASSWORD="foobarbaz" + +METABASE_APP_DB_HOST: "shoppy_app_db" +METABASE_APP_DB_PORT: "5432" +METABASE_APP_DB: "shoppy_app" +METABASE_APP_DB_USER: "shoppy_app" +METABASE_APP_DB_PASSWORD: "foobarbaz" SHOPPY_DB_HOST="shoppy_db" SHOPPY_DB_PORT="5432" @@ -11,3 +21,14 @@ SHOPPY_DB_USER="shoppy" SHOPPY_DB_PASSWORD="foobarbaz" WATCH="false" + +# for development +# MB_RUN_MODE="dev" +# METASTORE_DEV_SERVER_URL="" + +# Datadog RUM (optional client-side monitoring; leave empty to disable). +DATADOG_APPLICATION_ID="" +DATADOG_CLIENT_TOKEN="" +DATADOG_SITE="" +DATADOG_SERVICE="" +DATADOG_ENV="" diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index f8e097cd..bd8d6c15 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -27,10 +27,6 @@ jobs: role-session-name: GitHub_to_AWS_via_FederatedOIDC aws-region: us-east-1 - - name: Get runner public IP - id: runner-ip - run: echo "ip=$(curl -s https://checkip.amazonaws.com)" >> "$GITHUB_OUTPUT" - - name: Ensure that Esno executable is ready run: yarn install --cwd api --frozen-lockfile diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 85def127..537bd00f 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -14,20 +14,29 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 15 name: e2e-tests + env: + # The local Metabase runs EE in dev mode and validates its token against the metastore. + PREMIUM_EMBEDDING_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }} + MB_RUN_MODE: dev + METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }} permissions: contents: read steps: - uses: actions/checkout@v4 - - name: Prepare env + - name: Run Shoppy in Docker (local Metabase seeded from serialized data) run: | cp .env.docker.example .env.docker - # The api signs SSO JWTs that production Metabase must trust, so use the production secret. - sed -i "s|^METABASE_JWT_SHARED_SECRET=.*|METABASE_JWT_SHARED_SECRET=\"${{ secrets.SHOPPY_PROD_JWT_SHARED_SECRET }}\"|" .env.docker + yarn docker:e2e:up --wait - - name: Run Shoppy (client + api + warehouse) against production Metabase - run: yarn docker:e2e:up --wait + - name: Dump container logs on failure + if: failure() + run: | + echo "=== shoppy-metabase-1 ===" && docker logs shoppy-metabase-1 2>&1 || true + echo "=== shoppy-api-1 ===" && docker logs shoppy-api-1 2>&1 || true + echo "=== shoppy-client-1 ===" && docker logs shoppy-client-1 2>&1 || true + echo "=== container statuses ===" && docker ps -a 2>&1 || true - name: Install Chrome v111 uses: browser-actions/setup-chrome@v1 @@ -43,13 +52,6 @@ jobs: id: run-e2e-tests run: cd e2e && yarn cypress:run - - name: Dump container logs on failure - if: failure() - run: | - echo "=== shoppy-client-1 ===" && docker logs shoppy-client-1 2>&1 || true - echo "=== shoppy-api-1 ===" && docker logs shoppy-api-1 2>&1 || true - echo "=== container statuses ===" && docker ps -a 2>&1 || true - - name: Upload Cypress Artifacts upon failure uses: actions/upload-artifact@v4 if: ${{ steps.run-e2e-tests.outcome != 'success' }} diff --git a/.gitignore b/.gitignore index ec1940d1..34ed61fc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ node_modules .pnp .pnp.js +# local dist +local-dist/* +!local-dist/.gitkeep + # testing coverage cypress @@ -13,6 +17,11 @@ cypress dist build +# db dumps +*.sql +# but allow migration files +!api/drizzle/**/*.sql + # misc .DS_Store .env.development.local diff --git a/Dockerfile b/Dockerfile index 874fdab8..601f6b22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,13 @@ RUN yarn --frozen-lockfile # Copy source code last (changes most frequently) COPY --exclude=./api --exclude=./metabase . . +RUN if [ -d "./local-dist/embedding-sdk" ]; then \ + echo "Local embedding-sdk dist is found in ./local-dist/embedding-sdk, installing it..."; \ + yarn add file:./local-dist/embedding-sdk; \ + else \ + echo "Local embedding-sdk dist is not found in ./local-dist/embedding-sdk, skipping copy"; \ + fi + RUN if [ "$WATCH" != "true" ]; then \ echo "WATCH env is not set; running production yarn build..."; \ yarn build; \ diff --git a/README.md b/README.md index f079554e..2e905c76 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,22 @@ This demo uses the data from the hosted Metabase Cloud instance and provides a h ### Using Docker -- Clone `.env.docker.example` to `.env.docker`. Set `MB_INSTANCE_URL` to your Metabase instance and `METABASE_JWT_SHARED_SECRET` to that instance's JWT shared secret. -- Run `yarn docker:up` for a production build, or `WATCH=true yarn docker:up` for a dev build with watch. - - The command launches the Shoppy DWH, API and Client, pointing at the configured Metabase instance. +- Clone `.env.docker.example` to `.env.docker` and set the proper `PREMIUM_EMBEDDING_TOKEN` value. +- Run Docker via `yarn docker:up` for the `production` build or `WATCH=true yarn docker:up` for the development build with the `watch` support. + - The command launches a local MB instance, the Shoppy DWH, Shoppy API and Shoppy Client. The local Metabase is seeded from the committed serialized data (`./metabase/metabase_data.tar.gz`), so no database dump is needed. - Visit `http://localhost:4400`. - To stop containers run `yarn docker:down`. - To remove containers and images completely run `yarn docker:rm`. +#### Local development (For Metabase developers) + +To run the containers with a locally built `metabase.jar` and/or a locally built Embedding SDK: + +- Copy a locally built `metabase.jar` to `./local-dist/metabase.jar`. Without it, the `metabase.jar` from the Docker image is used. +- Copy a locally built Embedding SDK package to `./local-dist/embedding-sdk`. Without it, the `@metabase/embedding-sdk-react` version from `package.json` is used. +- Run `yarn docker:local-dist:up` to start the containers using the local dist from `./local-dist`. +- To remove containers and images completely run `yarn docker:rm`. + ### Using an existing running MB instance - Place the metabase repository in `../metabase` @@ -65,11 +74,11 @@ If you cannot use the hosted JWT server, you can run the JWT server locally. ### Running e2e tests (For Metabase developers) -The e2e tests run the client, api and warehouse locally (via Docker) against the production Metabase instance, and drive the local client with Cypress. +The e2e tests run the local Shoppy stack (a local Metabase seeded from the committed serialized data) and drive it with Cypress. -- In `.env.docker`, set `METABASE_JWT_SHARED_SECRET` to the production instance's JWT shared secret (from 1Password) so the api's SSO tokens are trusted, and `MB_INSTANCE_URL` to that instance. -- Start the stack: `yarn docker:e2e:up --wait`. -- Run the tests: `cd e2e && yarn cypress:run` (headless) or `yarn cypress:open` (Cypress UI). -- Stop the stack: `yarn docker:down`. +- Clone `.env.docker.example` to `.env.docker` and set the proper `PREMIUM_EMBEDDING_TOKEN` value. +- Run `yarn docker:e2e:up --wait` to start all required containers. +- Run the tests: `cd e2e && yarn cypress:run` (headless) or `cd e2e && yarn cypress:open` (Cypress UI). +- To stop containers run `yarn docker:rm`. -CI runs the same suite in `.github/workflows/e2e-tests.yml`, injecting the production secret from the `SHOPPY_PROD_JWT_SHARED_SECRET` GitHub secret. +CI runs the same suite in `.github/workflows/e2e-tests.yml`. diff --git a/api/src/constants/env.ts b/api/src/constants/env.ts index 9b2cce9d..b1e35973 100644 --- a/api/src/constants/env.ts +++ b/api/src/constants/env.ts @@ -14,6 +14,3 @@ export const FRONTEND_URL = process.env.FRONTEND_URL ?? `http://localhost:${process.env.CLIENT_PORT}` export const { VERCEL_ENV } = process.env - -// This variable is used in a local (Docker) environment only -export const METABASE_ADMIN_EMAIL = process.env.METABASE_ADMIN_EMAIL diff --git a/docker-compose.local-dist.yml b/docker-compose.local-dist.yml new file mode 100644 index 00000000..7b24a77f --- /dev/null +++ b/docker-compose.local-dist.yml @@ -0,0 +1,9 @@ +services: + metabase: + volumes: + - type: bind + source: ./local-dist/metabase.jar + target: /app/metabase.jar + read_only: true + bind: + create_host_path: false diff --git a/docker-compose.yml b/docker-compose.yml index 55143a10..97ed26e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -# Shoppy stack: client + api + warehouse. Metabase is the production instance (MB_INSTANCE_URL); -# the client talks to it same-origin via `/mb`, which the api proxies onward (see api/src/main.ts). services: shoppy_db: image: postgres:latest @@ -16,8 +14,25 @@ services: expose: - "${SHOPPY_DB_PORT}" + shoppy_app_db: + image: postgres:latest + environment: + POSTGRES_PORT: "${METABASE_APP_DB_PORT}" + POSTGRES_DB: "${METABASE_APP_DB}" + POSTGRES_USER: "${METABASE_APP_DB_USER}" + POSTGRES_PASSWORD: "${METABASE_APP_DB_PASSWORD}" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${METABASE_APP_DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + expose: + - "${METABASE_APP_DB_PORT}" + api: depends_on: + shoppy_app_db: + condition: service_healthy shoppy_db: condition: service_healthy build: @@ -30,8 +45,9 @@ services: CLIENT_PORT: "${CLIENT_PORT}" IS_LOCAL_DB: "true" DB_URL: "postgres://${SHOPPY_DB_USER}:${SHOPPY_DB_PASSWORD}@${SHOPPY_DB_HOST}:${SHOPPY_DB_PORT}/${SHOPPY_DB}" - METABASE_INSTANCE_URL: "${MB_INSTANCE_URL}" + METABASE_INSTANCE_URL: "http://metabase:${MB_PORT}" METABASE_JWT_SHARED_SECRET: "${METABASE_JWT_SHARED_SECRET}" + METABASE_ADMIN_EMAIL: "${METABASE_ADMIN_EMAIL}" healthcheck: test: curl --fail -X GET -I "http://localhost:${API_PORT}/" || exit 1 interval: 15s @@ -40,15 +56,63 @@ services: ports: - "${API_PORT}:${API_PORT}" + metabase: + depends_on: + shoppy_app_db: + condition: service_healthy + shoppy_db: + condition: service_healthy + api: + condition: service_healthy + image: metabase/metabase-enterprise:v1.63.x + entrypoint: ["sh", "/app/entrypoint.sh"] + environment: + MB_CONFIG_FILE_PATH: "./app/config.yml" + MB_JETTY_PORT: "${MB_PORT}" + MB_EDITION: "ee" + MB_SITE_URL: "http://localhost:${MB_PORT}/" + MB_PREMIUM_EMBEDDING_TOKEN: "${PREMIUM_EMBEDDING_TOKEN}" + MB_RUN_MODE: "${MB_RUN_MODE}" + METASTORE_DEV_SERVER_URL: "${METASTORE_DEV_SERVER_URL}" + MB_JWT_IDENTITY_PROVIDER_URI: "http://localhost:${API_PORT}/sso/metabase" + MB_JWT_SHARED_SECRET: "${METABASE_JWT_SHARED_SECRET}" + MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all" + MB_DB_TYPE: "postgres" + MB_DB_HOST: "${METABASE_APP_DB_HOST}" + MB_DB_PORT: "${METABASE_APP_DB_PORT}" + MB_DB_USER: "${METABASE_APP_DB_USER}" + MB_DB_PASS: "${METABASE_APP_DB_PASSWORD}" + MB_DB_DBNAME: "${METABASE_APP_DB}" + METABASE_ADMIN_EMAIL: "${METABASE_ADMIN_EMAIL}" + METABASE_ADMIN_PASSWORD: "${METABASE_ADMIN_PASSWORD}" + SHOPPY_DB_HOST: "${SHOPPY_DB_HOST}" + SHOPPY_DB_PORT: "${SHOPPY_DB_PORT}" + SHOPPY_DB: "${SHOPPY_DB}" + SHOPPY_DB_USER: "${SHOPPY_DB_USER}" + SHOPPY_DB_PASSWORD: "${SHOPPY_DB_PASSWORD}" + healthcheck: + test: curl --fail -X GET -I "http://localhost:${MB_PORT}/api/health" || exit 1 + interval: 15s + timeout: 5s + retries: 10 + ports: + - "${MB_PORT}:${MB_PORT}" + volumes: + - ./metabase/entrypoint.sh:/app/entrypoint.sh + - ./metabase/config.yml:/app/config.yml + - ./metabase/metabase_data.tar.gz:/app/metabase_data.tar.gz + client: depends_on: + metabase: + condition: service_healthy api: condition: service_healthy build: context: . dockerfile: Dockerfile args: - VITE_APP_METABASE_INSTANCE_URL: "/mb" + VITE_APP_METABASE_INSTANCE_URL: "http://localhost:${MB_PORT}" VITE_APP_BACKEND_URL: "http://localhost:${API_PORT}" VITE_DATADOG_APPLICATION_ID: "${DATADOG_APPLICATION_ID}" VITE_DATADOG_CLIENT_TOKEN: "${DATADOG_CLIENT_TOKEN}" @@ -58,7 +122,7 @@ services: WATCH: "${WATCH}" environment: CLIENT_PORT: "${CLIENT_PORT}" - VITE_APP_METABASE_INSTANCE_URL: "/mb" + VITE_APP_METABASE_INSTANCE_URL: "http://localhost:${MB_PORT}" VITE_APP_BACKEND_URL: "http://localhost:${API_PORT}" VITE_APP_DOCKER_OVERRIDE_BACKEND_URL: "http://api:${API_PORT}" WATCH: "${WATCH}" diff --git a/e2e/package.json b/e2e/package.json index e66e138e..5a2f42ee 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -9,6 +9,7 @@ }, "scripts": { "cypress:open": "env-cmd -f ../.env.docker cypress open --config-file ./support/cypress.config.js --e2e", - "cypress:run": "env-cmd -f ../.env.docker cypress run --config-file ./support/cypress.config.js" + "cypress:run": "env-cmd -f ../.env.docker cypress run --config-file ./support/cypress.config.js", + "cypress:synthetic": "cypress run --config-file ./support/cypress.synthetic.config.js --browser chrome" } } diff --git a/e2e/support/helpers/sign-in.js b/e2e/support/helpers/sign-in.js new file mode 100644 index 00000000..e4cb8a67 --- /dev/null +++ b/e2e/support/helpers/sign-in.js @@ -0,0 +1,59 @@ +// TODO (Kelvin 2026-07-07) bandage, not a fix (EMB-2059) — see compatibility.cy.spec.js. + +export const METABASE_URL = "http://localhost:4300" + +// JWT_SHARED_SECRET also appears in .env.docker.example, so it's not a real secret. +const JWT_SHARED_SECRET = + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +const ADMIN_EMAIL = "shoppy@metabase.com" + +function base64url(bytes) { + let binary = "" + new Uint8Array(bytes).forEach((byte) => { + binary += String.fromCharCode(byte) + }) + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") +} + +async function getSignedJwtForAdmin() { + const encoder = new TextEncoder() + const header = { alg: "HS256", typ: "JWT" } + const payload = { + email: ADMIN_EMAIL, + first_name: "Shoppy", + last_name: "Admin", + exp: Math.floor(Date.now() / 1000) + 600, + } + const signingInput = `${base64url(encoder.encode(JSON.stringify(header)))}.${base64url( + encoder.encode(JSON.stringify(payload)), + )}` + + const key = await crypto.subtle.importKey( + "raw", + encoder.encode(JWT_SHARED_SECRET), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ) + const signature = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(signingInput), + ) + + return `${signingInput}.${base64url(signature)}` +} + +export function signInAsAdmin() { + return cy + .wrap(getSignedJwtForAdmin()) + .then((jwt) => + cy.request({ + method: "GET", + url: `${METABASE_URL}/auth/sso`, + qs: { jwt }, + headers: { "X-Metabase-Client": "embedding-sdk-react" }, + }), + ) + .then(({ body }) => body.id) +} diff --git a/e2e/test/compatibility.cy.spec.js b/e2e/test/compatibility.cy.spec.js index 7bf6214d..af15c008 100644 --- a/e2e/test/compatibility.cy.spec.js +++ b/e2e/test/compatibility.cy.spec.js @@ -1,3 +1,5 @@ +import { METABASE_URL, signInAsAdmin } from "../support/helpers/sign-in" + const TIMEOUT = 20000 describe("Embedding SDK: shoppy compatibility", () => { @@ -128,48 +130,51 @@ describe("Embedding SDK: shoppy compatibility", () => { }) }) - it("should display dashboards with sandboxing for different shops", () => { - const getOrdersCountForShop = (site) => { - cy.log(`Get total orders count for ${site}`) - - cy.findByTestId(`site-switcher-button-${site}`).click() - - return ( - cy - .findAllByTestId("dashcard-container", { timeout: TIMEOUT }) - // TODO: find a way to not rely on dashboard name - .filter(":contains('Total Orders')") - .findByTestId("scalar-container", { timeout: TIMEOUT }) - .invoke("text") - ) - } - - cy.visit("/admin/analytics") - - cy.get("main").within(() => { - cy.findByText("Orders", { timeout: TIMEOUT }).click() - }) - - const shops = ["proficiency", "stitch", "luminara", "pug"] - const counts = [] + // TODO (Kelvin 2026-07-07) bandage, not a fix. Metabase's appdb search-index reindex has a + // race that can strand a fully-built index as "pending" instead of activating it. Couldn't + // land the real backend fix yet; this forces a reindex and waits for it first. The actual + // backend bug (QUE2-736) hasn't landed yet. + // Scoped to just this test, not the whole suite — it's the only one that needs it. + describe("data picker", () => { + before(() => { + const REINDEX_POLL_INTERVAL_MS = 1000 + const REINDEX_POLL_MAX_ATTEMPTS = 60 + + function waitForDatasetIndex(sessionId, attempt = 0) { + cy.request({ + method: "GET", + url: `${METABASE_URL}/api/search?models=dataset`, + headers: { "X-Metabase-Session": sessionId }, + }).then(({ body }) => { + if (body.total > 0) { + return + } + if (attempt >= REINDEX_POLL_MAX_ATTEMPTS) { + // Fail loudly here instead of silently proceeding into a doomed test — a confusing + // "can't find Orders" UI assertion failure downstream is much harder to diagnose than + // this being the actual problem. + throw new Error( + `waitForDatasetIndex: no datasets found after ${attempt} attempts`, + ) + } + + cy.wait(REINDEX_POLL_INTERVAL_MS) + waitForDatasetIndex(sessionId, attempt + 1) + }) + } + + signInAsAdmin().then((sessionId) => { + cy.request({ + method: "POST", + url: `${METABASE_URL}/api/search/force-reindex`, + headers: { "X-Metabase-Session": sessionId }, + failOnStatusCode: false, + }) - cy.wrap(shops).each((site) => { - getOrdersCountForShop(site).then((text) => { - counts.push(text) + waitForDatasetIndex(sessionId) }) }) - cy.then(() => { - const unique = new Set(counts) - - expect( - unique.size, - `All counts: [${counts.join(", ")}]`, - ).to.be.greaterThan(1) - }) - }) - - describe("data picker", () => { it("should not display tables in the data picker", () => { cy.visit({ url: "/admin/analytics/new/from-scratch", diff --git a/local-dist/.gitkeep b/local-dist/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/metabase/config.yml b/metabase/config.yml new file mode 100644 index 00000000..1dd2c826 --- /dev/null +++ b/metabase/config.yml @@ -0,0 +1,36 @@ +version: 1 +config: + users: + - first_name: "Admin" + last_name: "Admin" + password: "{{ env METABASE_ADMIN_PASSWORD }}" + email: "{{ env METABASE_ADMIN_EMAIL }}" + is_superuser: true + - first_name: "Rene" + last_name: "Mueller" + password: "{{ env METABASE_ADMIN_PASSWORD }}" + email: "rene@example.com" + - first_name: "Cecilia" + last_name: "Stark" + password: "{{ env METABASE_ADMIN_PASSWORD }}" + email: "cecilia@example.com" + - first_name: "Emily" + last_name: "Johnson" + password: "{{ env METABASE_ADMIN_PASSWORD }}" + email: "emily@example.com" + - first_name: "Jennifer" + last_name: "Martinez" + password: "{{ env METABASE_ADMIN_PASSWORD }}" + email: "jennifer@example.com" + settings: + enable-embedding-sdk: true + jwt-enabled: true + databases: + - name: Customer Zero Database + engine: postgres + details: + host: "{{ env SHOPPY_DB_HOST }}" + port: "{{ env SHOPPY_DB_PORT }}" + dbname: "{{ env SHOPPY_DB }}" + user: "{{ env SHOPPY_DB_USER }}" + password: "{{ env SHOPPY_DB_PASSWORD }}" diff --git a/metabase/entrypoint.sh b/metabase/entrypoint.sh new file mode 100644 index 00000000..a5564b2b --- /dev/null +++ b/metabase/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e + +./app/run_metabase.sh "$@" & +SERVER_PID=$! + +until curl -s http://metabase:${MB_JETTY_PORT}/api/health | grep -q "\"status\":\"ok\""; do + echo "Waiting for the Metabase to be healthy..." + sleep 2 +done + +echo "Metabase is healthy. Running additional configuration..." + +echo "Running import..."; + +SESSION_ID=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -d "{\"username\": \"${METABASE_ADMIN_EMAIL}\", \"password\": \"${METABASE_ADMIN_PASSWORD}\"}" \ + http://metabase:${MB_JETTY_PORT}/api/session | sed -n 's/.*"id":"\([^"]*\)".*/\1/p') + +curl -X POST \ + -H "X-Metabase-Session: $SESSION_ID" \ + -F file=@./app/metabase_data.tar.gz \ + http://metabase:${MB_JETTY_PORT}/api/ee/serialization/import + +wait $SERVER_PID diff --git a/metabase/export_metabase_data.sh b/metabase/export_metabase_data.sh new file mode 100755 index 00000000..086af65c --- /dev/null +++ b/metabase/export_metabase_data.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +if [ -z "$METABASE_INSTANCE_API_KEY" ]; then + echo "Define METABASE_INSTANCE_API_KEY value in your environment" + exit 1 +fi + +if [ -z "$METABASE_INSTANCE_URL" ]; then + echo "Define METABASE_INSTANCE_URL value in your environment" + exit 1 +fi + +curl -X POST \ + -H "x-api-key: $METABASE_INSTANCE_API_KEY" \ + "$METABASE_INSTANCE_URL/api/ee/serialization/export?data_model=false&dirname=metabase_data" \ + -o metabase_data.tar.gz + +tar -xzf metabase_data.tar.gz + +find metabase_data -type f -name "export.log" -delete + +# Find all directories ending with '_user_generated' and process them +find metabase_data/collections -type d -name "*_user_generated" | while IFS= read -r dir; do + # Recursively delete all nested directories inside the '_user_generated' directory + find "$dir" -mindepth 1 -type d -exec rm -rf {} + + + # Make sure to preserve files ending with '_user_generated.yaml' + find "$dir" -type f ! -name "*_user_generated.yaml" -exec rm -f {} + +done + +tar -czf metabase_data.tar.gz metabase_data +rm -rf metabase_data diff --git a/metabase/metabase_data.tar.gz b/metabase/metabase_data.tar.gz new file mode 100644 index 00000000..0ff40974 Binary files /dev/null and b/metabase/metabase_data.tar.gz differ diff --git a/package.json b/package.json index 9aa39c62..162fccf8 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ }, "scripts": { "docker:up": "docker compose --env-file .env.docker up", + "docker:local-dist:up": "docker compose -f docker-compose.yml -f docker-compose.local-dist.yml --env-file .env.docker up", "docker:e2e:up": "docker compose --env-file .env.docker up --build", "docker:down": "docker compose --env-file .env.docker down", "docker:rm": "yarn docker:down --rmi all --volumes",