From 497ae697ee6d9d9898ceb2f110914d0db36e2835 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 23:37:25 -0800 Subject: [PATCH 01/11] chore(deps): update actions/setup-python action to v4 (#2142) Signed-off-by: Renovate Bot Signed-off-by: Renovate Bot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test-chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 8d08b70c69..b913784631 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -19,7 +19,7 @@ jobs: uses: azure/setup-helm@v2.2 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.7 From aedb845d89170478a86a0c672956de48f721f0ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 23:37:51 -0800 Subject: [PATCH 02/11] chore(deps): update azure/setup-helm action to v3 (#2143) Signed-off-by: Renovate Bot Signed-off-by: Renovate Bot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test-chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index b913784631..a7ccf1569f 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -16,7 +16,7 @@ jobs: fetch-depth: 0 - name: Setup Helm - uses: azure/setup-helm@v2.2 + uses: azure/setup-helm@v3.4 - name: Setup Python uses: actions/setup-python@v4 From 7e9ed4f3c966af6c85f3cfaaf82a2a1a6ed07243 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 23:38:21 -0800 Subject: [PATCH 03/11] chore(deps): update helm/chart-testing-action action to v2.3.1 (#2154) Signed-off-by: Renovate Bot Signed-off-by: Renovate Bot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test-chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index a7ccf1569f..7d669bccf3 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -24,7 +24,7 @@ jobs: python-version: 3.7 - name: Setup chart-testing - uses: helm/chart-testing-action@v2.3.0 + uses: helm/chart-testing-action@v2.3.1 - name: Run chart-testing (list-changed) id: list-changed From 2253fee6b37ae61bad6cd75cfb9bd2b3e650f99c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 23:38:59 -0800 Subject: [PATCH 04/11] chore(deps): update helm release common to v2 (#2157) Signed-off-by: Renovate Bot Signed-off-by: Renovate Bot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- chart/Chart.lock | 6 +++--- chart/Chart.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chart/Chart.lock b/chart/Chart.lock index 352354e954..58554a821c 100644 --- a/chart/Chart.lock +++ b/chart/Chart.lock @@ -1,9 +1,9 @@ dependencies: - name: common repository: https://charts.bitnami.com/bitnami - version: 1.17.1 + version: 2.1.2 - name: postgresql repository: https://charts.bitnami.com/bitnami version: 11.8.1 -digest: sha256:4cd486fed5762493192ee280fa1ac8d6a14b4b2a8901ffa9410e2348f8c21bf9 -generated: "2022-09-07T06:26:19.99115998Z" +digest: sha256:5d4b20341df7c1d2a1e1e16a9e3248a5e4eabf765b307bb05acf13447ff51ae5 +generated: "2022-11-10T20:03:21.425592157Z" diff --git a/chart/Chart.yaml b/chart/Chart.yaml index b6425fa39a..adc1c35ddd 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -5,7 +5,7 @@ dependencies: repository: https://charts.bitnami.com/bitnami tags: - bitnami-common - version: 1.17.1 + version: 2.1.2 - condition: postgresql.enabled name: postgresql repository: https://charts.bitnami.com/bitnami @@ -29,4 +29,4 @@ name: marquez sources: - https://github.com/MarquezProject/marquez - https://marquezproject.github.io/marquez/ -version: 0.27.0 +version: 0.27.1 From e17536130e0809225fe9be3b3588fc65aaab6b84 Mon Sep 17 00:00:00 2001 From: Willy Lulciuc Date: Tue, 15 Nov 2022 00:13:17 -0800 Subject: [PATCH 05/11] Set `--allow-releaseinfo-change` (#2247) * Set `--allow-releaseinfo-change` Signed-off-by: wslulciuc * continued: Set `--allow-releaseinfo-change` Signed-off-by: wslulciuc * continued: Set `--allow-releaseinfo-change` Signed-off-by: wslulciuc * Enable debug Signed-off-by: wslulciuc * continued: Enable debug Signed-off-by: wslulciuc * continued: Set `--allow-releaseinfo-change` Signed-off-by: wslulciuc Signed-off-by: wslulciuc --- .circleci/get-jdk17.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.circleci/get-jdk17.sh b/.circleci/get-jdk17.sh index 172eeaef27..a43b7a90e9 100755 --- a/.circleci/get-jdk17.sh +++ b/.circleci/get-jdk17.sh @@ -14,11 +14,9 @@ # # Usage: $ ./get-jdk17.sh -set -e - wget -qO - https://adoptium.jfrog.io/adoptium/api/gpg/key/public | sudo apt-key add - sudo add-apt-repository --yes https://adoptium.jfrog.io/adoptium/deb -sudo apt-get update && sudo apt-get install temurin-17-jdk +sudo apt-get update --allow-releaseinfo-change && sudo apt-get install --yes temurin-17-jdk sudo update-alternatives --set java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java sudo update-alternatives --set javac /usr/lib/jvm/temurin-17-jdk-amd64/bin/javac java -version From be52a065b6821c133b367e84e3d5a6d688cc4872 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 00:20:38 -0800 Subject: [PATCH 06/11] Bump loader-utils from 1.4.0 to 1.4.1 in /web (#2242) Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Willy Lulciuc --- web/package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 2f963336e9..f11e2ef0f0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7252,9 +7252,9 @@ } }, "node_modules/file-loader/node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -11092,9 +11092,9 @@ } }, "node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.1.tgz", + "integrity": "sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q==", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -21685,9 +21685,9 @@ }, "dependencies": { "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -24561,9 +24561,9 @@ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" }, "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.1.tgz", + "integrity": "sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q==", "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", From 3998e05fc1c05d7647533db9d842bf293738cdde Mon Sep 17 00:00:00 2001 From: Peter Hicks Date: Tue, 15 Nov 2022 09:41:10 -0800 Subject: [PATCH 07/11] Reference a cloud link since asset bundling broke with new webpack asset loader. (#2212) Co-authored-by: phix Co-authored-by: Willy Lulciuc --- web/src/components/header/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/header/Header.tsx b/web/src/components/header/Header.tsx index 36a843d051..cf08512f7c 100644 --- a/web/src/components/header/Header.tsx +++ b/web/src/components/header/Header.tsx @@ -46,7 +46,7 @@ const Header = (props: HeaderProps): ReactElement => { - Marquez Logo + Marquez Logo From a7f88b06d5f0e53ddfd782d1fe2e660bd89fd52a Mon Sep 17 00:00:00 2001 From: Willy Lulciuc Date: Tue, 15 Nov 2022 15:16:13 -0800 Subject: [PATCH 08/11] [DOCS] Add deployment security expectations (#2250) * Add tls/https section to deployment overview Signed-off-by: wslulciuc * Add encryption at rest info to deployment overview Signed-off-by: wslulciuc Signed-off-by: wslulciuc --- docs/deployment-overview.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/deployment-overview.md b/docs/deployment-overview.md index ddca10d834..869ea1d986 100644 --- a/docs/deployment-overview.md +++ b/docs/deployment-overview.md @@ -6,14 +6,20 @@ layout: deployment-overview ## Helm Chart -Marquez uses [Helm](https://helm.sh) to manage deployments onto [Kubernetes](https://kubernetes.io) in a cloud environment. The chart and templates for the [HTTP API](https://github.com/MarquezProject/marquez/tree/main/api) server and [Web UI](https://github.com/MarquezProject/marquez/tree/main/web) are maintained in the Marquez [repository](https://github.com/MarquezProject/marquez) and can be found in the [chart](https://github.com/MarquezProject/marquez/tree/main/chart) directory. The chart's base `values.yaml` file includes an option to easily override deployment [settings](https://github.com/MarquezProject/marquez/tree/main/chart#configuration). +Marquez uses [Helm](https://helm.sh) to manage deployments onto [Kubernetes](https://kubernetes.io) in a cloud environment. The chart and templates for the [HTTP API](https://github.com/MarquezProject/marquez/tree/main/api) server and [Web UI](https://github.com/MarquezProject/marquez/tree/main/web) are maintained in the Marquez [repository](https://github.com/MarquezProject/marquez) and can be found in the [chart](https://github.com/MarquezProject/marquez/tree/main/chart) directory. The chart's base [`values.yaml`](https://github.com/MarquezProject/marquez/blob/main/chart/values.yaml#L183) file includes an option to easily override deployment [settings](https://github.com/MarquezProject/marquez/tree/main/chart#configuration). -> **Note:** The Marquez HTTP API server and Web UI images are publshed to [DockerHub](https://hub.docker.com/r/marquezproject/marquez). +> **Note:** The Marquez HTTP API server and Web UI images are published to [DockerHub](https://hub.docker.com/r/marquezproject/marquez). + +### `TLS/HTTPS` + +To enable HTTPS traffic when deploying Marquez onto Kubernetes, use the flag [`ingress.enabled`](https://github.com/MarquezProject/marquez/tree/main/chart#ingress-parameters) to configure the ingress controller. To secure ingress traffic, use the [`ingress.tls`](https://github.com/MarquezProject/marquez/tree/main/chart#ingress-parameters) section to define your TLS `secret` and `hosts` (see `ingress` in the chart's base [`values.yaml`](https://github.com/MarquezProject/marquez/blob/main/chart/values.yaml#L183) for more details). ## Database The Marquez [HTTP API](https://marquezproject.github.io/marquez/openapi.html) server relies only on PostgreSQL to store dataset, job, and run metadata allowing for minimal operational overhead. We recommend a cloud provided databases, such as AWS [RDS](https://aws.amazon.com/rds/postgresql), when deploying Marquez onto Kubernetes. +> **Note:** We encourage enabling encryption at rest when provisioning your database. + ## Architecture #### DOCKER @@ -44,7 +50,9 @@ The Marquez [HTTP API](https://marquezproject.github.io/marquez/openapi.html) se ## Authentication -Our [clients](https://github.com/MarquezProject/marquez/tree/main/clients) support authentication by automatically sending an API key on each request via [_Bearer Auth_](https://datatracker.ietf.org/doc/html/rfc6750) when configured on client instantiation. By default, the Marquez HTTP API does not require any form of authentication or authorization. +Our [clients](https://github.com/MarquezProject/marquez/tree/main/clients) support authentication by automatically sending an API key on each request via [_Bearer Auth_](https://datatracker.ietf.org/doc/html/rfc6750) when configured on client instantiation. + +> **Note:** By default, the Marquez HTTP API server does not require any form of authentication or authorization. ## Next Steps From 7885c8c240cc83f85b0a71dd960cee40405c437c Mon Sep 17 00:00:00 2001 From: "pawel.leszczynski" Date: Wed, 16 Nov 2022 14:43:16 +0100 Subject: [PATCH 09/11] fix symlink table column length (#2217) Signed-off-by: Pawel Leszczynski Signed-off-by: Pawel Leszczynski --- .../resources/marquez/db/migration/V48__dataset_symlinks.sql | 2 +- .../marquez/db/migration/V52__alter_dataset_symlinks.sql | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 api/src/main/resources/marquez/db/migration/V52__alter_dataset_symlinks.sql diff --git a/api/src/main/resources/marquez/db/migration/V48__dataset_symlinks.sql b/api/src/main/resources/marquez/db/migration/V48__dataset_symlinks.sql index 4ea94721fb..aeacd54362 100644 --- a/api/src/main/resources/marquez/db/migration/V48__dataset_symlinks.sql +++ b/api/src/main/resources/marquez/db/migration/V48__dataset_symlinks.sql @@ -2,7 +2,7 @@ CREATE TABLE dataset_symlinks ( dataset_uuid UUID, - name VARCHAR(255) NOT NULL, + name VARCHAR NOT NULL, namespace_uuid UUID REFERENCES namespaces(uuid), type VARCHAR(64), is_primary BOOLEAN DEFAULT FALSE, diff --git a/api/src/main/resources/marquez/db/migration/V52__alter_dataset_symlinks.sql b/api/src/main/resources/marquez/db/migration/V52__alter_dataset_symlinks.sql new file mode 100644 index 0000000000..8ce804db8d --- /dev/null +++ b/api/src/main/resources/marquez/db/migration/V52__alter_dataset_symlinks.sql @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +ALTER TABLE dataset_symlinks ALTER COLUMN name TYPE VARCHAR; \ No newline at end of file From 1d8334b22fe014cecdbda4f066eafa6c6db5fab3 Mon Sep 17 00:00:00 2001 From: "pawel.leszczynski" Date: Thu, 17 Nov 2022 09:18:57 +0100 Subject: [PATCH 10/11] introduce search service (#2203) Signed-off-by: Pawel Leszczynski Signed-off-by: Pawel Leszczynski Co-authored-by: Willy Lulciuc --- proposals/2180-search-service.md | 123 ++++++++++++++++++ .../images/search_service_architecture.png | Bin 0 -> 54513 bytes 2 files changed, 123 insertions(+) create mode 100644 proposals/2180-search-service.md create mode 100644 proposals/assets/images/search_service_architecture.png diff --git a/proposals/2180-search-service.md b/proposals/2180-search-service.md new file mode 100644 index 0000000000..3d4ed0e355 --- /dev/null +++ b/proposals/2180-search-service.md @@ -0,0 +1,123 @@ +# Proposal: Search Service @ Marquez + +Author(s): @pawel-big-lebowski + +Created: 2022-10-20 + +## Overview + +Extensive search capabilities is an awaited feature discussed during community meeting on September 22nd 2022 +(see [meeting notes](https://wiki.lfaidata.foundation/pages/viewpage.action?pageId=18481442#MarquezCommunityMeetings&Calendar-Discussiontopics)) +which lead to creating Marquez Issue [#2171](https://github.com/MarquezProject/marquez/issues/2171]). +Some of missing capabilities include: searching by job facet, content of the processed SQL, dataset field names, etc. + +Current architecture of Marquez consists of Java rest service with PostgreSQL backend. This may be not sufficient +to implement such rich search capabilities. On the other hand, the simplicity of current Marquez architecture is +extremely practical as fresh install can be easily done in minutes. It requires only database backend to run. + +### Why should we introduce external search backend? + +It's worth asking **Why relational database backend is not enough?**. Currently, Marquez supports full text searching +based on PostgreSQL database. This has some limitations that cannot be overcome within existing stack. + + * Users want to search by anything (any table in Marquez backend database) and being able to do SQL +`LIKE` on any column of any Marquez table could be a tedious task which could kill database at the end. + * Within current Marquez architecture it is really important to have service up and running, so that incoming lineage +events get saved in database. We don't want dummy search query to stop loading the events. + * We cannot create TEXT index on all the text fields. Creating index on jsonb columns could be dangerous as we know the +size of such fields can be counted in megabytes. + +[ElasticSearch](https://www.elastic.co) is widely used open-source search engine. +It is beneficial to have its capabilities in Marquez because: + * It is designed to index and search JSON documents and OpenLineage events are JSONs by definition. + * It is capable of searching by selected JSON fields or whole documents like OpenLineage event. + +### Pluggable Marquez Search Engine + +Some users find it extremely useful to use Marquez as UI for browsing OpenLineage events that is really +easy to install. For those, additional search service backend could be an overkill. +We still want to have an option to run Marquez without extra architecture component +which will mean limited search capabilities but with simple installation steps. + +Similarly to database configuration, `yaml` configuration could contain extra section: +``` +search: + engine: [ElasticSearch, ...] + enabled: true + # engine specific properties + elastic.url: http://my-elastic-cluster:9200 +``` + +### Search Service in action + +

+ +

+ + * Marquez CLI should be capable of indexing existing `lineage_events` table. + * Additionally, each event will be indexed after being received by Marquez endpoint. + * Whole events will be indexed except for `spark_plan` and `spark_unknown` fields. Extra config entry will be added to allow other facets being blacklisted from indexing. + * Two indexes in ElasticSearch (ES) need to be created: + * `lineage_event`: to contain whole lineage events, + * `dataset`: to contain elements of `inputs` and `outputs`. + * ElasticSearch returns found documents, so additional `dataset` index is required. Otherwise, when searching for +datasets, returning whole OpenLineage event would not be sufficient. + * Whenever lineage event is received in Marquez, it is sent to ElasticSearch indexes. Keep in mind that assuring uniqueness on +Elastic side may be difficult, so in case of sending same events multiple times, Marquez will have a single copy of that (as long +as it's a single run id), but SearchService will contain duplicate entries. + * A user sends request to `/api/v1/search` with a query specified. Additional filter may be used to specify if `dataset`, `job` or both are to be searched. + * Current implementation returns instances of `SearchResult` class containing: + ``` + ResultType type; // enum DATASET or JOB + String name; + Instant updatedAt; + NamespaceName namespace; + NodeId nodeId; +``` + * In order to find datasets, ES will query `dataset` index. When searching for jobs, `run` and `job` subtrees of `lineage` index will be queried. + * A response from Search Service Backend will contain runId for jobs, and (namespace, name) tuples for datasets. `SearchResult` will +be created based on Search Service Backend response. + +### Search service interface + +ElasticSearch is a great example of a search service backend, but we are open to support any other +implementations. Integration of the search service backend with Marquez should rely on the following interfaces: + +```java +interface SearchService { + + /** + * Clears all indexed documents + */ + void clear(); + + /** + * Index multiple events. + * + * @param events + */ + void index(List events); + + /** + * Index a single event. + * + * @param event + */ + void index(LineageEvent event); + + /** + * Returns all datasets and jobs that match the provided query; matching of datasets and jobs are + * string based and case-insensitive. + * + * @param query Query containing pattern to match. + * @param filter The filter to apply to the query result. + * @param sort The sort to apply to the query result. + * @param limit The limit to apply to the query result. + * @return A SearchResult object. + */ + List search(String query, SearchFilter filter, SearchSort sort, int limit); +} +``` + +Current implementation of `SearchDao` should be renamed into `SimpleSearchDao` and used by a `SimpleSearchService` +with empty `index` methods. diff --git a/proposals/assets/images/search_service_architecture.png b/proposals/assets/images/search_service_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..2f427c61dda528ff4516420cb02ca44d5749b151 GIT binary patch literal 54513 zcmeFZcT`hP^e;+NP(&0_z(SEGRT6p^LJJT=O#(VyAD_k3>E>2iGXE{ zK_Fgn6|l^oldLEhBxCk>eH#a~>puh~Ma2LDcVISlPOb#Jrz5YpD)0<-C7@k^D^Lua z;YPp-1^hr_Rv<|$X#wC?#m&tHjYQk(H~~CpNQp^`f7ONTE;M4UN8i>cX4t? z0~bxSoiqMVBLv#R(+TeiGyqA7f`9@e8;p&I)Bi!>&v?)T8;5_!YpZ7KjxxiVXzRI( z8+uq_@%mPOkHPyNT7QWJEG7yt@b?P>$qoH?V+TCm!3AId%qy-Ai1Q!00rZ{zk(7#* zojSJ-ql9rFXH0r(g=dBy%b(TLtg@4Djsi?2OWeI!NHLHe|eyJxR<2 zjFVLJw3U^`BBfNsk+OgYG^KIcQjX$8Q-ZOv21?pq#ajyDre)&kC@yJ4G%|2RdP%|z zv|Y9E+9U&zHW}-Pl2kDSqu|B}G*(U94h>Z|bt8JgA=+AQ9{PAwJ4u|Qx0VzXf?c)5kv@(vGcp$L01z>ik+s*7weyw` z_t20bV}K%68I3;!(?v^pfgvPsTMt7gO>s#XJ8elXLup&Gy*<{548_7QYH&Eo9to&E z2xJJzSrsE~4>Z$OA!F>taljoI1L&;=K}}QE$j(edPuI)Ig&^Sy@iLIb8fZ&O;Ur8= z;SSy?JQ;&_(l#{!I~walvD!vb7(=|FF;-T>*a7RMC#|ckV{78*V}cVCQ`JO~Y<2L4 z9?ozFC#*Zv&PhX476TC{sv$ij;b;^DhQg^iV%?E?4oI9498AK(U}i*b1RP?6H`NtG zqM>GvYFe5yI!5{q5Kl=}O&m$u1A^BQ*VQx8BfwPbT~l#{mba&bkCTz7tg4=#n3*cl1Sm&2Nsx>XVk!s~ zH@J~H$y-wbX$&{ga3bhBn1Ix6-7!8`6TAitCFO17hF7t1adXDwi87k9;h-a0xi zTAr>rxEs(I20ts3sbt?hZ2bCb$_vJjKMl zO-a%q7Xye4%+r%BPC#h6J4(4}JDF)nnyBjA>Z%Zt#^P{CaaU<~IM5zrpek($HPu&v zq6lJMAgGwLA<(6hp}wgY%tV5Ov%%{++k%Z04;UWb!BWb!24B%v>o&?GmSgs_PkG_M9 zr>iH{3*d{Oj`_nENfx04_LR0$(eZKu8-m~-VsN+ynWU%TsA}&Gan)2)bCp2?2%w5I zG$ni6XsDz0VJcGEI-06>5QvnPkGmRF%MK0&tEsqw5V{T+VA$?NM+`|t3S{I2GV;Kp zbZzwEItXoD9~T=(LnC(rfea?d zgM|9v)ScW--E~aV4N$7;u231IxIWZT3$5;mGH`~-I2$2Mks8t%j3Gz`2Qh$ni<{a? zNb2FiI2~88BbcD3tK%(3k~H?RSCiDosycc50E3nWYz_bx7~m`+Y3zW|fFfKS?OdT+ z_RdbuNJkfaJou08(L`&@xa&%LYmrP0rJ<4-Cqo#9;0Z;*y)})Y#sIeYz!7@tdQc?P zMxSi#1i^w$%n(xQwwi7>Di|FZPjxAr2Nd8Drl+kX?(Xew=xqx|BF!XJ)E%9naI_A> zPDjGc3}=8faxil=&~wMaz<7*-nF$P~f=A;W+%ZTq8*L3~cUu*LJFrM4@EU(?h`KDo z-JXOp)B!m3a3q1%B&9GYh=GSK!PG#~#t;VpqlSwK+5`qN0_lo_kZ$Up5*U*|@TiZ( zVQpmeZFF5gsy5Ev-VUAswmYj6wY5FG;P5}@S^zMepk8n( z4IE4wO#&8xs-}}90wv+8E$)cJxU1Sb;tlm=i5^%9u|FbJfxEhkxrh-AjWz9Mv1qbA zNJiYw0}a*?w~-XraMglnc>~tT%-s$wWkY~?0Vi=6xVx5(y^f=ngA7#5$>k68ww^LL zm=ng;Q`$>b3rhf&hXxj8C+-ME84I1B#aDq)Yr$lqm85y#!e__ zyp)%$=N}rt+^Raric6~7fb^a9fd>t+te2CIk%tS?K?V%0At=_?7$s|KuLaQ30GL-b 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�vd3O>Wd5tuLh3!7bLrvMNomgepxhu;3 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 V{Up_8uUh~B002ovPDHLkV1g<-f6D*> literal 0 HcmV?d00001 From 0f857c5251e4e603c478a7896dc27e321754fd91 Mon Sep 17 00:00:00 2001 From: Maciej Obuchowski Date: Thu, 17 Nov 2022 15:32:36 +0100 Subject: [PATCH 11/11] delete: add endpoint for deleting namespaces (#2244) * delete: child jobs are now properly deleted Signed-off-by: Maciej Obuchowski * delete: add delete namespace endpoint Signed-off-by: Maciej Obuchowski Signed-off-by: Maciej Obuchowski --- .github/workflows/headerchecker.yml | 2 +- CHANGELOG.md | 9 + .../main/java/marquez/api/JobResource.java | 3 +- .../java/marquez/api/NamespaceResource.java | 18 ++ api/src/main/java/marquez/db/Columns.java | 9 + api/src/main/java/marquez/db/DatasetDao.java | 23 ++- api/src/main/java/marquez/db/JobDao.java | 10 ++ .../main/java/marquez/db/NamespaceDao.java | 81 +++++---- .../marquez/db/mappers/NamespaceMapper.java | 4 +- .../db/mappers/NamespaceRowMapper.java | 4 +- .../java/marquez/db/models/NamespaceRow.java | 1 + .../marquez/service/models/Namespace.java | 1 + .../migration/V53__add_hidden_namespace.sql | 1 + .../java/marquez/DatasetIntegrationTest.java | 162 ++++++++++++------ .../marquez/NamespaceIntegrationTest.java | 16 ++ .../marquez/OpenLineageIntegrationTest.java | 73 ++++++-- .../test/java/marquez/PostgresContainer.java | 2 +- api/src/test/java/marquez/db/ColumnsTest.java | 18 ++ .../test/java/marquez/db/DatasetDaoTest.java | 30 ++++ .../V44_2__BackfillAirflowParentRunsTest.java | 108 ------------ .../V44_3_BackfillJobsWithParentsTest.java | 95 ---------- .../marquez/db/models/DbModelGenerator.java | 7 +- .../java/marquez/client/MarquezClient.java | 4 + .../java/marquez/client/models/Namespace.java | 6 +- .../marquez/client/MarquezClientTest.java | 3 +- .../java/marquez/client/MarquezHttpTest.java | 3 +- .../marquez/client/models/JsonGenerator.java | 1 + .../marquez/client/models/ModelGenerator.java | 2 +- codecov.yml | 7 + web/src/store/reducers/namespaces.ts | 2 +- web/src/types/api.ts | 1 + 31 files changed, 391 insertions(+), 315 deletions(-) create mode 100644 api/src/main/resources/marquez/db/migration/V53__add_hidden_namespace.sql delete mode 100644 api/src/test/java/marquez/db/migrations/V44_2__BackfillAirflowParentRunsTest.java delete mode 100644 api/src/test/java/marquez/db/migrations/V44_3_BackfillJobsWithParentsTest.java diff --git a/.github/workflows/headerchecker.yml b/.github/workflows/headerchecker.yml index db0c3a53b4..02986dd8b7 100644 --- a/.github/workflows/headerchecker.yml +++ b/.github/workflows/headerchecker.yml @@ -31,7 +31,7 @@ jobs: - name: Check for headers run: | ok=1 - readarray -t files <<<"$(jq -r '.[]' <<<'${{ steps.files.outputs.all }}')" + readarray -t files <<<"$(jq -r '.[]' <<<'${{ steps.files.outputs.added_modified }}')" for file in ${files[@]}; do if [[ ($file == *".java") ]]; then if ! grep -q Copyright "$file"; then diff --git a/CHANGELOG.md b/CHANGELOG.md index f75aed75fd..0b5de1de05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased](https://github.com/MarquezProject/marquez/compare/0.27.0...HEAD) +### Added +* Add possibility to soft-delete namespaces [`#2244`](https://github.com/MarquezProject/marquez/pull/2244) [@mobuchowski](https://github.com/mobuchowski) + *Adds the ability to "hide" inactive namespaces. The namespaces are being undeleted when relevant OL event is received.* + +### Fixed +* Fix bug where job isn't properly deleted [`#2244`](https://github.com/MarquezProject/marquez/pull/2244) [@mobuchowski](https://github.com/mobuchowski) + *It wasn't possible to delete jobs created from events that had `ParentRunFacet`. Now it's possible.* + + ## [0.27.0](https://github.com/MarquezProject/marquez/compare/0.26.0...0.27.0) - 2022-10-24 ### Added diff --git a/api/src/main/java/marquez/api/JobResource.java b/api/src/main/java/marquez/api/JobResource.java index 199c4ac2e3..4230a26342 100644 --- a/api/src/main/java/marquez/api/JobResource.java +++ b/api/src/main/java/marquez/api/JobResource.java @@ -176,7 +176,8 @@ public Response delete( .findJobByName(namespaceName.getValue(), jobName.getValue()) .orElseThrow(() -> new JobNotFoundException(jobName)); - jobService.delete(namespaceName.getValue(), jobName.getValue()); + // Should be simple name from `jobs_fqn`. + jobService.delete(namespaceName.getValue(), job.getSimpleName()); return Response.ok(job).build(); } diff --git a/api/src/main/java/marquez/api/NamespaceResource.java b/api/src/main/java/marquez/api/NamespaceResource.java index bb81c4e5ec..4f14ef4f57 100644 --- a/api/src/main/java/marquez/api/NamespaceResource.java +++ b/api/src/main/java/marquez/api/NamespaceResource.java @@ -15,6 +15,7 @@ import javax.validation.Valid; import javax.validation.constraints.Min; import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -77,6 +78,23 @@ public Response list( return Response.ok(new Namespaces(namespaces)).build(); } + @Timed + @ResponseMetered + @ExceptionMetered + @DELETE + @Path("/namespaces/{namespace}") + @Produces(APPLICATION_JSON) + public Response delete(@PathParam("namespace") NamespaceName name) { + final Namespace namespace = + namespaceService + .findBy(name.getValue()) + .orElseThrow(() -> new NamespaceNotFoundException(name)); + datasetService.deleteByNamespaceName(namespace.getName().getValue()); + jobService.deleteByNamespaceName(namespace.getName().getValue()); + namespaceService.delete(namespace.getName().getValue()); + return Response.ok(namespace).build(); + } + @Value static class Namespaces { @NonNull diff --git a/api/src/main/java/marquez/db/Columns.java b/api/src/main/java/marquez/db/Columns.java index 73d95f10a9..21678a2a55 100644 --- a/api/src/main/java/marquez/db/Columns.java +++ b/api/src/main/java/marquez/db/Columns.java @@ -55,6 +55,7 @@ private Columns() {} public static final String DATASET_NAME = "dataset_name"; public static final String FACETS = "facets"; public static final String TAGS = "tags"; + public static final String IS_HIDDEN = "is_hidden"; /* NAMESPACE ROW COLUMNS */ public static final String CURRENT_OWNER_NAME = "current_owner_name"; @@ -197,6 +198,14 @@ public static boolean booleanOrDefault( return results.getBoolean(column); } + public static boolean booleanOrThrow(final ResultSet results, final String column) + throws SQLException { + if (results.getObject(column) == null) { + throw new IllegalArgumentException(); + } + return results.getBoolean(column); + } + public static int intOrThrow(final ResultSet results, final String column) throws SQLException { if (results.getObject(column) == null) { throw new IllegalArgumentException(); diff --git a/api/src/main/java/marquez/db/DatasetDao.java b/api/src/main/java/marquez/db/DatasetDao.java index 64f7faee25..5cc80392f9 100644 --- a/api/src/main/java/marquez/db/DatasetDao.java +++ b/api/src/main/java/marquez/db/DatasetDao.java @@ -292,15 +292,24 @@ DatasetRow upsert( String name, String physicalName); + @SqlUpdate( + """ + UPDATE datasets d + SET is_hidden = true + FROM namespaces n + WHERE n.uuid=d.namespace_uuid + AND n.name=:namespaceName + """) + void deleteByNamespaceName(String namespaceName); + @SqlQuery( """ - UPDATE datasets - SET is_hidden = true - FROM dataset_symlinks, namespaces - WHERE dataset_symlinks.dataset_uuid = datasets.uuid - AND namespaces.uuid = dataset_symlinks.namespace_uuid - AND namespaces.name=:namespaceName AND dataset_symlinks.name=:name - RETURNING * + UPDATE datasets d + SET is_hidden = true + FROM namespaces n + WHERE n.uuid = d.namespace_uuid + AND n.name=:namespaceName AND d.name=:name + RETURNING * """) Optional delete(String namespaceName, String name); diff --git a/api/src/main/java/marquez/db/JobDao.java b/api/src/main/java/marquez/db/JobDao.java index 1a5ea2ad64..6805673d66 100644 --- a/api/src/main/java/marquez/db/JobDao.java +++ b/api/src/main/java/marquez/db/JobDao.java @@ -88,6 +88,16 @@ SELECT run_uuid, JSON_AGG(e.facets) AS facets """) void delete(String namespaceName, String name); + @SqlUpdate( + """ + UPDATE jobs + SET is_hidden = true + FROM namespaces n + WHERE jobs.namespace_uuid = n.uuid + AND n.name = :namespaceName + """) + void deleteByNamespaceName(String namespaceName); + default Optional findWithRun(String namespaceName, String jobName) { Optional job = findJobByName(namespaceName, jobName); job.ifPresent( diff --git a/api/src/main/java/marquez/db/NamespaceDao.java b/api/src/main/java/marquez/db/NamespaceDao.java index a7f36e2336..03521cce51 100644 --- a/api/src/main/java/marquez/db/NamespaceDao.java +++ b/api/src/main/java/marquez/db/NamespaceDao.java @@ -78,10 +78,20 @@ default Namespace upsertNamespaceMeta( @SqlQuery("SELECT * FROM namespaces ORDER BY name LIMIT :limit OFFSET :offset") List findAll(int limit, int offset); + @SqlQuery("UPDATE namespaces SET is_hidden=false WHERE name = :name RETURNING *") + NamespaceRow undelete(String name); + + @SqlUpdate("UPDATE namespaces SET is_hidden=true WHERE name = :name") + void delete(String name); + default NamespaceRow upsertNamespaceRow( UUID uuid, Instant now, String name, String currentOwnerName) { doUpsertNamespaceRow(uuid, now, name, currentOwnerName); - return findNamespaceByName(name).orElseThrow(); + NamespaceRow namespaceRow = findNamespaceByName(name).orElseThrow(); + if (namespaceRow.getIsHidden()) { + namespaceRow = undelete(namespaceRow.getName()); + } + return namespaceRow; } /** @@ -99,40 +109,47 @@ default NamespaceRow upsertNamespaceRow( * @param currentOwnerName */ @SqlUpdate( - "INSERT INTO namespaces ( " - + "uuid, " - + "created_at, " - + "updated_at, " - + "name, " - + "current_owner_name " - + ") VALUES (" - + ":uuid, " - + ":now, " - + ":now, " - + ":name, " - + ":currentOwnerName) " - + "ON CONFLICT(name) DO NOTHING") + """ + INSERT INTO namespaces ( + uuid, + created_at, + updated_at, + name, + current_owner_name + ) VALUES ( + :uuid, + :now, + :now, + :name, + :currentOwnerName) + ON CONFLICT(name) DO NOTHING + """) void doUpsertNamespaceRow(UUID uuid, Instant now, String name, String currentOwnerName); @SqlQuery( - "INSERT INTO namespaces ( " - + "uuid, " - + "created_at, " - + "updated_at, " - + "name, " - + "current_owner_name, " - + "description " - + ") VALUES (" - + ":uuid, " - + ":now, " - + ":now, " - + ":name, " - + ":currentOwnerName, " - + ":description " - + ") ON CONFLICT(name) DO " - + "UPDATE SET " - + "updated_at = EXCLUDED.updated_at " - + "RETURNING *") + """ + INSERT INTO namespaces ( + uuid, + created_at, + updated_at, + name, + current_owner_name, + description, + is_hidden + ) VALUES ( + :uuid, + :now, + :now, + :name, + :currentOwnerName, + :description, + false + ) ON CONFLICT(name) DO + UPDATE SET + updated_at = EXCLUDED.updated_at, + is_hidden = false + RETURNING * + """) NamespaceRow upsertNamespaceRow( UUID uuid, Instant now, String name, String currentOwnerName, String description); diff --git a/api/src/main/java/marquez/db/mappers/NamespaceMapper.java b/api/src/main/java/marquez/db/mappers/NamespaceMapper.java index eb50079a73..4d51d9fd7f 100644 --- a/api/src/main/java/marquez/db/mappers/NamespaceMapper.java +++ b/api/src/main/java/marquez/db/mappers/NamespaceMapper.java @@ -5,6 +5,7 @@ package marquez.db.mappers; +import static marquez.db.Columns.booleanOrThrow; import static marquez.db.Columns.stringOrNull; import static marquez.db.Columns.stringOrThrow; import static marquez.db.Columns.timestampOrThrow; @@ -28,6 +29,7 @@ public Namespace map(@NonNull ResultSet results, @NonNull StatementContext conte timestampOrThrow(results, Columns.CREATED_AT), timestampOrThrow(results, Columns.UPDATED_AT), OwnerName.of(stringOrThrow(results, Columns.CURRENT_OWNER_NAME)), - stringOrNull(results, Columns.DESCRIPTION)); + stringOrNull(results, Columns.DESCRIPTION), + booleanOrThrow(results, Columns.IS_HIDDEN)); } } diff --git a/api/src/main/java/marquez/db/mappers/NamespaceRowMapper.java b/api/src/main/java/marquez/db/mappers/NamespaceRowMapper.java index 27541dd8ff..d3799a19e1 100644 --- a/api/src/main/java/marquez/db/mappers/NamespaceRowMapper.java +++ b/api/src/main/java/marquez/db/mappers/NamespaceRowMapper.java @@ -5,6 +5,7 @@ package marquez.db.mappers; +import static marquez.db.Columns.booleanOrThrow; import static marquez.db.Columns.stringOrNull; import static marquez.db.Columns.stringOrThrow; import static marquez.db.Columns.timestampOrThrow; @@ -28,6 +29,7 @@ public NamespaceRow map(@NonNull ResultSet results, @NonNull StatementContext co timestampOrThrow(results, Columns.UPDATED_AT), stringOrThrow(results, Columns.NAME), stringOrNull(results, Columns.DESCRIPTION), - stringOrThrow(results, Columns.CURRENT_OWNER_NAME)); + stringOrThrow(results, Columns.CURRENT_OWNER_NAME), + booleanOrThrow(results, Columns.IS_HIDDEN)); } } diff --git a/api/src/main/java/marquez/db/models/NamespaceRow.java b/api/src/main/java/marquez/db/models/NamespaceRow.java index de032cc382..58aa7bc17d 100644 --- a/api/src/main/java/marquez/db/models/NamespaceRow.java +++ b/api/src/main/java/marquez/db/models/NamespaceRow.java @@ -20,6 +20,7 @@ public class NamespaceRow { @NonNull String name; @Nullable String description; @NonNull String currentOwnerName; + @NonNull Boolean isHidden; public Optional getDescription() { return Optional.ofNullable(description); diff --git a/api/src/main/java/marquez/service/models/Namespace.java b/api/src/main/java/marquez/service/models/Namespace.java index d5a876f496..b51d359e4c 100644 --- a/api/src/main/java/marquez/service/models/Namespace.java +++ b/api/src/main/java/marquez/service/models/Namespace.java @@ -20,6 +20,7 @@ public class Namespace { @NonNull Instant updatedAt; @NonNull OwnerName ownerName; @Nullable String description; + @NonNull Boolean isHidden; public Optional getDescription() { return Optional.ofNullable(description); diff --git a/api/src/main/resources/marquez/db/migration/V53__add_hidden_namespace.sql b/api/src/main/resources/marquez/db/migration/V53__add_hidden_namespace.sql new file mode 100644 index 0000000000..1249955cf9 --- /dev/null +++ b/api/src/main/resources/marquez/db/migration/V53__add_hidden_namespace.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces ADD COLUMN is_hidden BOOLEAN DEFAULT FALSE; diff --git a/api/src/test/java/marquez/DatasetIntegrationTest.java b/api/src/test/java/marquez/DatasetIntegrationTest.java index 718428285a..d0e31e5f8a 100644 --- a/api/src/test/java/marquez/DatasetIntegrationTest.java +++ b/api/src/test/java/marquez/DatasetIntegrationTest.java @@ -30,7 +30,9 @@ import marquez.client.models.DatasetId; import marquez.client.models.DatasetVersion; import marquez.client.models.DbTableMeta; +import marquez.client.models.Job; import marquez.client.models.JobMeta; +import marquez.client.models.Namespace; import marquez.client.models.Run; import marquez.client.models.RunMeta; import marquez.client.models.StreamVersion; @@ -139,15 +141,7 @@ public void testApp_getTableVersions() { .build())) .build(); - final CompletableFuture resp = - this.sendLineage(Utils.toJson(lineageEvent)) - .thenApply(HttpResponse::statusCode) - .whenComplete( - (val, error) -> { - if (error != null) { - Assertions.fail("Could not complete request"); - } - }); + final CompletableFuture resp = sendEvent(lineageEvent); assertThat(resp.join()).isEqualTo(201); datasetFacets.setAdditional(inputFacets); @@ -170,15 +164,7 @@ public void testApp_getTableVersions() { .outputs(Collections.emptyList()) .build(); - final CompletableFuture readResp = - this.sendLineage(Utils.toJson(readEvent)) - .thenApply(HttpResponse::statusCode) - .whenComplete( - (val, error) -> { - if (error != null) { - Assertions.fail("Could not complete request"); - } - }); + final CompletableFuture readResp = sendEvent(readEvent); assertThat(readResp.join()).isEqualTo(201); // update dataset facet to include input and output facets @@ -389,17 +375,7 @@ public void testApp_doesNotShowDeletedDataset() throws IOException { Collections.emptyList(), "the_producer"); - final CompletableFuture resp = - this.sendLineage(Utils.toJson(event)) - .thenApply(HttpResponse::statusCode) - .whenComplete( - (val, error) -> { - if (error != null) { - Assertions.fail("Could not complete request"); - } - }); - - // Ensure the event was correctly rejected and a proper response code returned. + final CompletableFuture resp = sendEvent(event); assertThat(resp.join()).isEqualTo(201); client.deleteDataset(namespace, name); @@ -422,33 +398,14 @@ public void testApp_showsDeletedDatasetAfterReceivingNewVersion() throws IOExcep Collections.emptyList(), "the_producer"); - CompletableFuture resp = - this.sendLineage(Utils.toJson(event)) - .thenApply(HttpResponse::statusCode) - .whenComplete( - (val, error) -> { - if (error != null) { - Assertions.fail("Could not complete request"); - } - }); - - // Ensure the event was correctly rejected and a proper response code returned. + CompletableFuture resp = sendEvent(event); assertThat(resp.join()).isEqualTo(201); client.deleteDataset(namespace, name); List datasets = client.listDatasets(namespace); assertThat(datasets).hasSize(0); - resp = - this.sendLineage(Utils.toJson(event)) - .thenApply(HttpResponse::statusCode) - .whenComplete( - (val, error) -> { - if (error != null) { - Assertions.fail("Could not complete request"); - } - }); - + resp = sendEvent(event); assertThat(resp.join()).isEqualTo(201); datasets = client.listDatasets(namespace); @@ -495,4 +452,109 @@ public void testApp_getDatasetContainsColumnLineage() { assertThat(columnLineage).hasSize(1); assertThat(columnLineage.get(0).getInputFields()).hasSize(2); } + + @Test + public void testApp_doesNotShowDeletedDatasetAfterDeleteNamespace() throws IOException { + String namespace = "namespace"; + String name = "table"; + LineageEvent event = + new LineageEvent( + "COMPLETE", + Instant.now().atZone(ZoneId.systemDefault()), + new LineageEvent.Run(UUID.randomUUID().toString(), null), + new LineageEvent.Job("namespace", "job_name", null), + List.of(new LineageEvent.Dataset(namespace, name, LineageTestUtils.newDatasetFacet())), + Collections.emptyList(), + "the_producer"); + + final CompletableFuture resp = sendEvent(event); + assertThat(resp.join()).isEqualTo(201); + + client.deleteNamespace(namespace); + + List datasets = client.listDatasets(namespace); + assertThat(datasets).hasSize(0); + } + + @Test + public void testApp_doesNotShowDeletedDatasetAfterUndeleteNamespace() throws IOException { + String namespaceName = "namespace"; + String name = "table"; + + LineageEvent firstEvent = + new LineageEvent( + "COMPLETE", + Instant.now().atZone(ZoneId.systemDefault()), + new LineageEvent.Run(UUID.randomUUID().toString(), null), + new LineageEvent.Job(namespaceName, "job_name", null), + List.of( + new LineageEvent.Dataset(namespaceName, name, LineageTestUtils.newDatasetFacet())), + Collections.emptyList(), + "the_producer"); + + LineageEvent secondEvent = + new LineageEvent( + "COMPLETE", + Instant.now().atZone(ZoneId.systemDefault()), + new LineageEvent.Run(UUID.randomUUID().toString(), null), + new LineageEvent.Job(namespaceName, "second_job_name", null), + List.of( + new LineageEvent.Dataset( + namespaceName, name + "2", LineageTestUtils.newDatasetFacet())), + Collections.emptyList(), + "the_producer"); + + CompletableFuture resp = sendEvent(firstEvent); + assertThat(resp.join()).isEqualTo(201); + + resp = sendEvent(secondEvent); + assertThat(resp.join()).isEqualTo(201); + + List datasets = client.listDatasets(namespaceName); + assertThat(datasets).hasSize(2); + + client.deleteNamespace(namespaceName); + + List namespaces = client.listNamespaces(); + assertThat(namespaces) + .anySatisfy( + namespace -> { + assertThat(namespace.getIsHidden()).isTrue(); + assertThat(namespace.getName()).isEqualTo(namespaceName); + }); + + datasets = client.listDatasets(namespaceName); + assertThat(datasets).hasSize(0); + + List jobs = client.listJobs(namespaceName); + assertThat(jobs).hasSize(0); + + LineageEvent eventThatWillUndeleteNamespace = + new LineageEvent( + "COMPLETE", + Instant.now().atZone(ZoneId.systemDefault()), + new LineageEvent.Run(UUID.randomUUID().toString(), null), + new LineageEvent.Job(namespaceName, "job_name", null), + List.of( + new LineageEvent.Dataset(namespaceName, name, LineageTestUtils.newDatasetFacet())), + Collections.emptyList(), + "the_producer"); + + resp = sendEvent(eventThatWillUndeleteNamespace); + assertThat(resp.join()).isEqualTo(201); + + namespaces = client.listNamespaces(); + assertThat(namespaces) + .anySatisfy( + namespace -> { + assertThat(namespace.getIsHidden()).isFalse(); + assertThat(namespace.getName()).isEqualTo(namespaceName); + }); + + datasets = client.listDatasets(namespaceName); + assertThat(datasets).hasSize(1); + + jobs = client.listJobs(namespaceName); + assertThat(jobs).hasSize(1); + } } diff --git a/api/src/test/java/marquez/NamespaceIntegrationTest.java b/api/src/test/java/marquez/NamespaceIntegrationTest.java index 4f46dc1d8a..08d74d83e9 100644 --- a/api/src/test/java/marquez/NamespaceIntegrationTest.java +++ b/api/src/test/java/marquez/NamespaceIntegrationTest.java @@ -66,5 +66,21 @@ public void testApp_getNamespace() { assertThat(namespace.getUpdatedAt()).isNotNull(); assertThat(namespace.getCreatedAt()).isNotNull(); assertThat(namespace.getDescription().get()).isEqualTo(NAMESPACE_DESCRIPTION); + assertThat(namespace.getIsHidden()).isFalse(); + } + + @Test + public void testApp_deleteNamespace() { + NamespaceMeta namespaceMeta = + NamespaceMeta.builder().ownerName(OWNER_NAME).description(NAMESPACE_DESCRIPTION).build(); + client.createNamespace(NAMESPACE_NAME, namespaceMeta); + Namespace namespace = client.getNamespace(NAMESPACE_NAME); + assertThat(namespace.getName()).isEqualTo(NAMESPACE_NAME); + assertThat(namespace.getIsHidden()).isFalse(); + + client.deleteNamespace(NAMESPACE_NAME); + namespace = client.getNamespace(NAMESPACE_NAME); + assertThat(namespace.getName()).isEqualTo(NAMESPACE_NAME); + assertThat(namespace.getIsHidden()).isTrue(); } } diff --git a/api/src/test/java/marquez/OpenLineageIntegrationTest.java b/api/src/test/java/marquez/OpenLineageIntegrationTest.java index bd5cc6f0c2..540399d184 100644 --- a/api/src/test/java/marquez/OpenLineageIntegrationTest.java +++ b/api/src/test/java/marquez/OpenLineageIntegrationTest.java @@ -5,6 +5,8 @@ package marquez; +import static marquez.db.LineageTestUtils.PRODUCER_URL; +import static marquez.db.LineageTestUtils.SCHEMA_URL; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -105,17 +107,7 @@ public void testSendOpenLineageBadArgument() throws IOException { Collections.emptyList(), "the_producer"); - final CompletableFuture resp = - this.sendLineage(Utils.toJson(event)) - .thenApply(HttpResponse::statusCode) - .whenComplete( - (val, error) -> { - if (error != null) { - Assertions.fail("Could not complete request"); - } - }); - - // Ensure the event was correctly rejected and a proper response code returned. + final CompletableFuture resp = sendEvent(event); assertThat(resp.join()).isEqualTo(400); } @@ -890,6 +882,65 @@ public void testFindEventBeforeAfterTime() { .isEqualTo(mapper.valueToTree(rawEvents.get(0))); } + @Test + public void testSendAndDeleteParentRunRelationshipFacet() { + marquez.service.models.LineageEvent.Run run = + new marquez.service.models.LineageEvent.Run( + UUID.randomUUID().toString(), + marquez.service.models.LineageEvent.RunFacet.builder() + .parent( + marquez.service.models.LineageEvent.ParentRunFacet.builder() + .run( + marquez.service.models.LineageEvent.RunLink.builder() + .runId(UUID.randomUUID().toString()) + .build()) + .job( + marquez.service.models.LineageEvent.JobLink.builder() + .name("parent") + .namespace(NAMESPACE_NAME) + .build()) + ._producer(PRODUCER_URL) + ._schemaURL(SCHEMA_URL) + .build()) + .build()); + marquez.service.models.LineageEvent.Job job = + marquez.service.models.LineageEvent.Job.builder() + .namespace(NAMESPACE_NAME) + .name(JOB_NAME) + .build(); + + marquez.service.models.LineageEvent event = + marquez.service.models.LineageEvent.builder() + .eventType("COMPLETE") + .eventTime(ZonedDateTime.of(2021, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC"))) + .producer(PRODUCER_URL.toString()) + .run(run) + .job(job) + .inputs(Collections.emptyList()) + .outputs(Collections.emptyList()) + .build(); + + CompletableFuture resp = sendEvent(event); + assertThat(resp.join()).isEqualTo(201); + + List jobs = client.listJobs(NAMESPACE_NAME); + + String marquezJobName = String.format("parent.%s", JOB_NAME); + + assertThat(jobs.size()).isEqualTo(2); + assertThat(jobs) + .anySatisfy(returnedJob -> assertThat(returnedJob.getName()).isEqualTo("parent")) + .anySatisfy(returnedJob -> assertThat(returnedJob.getName()).isEqualTo(marquezJobName)); + + client.deleteJob(NAMESPACE_NAME, marquezJobName); + + jobs = client.listJobs(NAMESPACE_NAME); + assertThat(jobs.size()).isEqualTo(1); + assertThat(jobs) + .anySatisfy(returnedJob -> assertThat(returnedJob.getName()).isEqualTo("parent")) + .noneSatisfy(returnedJob -> assertThat(returnedJob.getName()).isEqualTo(marquezJobName)); + } + private CompletableFuture sendEvent(marquez.service.models.LineageEvent event) { return this.sendLineage(Utils.toJson(event)) .thenApply(HttpResponse::statusCode) diff --git a/api/src/test/java/marquez/PostgresContainer.java b/api/src/test/java/marquez/PostgresContainer.java index a74b4893a8..1d536694ce 100644 --- a/api/src/test/java/marquez/PostgresContainer.java +++ b/api/src/test/java/marquez/PostgresContainer.java @@ -11,7 +11,7 @@ import org.testcontainers.utility.DockerImageName; public final class PostgresContainer extends PostgreSQLContainer { - private static final DockerImageName POSTGRES = DockerImageName.parse("postgres:11.8"); + private static final DockerImageName POSTGRES = DockerImageName.parse("postgres:12.12"); private static final int JDBC = 5; private static final Map containers = new HashMap<>(); diff --git a/api/src/test/java/marquez/db/ColumnsTest.java b/api/src/test/java/marquez/db/ColumnsTest.java index 3798e03710..ae9d6ad4b1 100644 --- a/api/src/test/java/marquez/db/ColumnsTest.java +++ b/api/src/test/java/marquez/db/ColumnsTest.java @@ -307,4 +307,22 @@ public void testBooleanOrDefaultWhenNoValue() throws SQLException { final boolean actual = Columns.booleanOrDefault(results, column, true); assertThat(actual).isTrue(); } + + @Test + public void testBooleanOrThrow() throws SQLException { + final String column = "is_deleted"; + when(results.getObject(column)).thenReturn(true); + when(results.getBoolean(column)).thenReturn(true); + + final boolean actual = Columns.booleanOrThrow(results, column); + assertThat(actual).isTrue(); + } + + @Test + public void testBooleanOrThrowNoValue() throws SQLException { + final String column = "is_deleted"; + when(results.getObject(column)).thenReturn(null); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> Columns.booleanOrThrow(results, column)); + } } diff --git a/api/src/test/java/marquez/db/DatasetDaoTest.java b/api/src/test/java/marquez/db/DatasetDaoTest.java index 15fa888c4e..2d0e06f4a4 100644 --- a/api/src/test/java/marquez/db/DatasetDaoTest.java +++ b/api/src/test/java/marquez/db/DatasetDaoTest.java @@ -394,6 +394,36 @@ public void testGetDatasets() { "http://test.schema/")); } + @Test + public void testDeleteDatasetByNamespaceDoesNotReturnFromDeletedNamespace() { + createLineageRow( + openLineageDao, + "writeJob", + "COMPLETE", + jobFacet, + Collections.emptyList(), + Collections.singletonList(newCommonDataset(Collections.emptyMap()))); + + createLineageRow( + openLineageDao, + "writeJob2", + "COMPLETE", + jobFacet, + Collections.emptyList(), + Collections.singletonList( + new Dataset( + NAMESPACE, + DATASET, + LineageEvent.DatasetFacets.builder() + .lifecycleStateChange( + new LineageEvent.LifecycleStateChangeFacet( + PRODUCER_URL, SCHEMA_URL, "DROP")) + .build()))); + + datasetDao.deleteByNamespaceName(NAMESPACE); + assertThat(datasetDao.findDatasetByName(NAMESPACE, DATASET)).isEmpty(); + } + @Test public void testGetSpecificDatasetReturnsDatasetIfDeleted() { createLineageRow( diff --git a/api/src/test/java/marquez/db/migrations/V44_2__BackfillAirflowParentRunsTest.java b/api/src/test/java/marquez/db/migrations/V44_2__BackfillAirflowParentRunsTest.java deleted file mode 100644 index fbffc59094..0000000000 --- a/api/src/test/java/marquez/db/migrations/V44_2__BackfillAirflowParentRunsTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2018-2022 contributors to the Marquez project - * SPDX-License-Identifier: Apache-2.0 - */ - -package marquez.db.migrations; - -import static marquez.db.LineageTestUtils.NAMESPACE; -import static org.assertj.core.api.Assertions.assertThat; - -import com.fasterxml.jackson.core.JsonProcessingException; -import java.sql.Connection; -import java.sql.SQLException; -import java.time.Instant; -import java.util.Optional; -import java.util.UUID; -import marquez.db.BackfillTestUtils; -import marquez.db.JobDao; -import marquez.db.NamespaceDao; -import marquez.db.OpenLineageDao; -import marquez.db.RunArgsDao; -import marquez.db.RunDao; -import marquez.db.models.NamespaceRow; -import marquez.jdbi.JdbiExternalPostgresExtension.FlywaySkipRepeatable; -import marquez.jdbi.JdbiExternalPostgresExtension.FlywayTarget; -import marquez.jdbi.MarquezJdbiExternalPostgresExtension; -import org.flywaydb.core.api.configuration.Configuration; -import org.flywaydb.core.api.migration.Context; -import org.jdbi.v3.core.Jdbi; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@ExtendWith(MarquezJdbiExternalPostgresExtension.class) -// fix the flyway migration up to v44 since we depend on the database structure as it exists at this -// point in time. The migration will only ever be applied on a database at this version. -@FlywayTarget("44") -// As of the time of this migration, there were no repeatable migrations, so ignore any that are -// added -@FlywaySkipRepeatable() -class V44_2__BackfillAirflowParentRunsTest { - - static Jdbi jdbi; - private static OpenLineageDao openLineageDao; - private static JobDao jobDao; - private static RunArgsDao runArgsDao; - private static RunDao runDao; - - @BeforeAll - public static void setUpOnce(Jdbi jdbi) { - V44_2__BackfillAirflowParentRunsTest.jdbi = jdbi; - openLineageDao = jdbi.onDemand(OpenLineageDao.class); - jobDao = jdbi.onDemand(JobDao.class); - runArgsDao = jdbi.onDemand(RunArgsDao.class); - runDao = jdbi.onDemand(RunDao.class); - } - - @Test - public void testMigrateAirflowTasks() throws SQLException, JsonProcessingException { - String dagName = "airflowDag"; - String task1Name = dagName + ".task1"; - NamespaceDao namespaceDao = jdbi.onDemand(NamespaceDao.class); - Instant now = Instant.now(); - NamespaceRow namespace = - namespaceDao.upsertNamespaceRow(UUID.randomUUID(), now, NAMESPACE, "me"); - - BackfillTestUtils.writeNewEvent( - jdbi, task1Name, now, namespace, "schedule:00:00:00", task1Name); - BackfillTestUtils.writeNewEvent( - jdbi, "airflowDag.task2", now, namespace, "schedule:00:00:00", task1Name); - - BackfillTestUtils.writeNewEvent(jdbi, "a_non_airflow_task", now, namespace, null, null); - - jdbi.useHandle( - handle -> { - try { - new V44_2__BackfillAirflowParentRuns() - .migrate( - new Context() { - @Override - public Configuration getConfiguration() { - return null; - } - - @Override - public Connection getConnection() { - return handle.getConnection(); - } - }); - } catch (Exception e) { - throw new AssertionError("Unable to execute migration", e); - } - }); - Optional jobNameResult = - jdbi.withHandle( - h -> - h.createQuery( - """ - SELECT name FROM jobs_view - WHERE namespace_name=:namespace AND simple_name=:jobName - """) - .bind("namespace", NAMESPACE) - .bind("jobName", dagName) - .mapTo(String.class) - .findFirst()); - assertThat(jobNameResult).isPresent(); - } -} diff --git a/api/src/test/java/marquez/db/migrations/V44_3_BackfillJobsWithParentsTest.java b/api/src/test/java/marquez/db/migrations/V44_3_BackfillJobsWithParentsTest.java deleted file mode 100644 index bf33c08cb7..0000000000 --- a/api/src/test/java/marquez/db/migrations/V44_3_BackfillJobsWithParentsTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2018-2022 contributors to the Marquez project - * SPDX-License-Identifier: Apache-2.0 - */ - -package marquez.db.migrations; - -import static marquez.db.BackfillTestUtils.writeNewEvent; -import static marquez.db.LineageTestUtils.NAMESPACE; -import static org.assertj.core.api.Assertions.assertThat; - -import com.fasterxml.jackson.core.JsonProcessingException; -import java.sql.Connection; -import java.sql.SQLException; -import java.time.Instant; -import java.util.Optional; -import java.util.UUID; -import marquez.db.NamespaceDao; -import marquez.db.OpenLineageDao; -import marquez.db.models.NamespaceRow; -import marquez.db.models.RunRow; -import marquez.jdbi.JdbiExternalPostgresExtension.FlywaySkipRepeatable; -import marquez.jdbi.JdbiExternalPostgresExtension.FlywayTarget; -import marquez.jdbi.MarquezJdbiExternalPostgresExtension; -import org.flywaydb.core.api.configuration.Configuration; -import org.flywaydb.core.api.migration.Context; -import org.jdbi.v3.core.Jdbi; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@ExtendWith(MarquezJdbiExternalPostgresExtension.class) -// fix the flyway migration up to v44 since we depend on the database structure as it exists at this -// point in time. The migration will only ever be applied on a database at this version. -@FlywayTarget("44") -// As of the time of this migration, there were no repeatable migrations, so ignore any that are -// added -@FlywaySkipRepeatable() -class V44_3_BackfillJobsWithParentsTest { - - static Jdbi jdbi; - private static OpenLineageDao openLineageDao; - - @BeforeAll - public static void setUpOnce(Jdbi jdbi) { - V44_3_BackfillJobsWithParentsTest.jdbi = jdbi; - openLineageDao = jdbi.onDemand(OpenLineageDao.class); - } - - @Test - public void testBackfill() throws SQLException, JsonProcessingException { - NamespaceDao namespaceDao = jdbi.onDemand(NamespaceDao.class); - Instant now = Instant.now(); - NamespaceRow namespace = - namespaceDao.upsertNamespaceRow(UUID.randomUUID(), now, NAMESPACE, "me"); - String parentName = "parentJob"; - RunRow parentRun = writeNewEvent(jdbi, parentName, now, namespace, null, null); - - String task1Name = "task1"; - writeNewEvent(jdbi, task1Name, now, namespace, parentRun.getUuid().toString(), parentName); - writeNewEvent(jdbi, "task2", now, namespace, parentRun.getUuid().toString(), parentName); - - jdbi.useHandle( - handle -> { - try { - Context context = - new Context() { - @Override - public Configuration getConfiguration() { - return null; - } - - @Override - public Connection getConnection() { - return handle.getConnection(); - } - }; - // apply migrations in order - new V44_1__UpdateRunsWithJobUUID().migrate(context); - new V44_3_BackfillJobsWithParents().migrate(context); - } catch (Exception e) { - throw new AssertionError("Unable to execute migration", e); - } - }); - - Optional jobName = - jdbi.withHandle( - h -> - h.createQuery("SELECT name FROM jobs_view WHERE simple_name=:jobName") - .bind("jobName", task1Name) - .mapTo(String.class) - .findFirst()); - assertThat(jobName).isPresent().get().isEqualTo(parentName + "." + task1Name); - } -} diff --git a/api/src/test/java/marquez/db/models/DbModelGenerator.java b/api/src/test/java/marquez/db/models/DbModelGenerator.java index 2b5de8320b..de2ea22774 100644 --- a/api/src/test/java/marquez/db/models/DbModelGenerator.java +++ b/api/src/test/java/marquez/db/models/DbModelGenerator.java @@ -26,7 +26,9 @@ private DbModelGenerator() {} /** Returns new {@link NamespaceRow} objects with a specified {@code limit}. */ public static List newNamespaceRows(int limit) { - return Stream.generate(() -> newNamespaceRow()).limit(limit).collect(toImmutableList()); + return Stream.generate(DbModelGenerator::newNamespaceRow) + .limit(limit) + .collect(toImmutableList()); } /** Returns a new {@link NamespaceRow} object. */ @@ -38,7 +40,8 @@ public static NamespaceRow newNamespaceRow() { now, newNamespaceName().getValue(), newDescription(), - newOwnerName().getValue()); + newOwnerName().getValue(), + false); } /** Returns a new {@code row} uuid. */ diff --git a/clients/java/src/main/java/marquez/client/MarquezClient.java b/clients/java/src/main/java/marquez/client/MarquezClient.java index c676c562eb..3972f868ac 100644 --- a/clients/java/src/main/java/marquez/client/MarquezClient.java +++ b/clients/java/src/main/java/marquez/client/MarquezClient.java @@ -230,6 +230,10 @@ public Dataset deleteDataset(@NonNull String namespaceName, @NonNull String data return Dataset.fromJson(bodyAsJson); } + public void deleteNamespace(@NonNull String namespaceName) { + http.delete(url.toNamespaceUrl(namespaceName)); + } + public DatasetVersion getDatasetVersion( @NonNull String namespaceName, @NonNull String datasetName, @NonNull String version) { final String bodyAsJson = diff --git a/clients/java/src/main/java/marquez/client/models/Namespace.java b/clients/java/src/main/java/marquez/client/models/Namespace.java index 9565ea5a59..37c7fa62fd 100644 --- a/clients/java/src/main/java/marquez/client/models/Namespace.java +++ b/clients/java/src/main/java/marquez/client/models/Namespace.java @@ -21,16 +21,20 @@ public final class Namespace extends NamespaceMeta { @Getter private final Instant createdAt; @Getter private final Instant updatedAt; + @Getter private final Boolean isHidden; + public Namespace( @NonNull final String name, @NonNull final Instant createdAt, @NonNull final Instant updatedAt, final String ownerName, - @Nullable final String description) { + @Nullable final String description, + @NonNull final Boolean isHidden) { super(ownerName, description); this.name = name; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.isHidden = isHidden; } public static Namespace fromJson(@NonNull final String json) { diff --git a/clients/java/src/test/java/marquez/client/MarquezClientTest.java b/clients/java/src/test/java/marquez/client/MarquezClientTest.java index 4c2a725d4c..dce0192e87 100644 --- a/clients/java/src/test/java/marquez/client/MarquezClientTest.java +++ b/clients/java/src/test/java/marquez/client/MarquezClientTest.java @@ -118,7 +118,8 @@ public class MarquezClientTest { private static final String OWNER_NAME = newOwnerName(); private static final String NAMESPACE_DESCRIPTION = newDescription(); private static final Namespace NAMESPACE = - new Namespace(NAMESPACE_NAME, CREATED_AT, UPDATED_AT, OWNER_NAME, NAMESPACE_DESCRIPTION); + new Namespace( + NAMESPACE_NAME, CREATED_AT, UPDATED_AT, OWNER_NAME, NAMESPACE_DESCRIPTION, false); // SOURCE private static final String SOURCE_TYPE = newSourceType(); diff --git a/clients/java/src/test/java/marquez/client/MarquezHttpTest.java b/clients/java/src/test/java/marquez/client/MarquezHttpTest.java index 4116b988b7..c562c02380 100644 --- a/clients/java/src/test/java/marquez/client/MarquezHttpTest.java +++ b/clients/java/src/test/java/marquez/client/MarquezHttpTest.java @@ -181,7 +181,8 @@ public void testPut() throws Exception { final String ownerName = newOwnerName(); final String description = newDescription(); - final Namespace namespace = new Namespace(namespaceName, now, now, ownerName, description); + final Namespace namespace = + new Namespace(namespaceName, now, now, ownerName, description, false); final String json = JsonGenerator.newJsonFor(namespace); final ByteArrayInputStream stream = new ByteArrayInputStream(json.getBytes(UTF_8)); when(httpEntity.getContent()).thenReturn(stream); diff --git a/clients/java/src/test/java/marquez/client/models/JsonGenerator.java b/clients/java/src/test/java/marquez/client/models/JsonGenerator.java index 26cb75def9..81e06b016c 100644 --- a/clients/java/src/test/java/marquez/client/models/JsonGenerator.java +++ b/clients/java/src/test/java/marquez/client/models/JsonGenerator.java @@ -38,6 +38,7 @@ public static String newJsonFor(final Namespace namespace) { .put("updatedAt", ISO_INSTANT.format(namespace.getUpdatedAt())) .put("ownerName", namespace.getOwnerName()) .put("description", namespace.getDescription().orElse(null)) + .put("isHidden", namespace.getIsHidden()) .toString(); } diff --git a/clients/java/src/test/java/marquez/client/models/ModelGenerator.java b/clients/java/src/test/java/marquez/client/models/ModelGenerator.java index bc28f4be08..af1f0341d9 100644 --- a/clients/java/src/test/java/marquez/client/models/ModelGenerator.java +++ b/clients/java/src/test/java/marquez/client/models/ModelGenerator.java @@ -38,7 +38,7 @@ public static List newNamespaces(final int limit) { public static Namespace newNamespace() { final Instant now = newTimestamp(); - return new Namespace(newNamespaceName(), now, now, newOwnerName(), newDescription()); + return new Namespace(newNamespaceName(), now, now, newOwnerName(), newDescription(), false); } public static SourceMeta newSourceMeta() { diff --git a/codecov.yml b/codecov.yml index a9aec2ad72..363a04d59a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,11 @@ codecov: branch: main + +coverage: status: patch: off + +ignore: + - "api/src/main/java/marquez/db/migrations/V44_1__UpdateRunsWithJobUUID.java" + - "api/src/main/java/marquez/db/migrations/V44_2__BackfillAirflowParentRuns.java" + - "api/src/main/java/marquez/db/migrations/V44_3_BackfillJobsWithParents.java" \ No newline at end of file diff --git a/web/src/store/reducers/namespaces.ts b/web/src/store/reducers/namespaces.ts index 607b554723..a0e0518eeb 100644 --- a/web/src/store/reducers/namespaces.ts +++ b/web/src/store/reducers/namespaces.ts @@ -21,7 +21,7 @@ export default ( switch (type) { case FETCH_NAMESPACES_SUCCESS: return { - result: payload.namespaces, + result: payload.namespaces.filter(namespace => !namespace.isHidden), selectedNamespace: window.localStorage.getItem('selectedNamespace') && action.payload.namespaces.find( diff --git a/web/src/types/api.ts b/web/src/types/api.ts index d54cd1c92c..320bf02fd2 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -20,6 +20,7 @@ export interface Namespace { updatedAt: string ownerName: string description: string + isHidden: boolean } export interface Datasets {