From 107cd6484a3d0ea3c65211cd8b90efbd1627f233 Mon Sep 17 00:00:00 2001 From: sagar-salvi-apptware <159135491+sagar-salvi-apptware@users.noreply.github.com> Date: Fri, 4 Oct 2024 03:14:21 +0530 Subject: [PATCH 01/25] fix(ingest/looker) : Handle DeserializeError to improve error reporting. (#11457) Co-authored-by: Harshal Sheth --- .../datahub/ingestion/source/looker/looker_common.py | 12 +++++++++++- .../datahub/ingestion/source/looker/looker_source.py | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/metadata-ingestion/src/datahub/ingestion/source/looker/looker_common.py b/metadata-ingestion/src/datahub/ingestion/source/looker/looker_common.py index 1f54767de5a68b..3cbb13375229b9 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/looker/looker_common.py +++ b/metadata-ingestion/src/datahub/ingestion/source/looker/looker_common.py @@ -21,6 +21,7 @@ ) from looker_sdk.error import SDKError +from looker_sdk.rtl.serialize import DeserializeError from looker_sdk.sdk.api40.models import ( LookmlModelExplore, LookmlModelExploreField, @@ -1131,7 +1132,16 @@ def from_api( # noqa: C901 logger.warning( f"Failed to extract explore {explore_name} from model {model}: {e}" ) - + except DeserializeError as e: + reporter.warning( + title="Failed to fetch explore from the Looker API", + message=( + "An error occurred while extracting the explore from the model. " + "Please check the explore and model configurations." + ), + context=f"Explore: {explore_name}, Model: {model}", + exc=e, + ) except AssertionError: reporter.report_warning( title="Unable to find Views", diff --git a/metadata-ingestion/src/datahub/ingestion/source/looker/looker_source.py b/metadata-ingestion/src/datahub/ingestion/source/looker/looker_source.py index e593e132dafd7e..f269ccf1cd98f8 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/looker/looker_source.py +++ b/metadata-ingestion/src/datahub/ingestion/source/looker/looker_source.py @@ -16,6 +16,7 @@ ) from looker_sdk.error import SDKError +from looker_sdk.rtl.serialize import DeserializeError from looker_sdk.sdk.api40.models import ( Dashboard, DashboardElement, @@ -1288,12 +1289,13 @@ def process_dashboard( dashboard_id=dashboard_id, fields=fields, ) - except SDKError: + except (SDKError, DeserializeError) as e: # A looker dashboard could be deleted in between the list and the get self.reporter.report_warning( - title="Error Loading Dashboard", + title="Failed to fetch dashboard from the Looker API", message="Error occurred while attempting to loading dashboard from Looker API. Skipping.", context=f"Dashboard ID: {dashboard_id}", + exc=e, ) return [], None, dashboard_id, start_time, datetime.datetime.now() From db490671113aec07a328403f188bc214c7ce2b51 Mon Sep 17 00:00:00 2001 From: david-leifker <114954101+david-leifker@users.noreply.github.com> Date: Thu, 3 Oct 2024 21:10:26 -0500 Subject: [PATCH 02/25] build(misc): misc build, config, version updates (#11527) --- .github/workflows/metadata-io.yml | 4 ++++ build.gradle | 2 +- datahub-frontend/play.gradle | 2 +- docker/build.gradle | 2 ++ .../java/datahub-client/scripts/check_jar.sh | 9 +++++---- .../spark-lineage-legacy/spark-smoke-test/smoke-gms.env | 2 ++ .../configuration/src/main/resources/application.yaml | 4 ++-- 7 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 metadata-integration/java/spark-lineage-legacy/spark-smoke-test/smoke-gms.env diff --git a/.github/workflows/metadata-io.yml b/.github/workflows/metadata-io.yml index e5e5f1dae5447d..7018b42949e892 100644 --- a/.github/workflows/metadata-io.yml +++ b/.github/workflows/metadata-io.yml @@ -8,6 +8,7 @@ on: - "li-utils/**" - "metadata-models/**" - "metadata-io/**" + - ".github/workflows/metadata-io.yml" pull_request: branches: - "**" @@ -16,6 +17,7 @@ on: - "li-utils/**" - "metadata-models/**" - "metadata-io/**" + - ".github/workflows/metadata-io.yml" release: types: [published] @@ -52,6 +54,8 @@ jobs: sudo apt-get remove 'dotnet-*' azure-cli || true sudo rm -rf /usr/local/lib/android/ || true sudo docker image prune -a -f || true + - name: Disk Check + run: df -h . && docker images - uses: acryldata/sane-checkout-action@v3 - name: Set up JDK 17 uses: actions/setup-java@v4 diff --git a/build.gradle b/build.gradle index 302b37281798fc..5857746a8fd919 100644 --- a/build.gradle +++ b/build.gradle @@ -391,7 +391,7 @@ subprojects { implementation externalDependency.annotationApi constraints { implementation("com.google.googlejavaformat:google-java-format:$googleJavaFormatVersion") - implementation('io.netty:netty-all:4.1.100.Final') + implementation('io.netty:netty-all:4.1.114.Final') implementation('org.apache.commons:commons-compress:1.26.0') implementation('org.apache.velocity:velocity-engine-core:2.3') implementation('org.hibernate:hibernate-validator:6.0.20.Final') diff --git a/datahub-frontend/play.gradle b/datahub-frontend/play.gradle index b14962e5900cd2..ff43e4a93a80f8 100644 --- a/datahub-frontend/play.gradle +++ b/datahub-frontend/play.gradle @@ -20,7 +20,7 @@ dependencies { play('com.nimbusds:nimbus-jose-jwt:8.18') play('com.typesafe.akka:akka-actor_2.12:2.6.20') play(externalDependency.jsonSmart) - play('io.netty:netty-all:4.1.86.Final') + play('io.netty:netty-all:4.1.114.Final') implementation(externalDependency.commonsText) { because("previous versions are vulnerable to CVE-2022-42889") } diff --git a/docker/build.gradle b/docker/build.gradle index 20608bd8578270..c09bf16d1d7242 100644 --- a/docker/build.gradle +++ b/docker/build.gradle @@ -108,6 +108,8 @@ dockerCompose { environment.put "ACTIONS_EXTRA_PACKAGES", 'acryl-datahub-actions[executor] acryl-datahub-actions' environment.put "ACTIONS_CONFIG", 'https://raw.githubusercontent.com/acryldata/datahub-actions/main/docker/config/executor.yaml' environment.put 'DATAHUB_TELEMETRY_ENABLED', 'false' // disabled when built locally + // disabled for spark-lineage smoke-test + environment.put 'DATAHUB_LOCAL_COMMON_ENV', "${rootProject.project(':metadata-integration:java:spark-lineage-legacy').projectDir}/spark-smoke-test/smoke-gms.env" useComposeFiles = ['profiles/docker-compose.yml'] projectName = 'datahub' diff --git a/metadata-integration/java/datahub-client/scripts/check_jar.sh b/metadata-integration/java/datahub-client/scripts/check_jar.sh index f76931428e3d6c..10299ec714d165 100755 --- a/metadata-integration/java/datahub-client/scripts/check_jar.sh +++ b/metadata-integration/java/datahub-client/scripts/check_jar.sh @@ -36,10 +36,11 @@ jar -tvf $jarFile |\ grep -v "darwin" |\ grep -v "MetadataChangeProposal.avsc" |\ grep -v "aix" |\ - grep -v "com/sun/" - grep -v "mozilla" - grep -v "VersionInfo.java" - grep -v "mime.types" + grep -v "com/sun/" |\ + grep -v "mozilla" |\ + grep -v "VersionInfo.java" |\ + grep -v "mime.types" |\ + grep -v "com/ibm/.*" if [ $? -ne 0 ]; then diff --git a/metadata-integration/java/spark-lineage-legacy/spark-smoke-test/smoke-gms.env b/metadata-integration/java/spark-lineage-legacy/spark-smoke-test/smoke-gms.env new file mode 100644 index 00000000000000..7b437a98089ce7 --- /dev/null +++ b/metadata-integration/java/spark-lineage-legacy/spark-smoke-test/smoke-gms.env @@ -0,0 +1,2 @@ +REST_API_AUTHORIZATION_ENABLED=false +METADATA_SERVICE_AUTH_ENABLED=false \ No newline at end of file diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml index 5e07bfc479e93c..aaeb8b53682826 100644 --- a/metadata-service/configuration/src/main/resources/application.yaml +++ b/metadata-service/configuration/src/main/resources/application.yaml @@ -176,8 +176,8 @@ cassandra: elasticsearch: host: ${ELASTICSEARCH_HOST:localhost} port: ${ELASTICSEARCH_PORT:9200} - threadCount: ${ELASTICSEARCH_THREAD_COUNT:1} - connectionRequestTimeout: ${ELASTICSEARCH_CONNECTION_REQUEST_TIMEOUT:0} + threadCount: ${ELASTICSEARCH_THREAD_COUNT:2} + connectionRequestTimeout: ${ELASTICSEARCH_CONNECTION_REQUEST_TIMEOUT:5000} username: ${ELASTICSEARCH_USERNAME:#{null}} password: ${ELASTICSEARCH_PASSWORD:#{null}} pathPrefix: ${ELASTICSEARCH_PATH_PREFIX:#{null}} From 67ff486cc1eee1578fd4c8e6dbfe02624046a999 Mon Sep 17 00:00:00 2001 From: Hyejin Yoon <0327jane@gmail.com> Date: Fri, 4 Oct 2024 13:08:25 +0900 Subject: [PATCH 03/25] feat: add miro & foursquare & Deutsche Telekom (#11516) --- docs-website/adoptionStoriesIndexes.json | 36 ++++++++++++++++++ .../adoption-stories-deutsche-telekom.png | Bin 0 -> 62725 bytes .../adoption-stories-foursquare.png | Bin 0 -> 64688 bytes .../adoption-stories-miro.png | Bin 0 -> 280693 bytes .../img/logos/companies/deutsche-telekom.png | Bin 0 -> 61401 bytes .../static/img/logos/companies/foursquare.png | Bin 0 -> 12042 bytes 6 files changed, 36 insertions(+) create mode 100644 docs-website/static/img/adoption-stories/adoption-stories-deutsche-telekom.png create mode 100644 docs-website/static/img/adoption-stories/adoption-stories-foursquare.png create mode 100644 docs-website/static/img/adoption-stories/adoption-stories-miro.png create mode 100644 docs-website/static/img/logos/companies/deutsche-telekom.png create mode 100644 docs-website/static/img/logos/companies/foursquare.png diff --git a/docs-website/adoptionStoriesIndexes.json b/docs-website/adoptionStoriesIndexes.json index 79ddf81e4ed6fd..f7b78e6cab8b2a 100644 --- a/docs-website/adoptionStoriesIndexes.json +++ b/docs-website/adoptionStoriesIndexes.json @@ -93,6 +93,42 @@ "category": "B2B & B2C", "description": "“We looked around for data catalog tool, and DataHub was a clear winner.”

Zynga levels up data management using DataHub, highlighting its role in enhancing data management, tracing data lineage, and ensuring data quality." }, + { + "name": "Miro", + "slug": "miro", + "imageUrl": "/img/logos/companies/miro.png", + "imageSize": "medium", + "link": "https://miro.com/careers/life-at-miro/tech/data-products-reliability-the-power-of-metadata/", + "linkType": "blog", + "tagline": "Data Products Reliability: The Power of Metadata", + "category": "B2B & B2C", + "platform": "cloud", + "description": "\"Leveraging our Datahub catalog, we have centralized metadata access for all data products. This integration eliminates the dependency on Airflow metadata alone for defining contracts, enabling flexible definitions for both building blocks and business metrics.\"" + }, + { + "name": "Foursquare", + "slug": "foursquare", + "imageUrl": "/img/logos/companies/foursquare.png", + "imageSize": "medium", + "link": "https://location.foursquare.com/resources/blog/leadership/foursquare-data-platform-from-fragmentation-to-control-plane/", + "linkType": "blog", + "tagline": "Foursquare Data Platform: From Fragmentation to Control (Plane)", + "category": "B2B & B2C", + "platform": "cloud", + "description": "\"After evaluating several options (...) we chose DataHub as the control plane for our data platform and partnered with Acryl Data, which offers DataHub Cloud, a premium hosted version of DataHub. (...) Another important thing that worked in DataHub’s favor is the rich and flexible taxonomy it offered for modeling the various aspects of a data platform.\"" + }, + { + "name": "Deutsche Telekom", + "slug": "deutsche-telekom", + "imageUrl": "/img/logos/companies/deutsche-telekom.png", + "imageSize": "medium", + "link": "https://karanjindal95.medium.com/from-chaos-to-clarity-how-datahub-transformed-our-data-utilization-5b5151efd34a", + "linkType": "blog", + "tagline": "From Chaos to Clarity: How DataHub Transformed our Data Utilization", + "category": "B2B & B2C", + "platform": "cloud", + "description": "\"The DataHub data catalog significantly supported our AI/ML team’s efforts by offering seamless access to detailed column descriptions and table schemas through its APIs. This comprehensive data accessibility enabled the team to efficiently develop a text-to-SQL tool, which translates natural language queries into SQL commands.\"" + }, { "name": "Chime", "slug": "chime", diff --git a/docs-website/static/img/adoption-stories/adoption-stories-deutsche-telekom.png b/docs-website/static/img/adoption-stories/adoption-stories-deutsche-telekom.png new file mode 100644 index 0000000000000000000000000000000000000000..015e2085237200d9f7bb636a0adfa666574b0fa2 GIT binary patch literal 62725 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 P2l%r;>}XqP<8$@@V)i=# literal 0 HcmV?d00001 diff --git a/docs-website/static/img/adoption-stories/adoption-stories-foursquare.png b/docs-website/static/img/adoption-stories/adoption-stories-foursquare.png new file mode 100644 index 0000000000000000000000000000000000000000..7140db1be24943790a0e6c186880b69552a1c5c9 GIT binary patch literal 64688 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 IEA!C*0h_M3SO5S3 literal 0 HcmV?d00001 diff --git a/docs-website/static/img/adoption-stories/adoption-stories-miro.png b/docs-website/static/img/adoption-stories/adoption-stories-miro.png new file mode 100644 index 0000000000000000000000000000000000000000..1ee3ee657292525a7283b5da82ece76f8ccf7700 GIT binary patch literal 280693 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

4lh;YCCsEuL}iIq6Wt9@;Wbz%1ePoH%i2 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 uMs!#izJv+hx> zCFP{*)4t3}flk+qVx1QeYYBfVf@$*{p{STF@2oQB1zm1}Y=-pKbRSk;Tsu)SY_FzV zCO2>?M-!P~ihU=`){iCaHkvK%A&GKdbNEMeL1@8@P-4}J5{{F0ORinq2te5meeF2- zeKH)ptw0yeaukW@e+;i!w0&5 z)BZYaASq!%kk|X;7UH9Rs9*%Qv`%AqQt6J+8?Rb9X}v_c4XOP)ESY~qrOdNBBs*O( z6(Rxk6SxL61EUg+gK$q6)K2eSU6=Uagu%PdzK)yF6n$9{aD-!{0E0pQCoBUePbBWPN zbixs76d@n&77x!w@^j6wtE*9y5Ps)7G@t6_dNv$^OGG7P#+9wwV%jz?MZkqj@5V^l z!KFxUs`hPvZQmh?876L@Rh=}~3g`F!!r!8GxXtbdmsI?8?H^rW9B_OU9ID2fPo2A; z3Bkoe@%-?$D4f3(Izx?wwVAU6PKn!tB<0 z3G8dknlv{Xbz6e>tnP6KkK4ftcQ7=om)JmT;9o%p+~k4LWADJu+!NL(1}q`McTcr1 zzr}aE$;Sc*kgALp9w)|#v?;8Azx(o*$yAEfH5SZbBVX7TpiNX?R-lNK^~+=rM?0Rp zF2av-fT7fUonS# zqO-eKMaJG9T#6g2TnumbOB~JyrlDc!x>2cM=bsnO3%VSAjUoWC=;T$+nL&eD-WckG zSpAL|8o5-fJqTYS4OKSFmo1$Y`Osrko42f-r3hw3m%>0?DZt&$U~Vw$nsh2hQTGiQzt}wETK}b{9L)ibOv!g)TMh7!=#f==l)}s4NK;&1xm0l0%**)E7y2Xn7`CRDHFJxI$s^oyA=B* zKM>J-+p0r8NK9)>gy*fuT0mAWZZa|X5YOQ1@HafWN7&ibOYNt7wg*)HSTXt@@@O+v zV%X~I5uf&)ZH?IfdcUjrv4SKtczI3eIzc7p!HDV0cKEd2&UXmz0+Vmlth-MuQ5Ate zb}0$NYxq0av6=1F)6$Sv>#8Jx!wmD3XpM($7pLi)B!4Z$lisyl^aqJ3w(%!P%LEqh zAF2?YT^KTgm;X)<3$Hu>9V1jGAIFP;6UA>wpqj~5Li_xai`btx;4um4l?`+uBvgvy zV!0f+QO=Skb^1mDr_QB%b5Remh%e200sj)Uq=ztz^O9!cr%2$SEREgH`?AS_sp2%W zu0_}%U`cacoa*G+QC@kR-@%Ory@$~d3Y4CQ#kk-Chqkl9%<(Zdyt|!u0%=f6m||eJ zd{YqP0||c}PUSefLfIVBEraLuas>zouU7J*YJ^okdY=VOF4RvUs1_m0Gj)$3_(+S7 z8_XPTSf03W1?bEgmY1fGU(CL*0QySly4H0bkEUSg!d_T@!$x?u8QA=QCLtn+iHmBo zDpXq`({enJGD|$+D&(y>mD61&Sl)Naw2_wDI$|3;b!8v3XCiOD*(}X&i&Ab>;o1nmC7+`s5=H>cq@O0tE|;`Xyc> z*@Z0}9bAG82e~6U&a4_~RY=fB7X+c?#%Uw9p#cO8qDvJ?=7v`Qez3BG)k^lXRnuR2 zrL|G8HYp;rgE>9R^HD+s{~mPE#txZ|hE>7$%;zkmPi>(7$G6cD)AF%ENVasaEX^c? zyK<0)?c_1y+bXhp&f1(nW^6C}<6�`!h~;MTW=uQ(L)&%njW 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


!mtdOs9f$Z$#a^{u~O|x4l`nPJIzC zF+j-(F)~9K_qS7LY)f7&;QGcnwiMuOac(FWKNs1pUK#i+X|t%LJM zfhZaEse>*{qsTxM;4C<$tCgHxT>xvdCzxsRPNoiqhLm;|J^ z2!CW%orx>mIDy18Ur_>?f(&t{2u;(bd~1pybt{K`1Y`H-wtuJlz#A$|h2mltn^r{$ z1EPX`f_)Xf>)9_zPerCg10<#+RYbRg8vH(+K!sk%dE0&@+J3pgGqeAfkm6U#Talqa zgNK?+&Fpvn%C|Khvhu~ZoBPRg-IObHuS_$Q{U{NykrN^`*1YA>2iB4kAB%ZCfgK{Y z_QW>XpL)P~UZ5|~^N@W_8a9(N(B?>FIpHJLPMy)0eL2Jv- zba*f91WG8-I--hlt&6+PcQr}ZrE7B^tiQ!leWf*^ajmk z7kDgBbZ1Fre@wlr#;vIh6@odW974x%zTHie>7wLjfujNd|M%tz_{eoQQi;%bh@N^VEACam#+; z{?f=O(~>4I?T+D-8d#51fNN^fDAvWdNCM=qYR5!YQId@$^=H&Eh(594Ehj%@>fO6V z%!O5>fzeVFp{S_Vz6d#Ii;ai5<(;cgK`x()-^%$ITNaOab8@}|%6sHhIo!?V2s4o` z*@+tb{)dOxHO4Gw_(P;lwuH*Ksj@*sc+^j};&iGZxCNho`%A|5`s0-!rPO|(!WS8# zKz00~{kNh@ltic{zC)LO`zq5T$B2UiT#P+WYdaY(uV2@q#dI#VM0)A=s9WgW@mZ3= zh7@!SCrYMVEdxDOo4rDhZsH0&E&dg1DpGb|?4RcTHG`t9Ug5%bVr|>Aq#itIO}E(c z{;GB~U3U-(J&tmnGK*xp6&W}wVN2y@oGJYQvzMqB<3p%;rCx5d9W9~ROf&ZPXvSfq z#6kZ@NB!}b00^U1<;9Mr#crP4<-jf4b`7tWt?@V2`UlNNUoly8E95e$Djj%!H(gSc zBw`$;VwO99)m?)Uy?@`3bh~NkW+yZ4ZSwjI&zwD+G+DyvZOo&+MS>2{=XUSep07Pw zLIBKY^?yAKa4uZgmFRkB=`qkm4uG^ogjswnn#!-G6*tO$6788Lb7H;rrrvOHpVQOg z)Up1YkC??Koa?*#*D=-1p;agMI9lVokx)>p`OhX5h(_wsW!eRphp7;$woU{)3QH=7 zn0J<`>lxC^iobx^`l@z z5^6ZbR1oaFV+Q2x&l}(}T-9oCEEtN977YLBb>zp%OY4s7$w_IiqhJw7m<1C1nj;|5 z)w=QIN!njE@M3c3r|f^#F)(otW917oYdwSh z1ZG%{+1`J0z9^brS<=~4C?RW|!SZ&*W;sN(#pVaEOo*eq8+fbf8 zn%&a2G(>!lw64dP>{lW3Ne-P_<~VzcoP+@(N$n�*G;QsB?Ksao0EmW#olRT%qc( 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≷wiW*Y;ea^!pDUV)R@O_O=_F)`G`2 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 m!t=_GKeGz5J;>xEEITe|-+O6qUjGz5&&x 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 J#cO?&{sOS`7_$HX literal 0 HcmV?d00001 diff --git a/docs-website/static/img/logos/companies/foursquare.png b/docs-website/static/img/logos/companies/foursquare.png new file mode 100644 index 0000000000000000000000000000000000000000..896f33c2beaf557f42432193807f0b0b7450c6a7 GIT binary patch literal 12042 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 literal 0 HcmV?d00001 From 134ad21afec91ad58a3e8e1efd580cdc2416ecf1 Mon Sep 17 00:00:00 2001 From: skrydal Date: Fri, 4 Oct 2024 13:59:06 +0200 Subject: [PATCH 04/25] fix(ingestion/nifi): Fix for incremental lineage ingestion for nifi (#11517) --- .../src/datahub/ingestion/source/nifi.py | 22 +++++ .../src/datahub/specific/datajob.py | 2 +- .../tests/unit/patch/test_patch_builder.py | 87 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/metadata-ingestion/src/datahub/ingestion/source/nifi.py b/metadata-ingestion/src/datahub/ingestion/source/nifi.py index 25781cd2f1dcc9..7072ebf6473df1 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/nifi.py +++ b/metadata-ingestion/src/datahub/ingestion/source/nifi.py @@ -332,10 +332,14 @@ def __init__(self) -> None: } def process_s3_provenance_event(self, event): + logger.debug(f"Processing s3 provenance event: {event}") attributes = event.get("attributes", []) s3_bucket = get_attribute_value(attributes, "s3.bucket") s3_key = get_attribute_value(attributes, "s3.key") if not s3_key: + logger.debug( + "s3.key not present in the list of attributes, trying to use filename attribute instead" + ) s3_key = get_attribute_value(attributes, "filename") s3_url = f"s3://{s3_bucket}/{s3_key}" @@ -344,6 +348,7 @@ def process_s3_provenance_event(self, event): dataset_name = s3_path.replace("/", ".") platform = "s3" dataset_urn = builder.make_dataset_urn(platform, s3_path, self.env) + logger.debug(f"Reasoned s3 dataset urn: {dataset_urn}") return ExternalDataset( platform, dataset_name, @@ -910,6 +915,11 @@ def construct_workunits(self) -> Iterable[MetadataWorkUnit]: # noqa: C901 ) for component in self.nifi_flow.components.values(): + logger.debug( + f"Beginng construction of workunits for component {component.id} of type {component.type} and name {component.name}" + ) + logger.debug(f"Inlets of the component: {component.inlets.keys()}") + logger.debug(f"Outlets of the component: {component.outlets.keys()}") job_name = component.name job_urn = builder.make_data_job_urn_with_flow(flow_urn, component.id) @@ -937,6 +947,9 @@ def construct_workunits(self) -> Iterable[MetadataWorkUnit]: # noqa: C901 jobProperties["last_event_time"] = component.last_event_time for dataset in component.inlets.values(): + logger.debug( + f"Yielding dataset workunits for {dataset.dataset_urn} (inlet)" + ) yield from self.construct_dataset_workunits( dataset.platform, dataset.dataset_name, @@ -945,6 +958,9 @@ def construct_workunits(self) -> Iterable[MetadataWorkUnit]: # noqa: C901 ) for dataset in component.outlets.values(): + logger.debug( + f"Yielding dataset workunits for {dataset.dataset_urn} (outlet)" + ) yield from self.construct_dataset_workunits( dataset.platform, dataset.dataset_name, @@ -1207,6 +1223,7 @@ def construct_job_workunits( inputJobs: List[str] = [], status: Optional[str] = None, ) -> Iterable[MetadataWorkUnit]: + logger.debug(f"Begining construction of job workunit for {job_urn}") if job_properties: job_properties = {k: v for k, v in job_properties.items() if v is not None} @@ -1229,8 +1246,12 @@ def construct_job_workunits( inlets.sort() outlets.sort() inputJobs.sort() + logger.debug(f"Inlets after sorting: {inlets}") + logger.debug(f"Outlets after sorting: {outlets}") + logger.debug(f"Input jobs after sorting: {inputJobs}") if self.config.incremental_lineage: + logger.debug("Preparing mcps for incremental lineage") patch_builder: DataJobPatchBuilder = DataJobPatchBuilder(job_urn) for inlet in inlets: patch_builder.add_input_dataset(inlet) @@ -1239,6 +1260,7 @@ def construct_job_workunits( for inJob in inputJobs: patch_builder.add_input_datajob(inJob) for patch_mcp in patch_builder.build(): + logger.debug(f"Preparing Patch MCP: {patch_mcp}") yield MetadataWorkUnit( id=f"{job_urn}-{patch_mcp.aspectName}", mcp_raw=patch_mcp ) diff --git a/metadata-ingestion/src/datahub/specific/datajob.py b/metadata-ingestion/src/datahub/specific/datajob.py index 2d944edeb36403..8da8edc8ef0f22 100644 --- a/metadata-ingestion/src/datahub/specific/datajob.py +++ b/metadata-ingestion/src/datahub/specific/datajob.py @@ -330,7 +330,7 @@ def add_output_dataset( self._add_patch( DataJobInputOutput.ASPECT_NAME, "add", - path=f"/outputDatasetEdges/{self.quote(str(input))}", + path=f"/outputDatasetEdges/{self.quote(str(output))}", value=output_edge, ) return self diff --git a/metadata-ingestion/tests/unit/patch/test_patch_builder.py b/metadata-ingestion/tests/unit/patch/test_patch_builder.py index 8c2a4b2c4a6ddd..267da6cdd5d205 100644 --- a/metadata-ingestion/tests/unit/patch/test_patch_builder.py +++ b/metadata-ingestion/tests/unit/patch/test_patch_builder.py @@ -1,10 +1,14 @@ +import json import pathlib import pytest +from freezegun.api import freeze_time from datahub.emitter.mce_builder import ( make_chart_urn, make_dashboard_urn, + make_data_flow_urn, + make_data_job_urn_with_flow, make_dataset_urn, make_schema_field_urn, make_tag_urn, @@ -22,6 +26,7 @@ ) from datahub.specific.chart import ChartPatchBuilder from datahub.specific.dashboard import DashboardPatchBuilder +from datahub.specific.datajob import DataJobPatchBuilder from datahub.specific.dataset import DatasetPatchBuilder from tests.test_helpers import mce_helpers @@ -175,3 +180,85 @@ def test_basic_dashboard_patch_builder(): ), ), ] + + +@freeze_time("2020-04-14 07:00:00") +def test_datajob_patch_builder(): + flow_urn = make_data_flow_urn( + orchestrator="nifi", flow_id="252C34e5af19-0192-1000-b248-b1abee565b5d" + ) + job_urn = make_data_job_urn_with_flow( + flow_urn, "5ca6fee7-0192-1000-f206-dfbc2b0d8bfb" + ) + patcher = DataJobPatchBuilder(job_urn) + + patcher.add_output_dataset( + "urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket/folder1,DEV)" + ) + patcher.add_output_dataset( + "urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket/folder3,DEV)" + ) + patcher.add_output_dataset( + "urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket/folder2,DEV)" + ) + + assert patcher.build() == [ + MetadataChangeProposalClass( + entityType="dataJob", + entityUrn="urn:li:dataJob:(urn:li:dataFlow:(nifi,252C34e5af19-0192-1000-b248-b1abee565b5d,prod),5ca6fee7-0192-1000-f206-dfbc2b0d8bfb)", + changeType="PATCH", + aspectName="dataJobInputOutput", + aspect=GenericAspectClass( + value=json.dumps( + [ + { + "op": "add", + "path": "/outputDatasetEdges/urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket~1folder1,DEV)", + "value": { + "destinationUrn": "urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket/folder1,DEV)", + "created": { + "time": 1586847600000, + "actor": "urn:li:corpuser:datahub", + }, + "lastModified": { + "time": 1586847600000, + "actor": "urn:li:corpuser:datahub", + }, + }, + }, + { + "op": "add", + "path": "/outputDatasetEdges/urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket~1folder3,DEV)", + "value": { + "destinationUrn": "urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket/folder3,DEV)", + "created": { + "time": 1586847600000, + "actor": "urn:li:corpuser:datahub", + }, + "lastModified": { + "time": 1586847600000, + "actor": "urn:li:corpuser:datahub", + }, + }, + }, + { + "op": "add", + "path": "/outputDatasetEdges/urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket~1folder2,DEV)", + "value": { + "destinationUrn": "urn:li:dataset:(urn:li:dataPlatform:s3,output-bucket/folder2,DEV)", + "created": { + "time": 1586847600000, + "actor": "urn:li:corpuser:datahub", + }, + "lastModified": { + "time": 1586847600000, + "actor": "urn:li:corpuser:datahub", + }, + }, + }, + ] + ).encode("utf-8"), + contentType="application/json-patch+json", + ), + ) + ] From 9e0f68cc94a65b6dacd289590a0bb5468f2e7d81 Mon Sep 17 00:00:00 2001 From: david-leifker <114954101+david-leifker@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:42:21 -0500 Subject: [PATCH 05/25] fix(tests): fix metadata-io tests (#11530) --- .../elasticsearch/ElasticSearchService.java | 4 +- .../elasticsearch/query/ESSearchDAO.java | 47 +++++++ .../query/request/SearchRequestHandler.java | 3 +- .../entity/DeleteEntityServiceTest.java | 4 +- .../entity/ebean/EbeanAspectDaoTest.java | 13 +- .../extractor/AspectExtractorTest.java | 2 +- .../SearchGraphServiceElasticSearchTest.java | 5 +- .../SearchGraphServiceOpenSearchTest.java | 5 +- .../LineageSearchResultCacheKeyTest.java | 4 +- .../search/LineageServiceTestBase.java | 9 +- .../search/SearchServiceTestBase.java | 9 +- .../metadata/search/TestEntityTestBase.java | 9 +- .../GoldenElasticSearchTest.java | 2 +- .../IndexBuilderElasticSearchTest.java | 2 +- .../LineageDataFixtureElasticSearchTest.java | 5 +- .../LineageServiceElasticSearchTest.java | 17 ++- .../SampleDataFixtureElasticSearchTest.java | 8 +- .../SearchDAOElasticSearchTest.java | 18 ++- .../SearchServiceElasticSearchTest.java | 17 ++- ...ystemMetadataServiceElasticSearchTest.java | 5 +- .../TestEntityElasticSearchTest.java | 17 ++- ...eseriesAspectServiceElasticSearchTest.java | 5 +- .../fixtures/SampleDataFixtureSetupTest.java | 47 ------- .../fixtures/SampleDataFixtureTestBase.java | 94 ++++++++++++-- .../opensearch/GoldenOpenSearchTest.java | 2 +- .../IndexBuilderOpenSearchTest.java | 2 +- .../LineageDataFixtureOpenSearchTest.java | 5 +- .../LineageServiceOpenSearchTest.java | 17 ++- .../SampleDataFixtureOpenSearchTest.java | 8 +- .../opensearch/SearchDAOOpenSearchTest.java | 19 ++- .../SearchServiceOpenSearchTest.java | 17 ++- .../SystemMetadataServiceOpenSearchTest.java | 5 +- .../opensearch/TestEntityOpenSearchTest.java | 17 ++- ...TimeseriesAspectServiceOpenSearchTest.java | 5 +- .../metadata/search/query/BrowseDAOTest.java | 6 +- .../search/query/SearchDAOTestBase.java | 78 ++++++++---- .../request/AggregationQueryBuilderTest.java | 2 +- .../query/request/SearchQueryBuilderTest.java | 4 + .../request/SearchRequestHandlerTest.java | 115 ++++++++++++++++++ .../metadata/search/utils/ESUtilsTest.java | 2 +- .../PropertyDefinitionValidatorTest.java | 2 +- ...chemaMetadataChangeEventGeneratorTest.java | 2 +- .../SampleDataFixtureConfiguration.java | 16 +-- .../SearchLineageFixtureConfiguration.java | 6 +- .../config/SearchCommonTestConfiguration.java | 12 +- .../common/DocumentationAssociation.pdl | 9 +- .../pegasus/com/linkedin/common/Forms.pdl | 15 ++- .../common/GlossaryTermAssociation.pdl | 9 +- .../com/linkedin/common/IncidentsSummary.pdl | 6 +- .../com/linkedin/common/RoleAssociation.pdl | 3 +- .../pegasus/com/linkedin/common/SubTypes.pdl | 2 +- .../com/linkedin/common/TagAssociation.pdl | 9 +- .../glossary/GlossaryRelatedTerms.pdl | 4 +- .../com/linkedin/identity/CorpUserInfo.pdl | 2 +- .../schema/EditableSchemaFieldInfo.pdl | 12 +- .../com/linkedin/schema/SchemaField.pdl | 18 ++- .../pegasus/com/linkedin/test/TestResults.pdl | 2 + 57 files changed, 540 insertions(+), 244 deletions(-) delete mode 100644 metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureSetupTest.java diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/ElasticSearchService.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/ElasticSearchService.java index e66b12db891df8..6001e2f6e660fa 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/ElasticSearchService.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/ElasticSearchService.java @@ -2,6 +2,7 @@ import static com.linkedin.metadata.search.utils.SearchUtils.applyDefaultSearchFlags; +import com.google.common.annotations.VisibleForTesting; import com.linkedin.common.urn.Urn; import com.linkedin.metadata.browse.BrowseResult; import com.linkedin.metadata.browse.BrowseResultV2; @@ -30,6 +31,7 @@ import java.util.Optional; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.opensearch.action.explain.ExplainResponse; @@ -51,7 +53,7 @@ public class ElasticSearchService implements EntitySearchService, ElasticSearchI private static final int MAX_RUN_IDS_INDEXED = 25; // Save the previous 25 run ids in the index. private final EntityIndexBuilders indexBuilders; - private final ESSearchDAO esSearchDAO; + @VisibleForTesting @Getter private final ESSearchDAO esSearchDAO; private final ESBrowseDAO esBrowseDAO; private final ESWriteDAO esWriteDAO; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java index cec73de7041263..f09a81c0c8b891 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java @@ -6,7 +6,9 @@ import com.codahale.metrics.Timer; import com.datahub.util.exception.ESQueryException; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.linkedin.data.template.LongMap; import com.linkedin.metadata.config.search.SearchConfiguration; @@ -78,6 +80,24 @@ public class ESSearchDAO { @Nonnull private final SearchConfiguration searchConfiguration; @Nullable private final CustomSearchConfiguration customSearchConfiguration; @Nonnull private final QueryFilterRewriteChain queryFilterRewriteChain; + private final boolean testLoggingEnabled; + + public ESSearchDAO( + RestHighLevelClient client, + boolean pointInTimeCreationEnabled, + String elasticSearchImplementation, + @Nonnull SearchConfiguration searchConfiguration, + @Nullable CustomSearchConfiguration customSearchConfiguration, + @Nonnull QueryFilterRewriteChain queryFilterRewriteChain) { + this( + client, + pointInTimeCreationEnabled, + elasticSearchImplementation, + searchConfiguration, + customSearchConfiguration, + queryFilterRewriteChain, + false); + } public long docCount(@Nonnull OperationContext opContext, @Nonnull String entityName) { return docCount(opContext, entityName, null); @@ -279,6 +299,11 @@ public SearchResult search( searchRequest.indices( entityNames.stream().map(indexConvention::getEntityIndexName).toArray(String[]::new)); searchRequestTimer.stop(); + + if (testLoggingEnabled) { + testLog(opContext.getObjectMapper(), searchRequest); + } + // Step 2: execute the query and extract results, validated against document model as well return executeAndExtract(opContext, entitySpecs, searchRequest, transformedFilters, from, size); } @@ -478,6 +503,11 @@ public ScrollResult scroll( } scrollRequestTimer.stop(); + + if (testLoggingEnabled) { + testLog(opContext.getObjectMapper(), searchRequest); + } + return executeAndExtract( opContext, entitySpecs, searchRequest, transformedFilters, keepAlive, size); } @@ -605,4 +635,21 @@ public ExplainResponse explain( throw new IllegalStateException("Failed to explain query:", e); } } + + private void testLog(ObjectMapper mapper, SearchRequest searchRequest) { + try { + log.warn("SearchRequest(custom): {}", mapper.writeValueAsString(customSearchConfiguration)); + final String[] indices = searchRequest.indices(); + log.warn( + String.format( + "SearchRequest(indices): %s", + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(indices))); + log.warn( + String.format( + "SearchRequest(query): %s", + mapper.writeValueAsString(mapper.readTree(searchRequest.source().toString())))); + } catch (JsonProcessingException e) { + log.warn("Error writing test log"); + } + } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java index c935e6f54742c3..cb02fb1c8b2f76 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java @@ -58,6 +58,7 @@ import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.opensearch.action.search.SearchRequest; @@ -80,7 +81,7 @@ public class SearchRequestHandler { private static final Map, SearchRequestHandler> REQUEST_HANDLER_BY_ENTITY_NAME = new ConcurrentHashMap<>(); private final List entitySpecs; - private final Set defaultQueryFieldNames; + @Getter private final Set defaultQueryFieldNames; @Nonnull private final HighlightBuilder highlights; private final SearchConfiguration configs; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/entity/DeleteEntityServiceTest.java b/metadata-io/src/test/java/com/linkedin/metadata/entity/DeleteEntityServiceTest.java index d585ff1ce8383f..0e8ee08e60739f 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/entity/DeleteEntityServiceTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/entity/DeleteEntityServiceTest.java @@ -2,7 +2,9 @@ import static com.linkedin.metadata.search.utils.QueryUtils.*; import static org.mockito.Mockito.*; -import static org.testng.AssertJUnit.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import com.datahub.util.RecordUtils; import com.google.common.collect.ImmutableList; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/entity/ebean/EbeanAspectDaoTest.java b/metadata-io/src/test/java/com/linkedin/metadata/entity/ebean/EbeanAspectDaoTest.java index 43123fb9872a0f..109c9b5c44efb9 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/entity/ebean/EbeanAspectDaoTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/entity/ebean/EbeanAspectDaoTest.java @@ -4,6 +4,8 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.linkedin.metadata.EbeanTestUtils; import com.linkedin.metadata.aspect.batch.AspectsBatch; import com.linkedin.metadata.config.EbeanConfiguration; @@ -41,15 +43,15 @@ public void testGetNextVersionForUpdate() { // Get the captured SQL statements List sql = LoggedSql.stop().stream() - .filter(str -> !str.contains("INFORMATION_SCHEMA.TABLES")) + .filter(str -> str.contains("(t0.urn,t0.aspect,t0.version)")) .toList(); - assertEquals(sql.size(), 2, String.format("Found: %s", sql)); + assertEquals(sql.size(), 1, String.format("Found: %s", sql)); assertTrue( sql.get(0).contains("for update;"), String.format("Did not find `for update` in %s ", sql)); } @Test - public void testGetLatestAspectsForUpdate() { + public void testGetLatestAspectsForUpdate() throws JsonProcessingException { LoggedSql.start(); testDao.runInTransactionWithRetryUnlocked( @@ -63,9 +65,10 @@ public void testGetLatestAspectsForUpdate() { // Get the captured SQL statements List sql = LoggedSql.stop().stream() - .filter(str -> !str.contains("INFORMATION_SCHEMA.TABLES")) + .filter(str -> str.contains("(t0.urn,t0.aspect,t0.version)")) .toList(); - assertEquals(sql.size(), 1, String.format("Found: %s", sql)); + assertEquals( + sql.size(), 1, String.format("Found: %s", new ObjectMapper().writeValueAsString(sql))); assertTrue( sql.get(0).contains("for update;"), String.format("Did not find `for update` in %s ", sql)); } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/extractor/AspectExtractorTest.java b/metadata-io/src/test/java/com/linkedin/metadata/extractor/AspectExtractorTest.java index a98386f6f871b0..f8bf2376e6bc12 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/extractor/AspectExtractorTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/extractor/AspectExtractorTest.java @@ -1,6 +1,6 @@ package com.linkedin.metadata.extractor; -import static org.testng.AssertJUnit.assertEquals; +import static org.testng.Assert.assertEquals; import com.datahub.test.TestEntityAspect; import com.datahub.test.TestEntityAspectArray; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/graph/search/elasticsearch/SearchGraphServiceElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/graph/search/elasticsearch/SearchGraphServiceElasticSearchTest.java index b2c49857cb0b96..8a0dfcbe34a69e 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/graph/search/elasticsearch/SearchGraphServiceElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/graph/search/elasticsearch/SearchGraphServiceElasticSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.graph.search.elasticsearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.graph.search.SearchGraphServiceTestBase; import com.linkedin.metadata.search.elasticsearch.ElasticSearchSuite; import com.linkedin.metadata.search.elasticsearch.indexbuilder.ESIndexBuilder; @@ -9,7 +11,6 @@ import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ElasticSearchSuite.class, SearchTestContainerConfiguration.class}) @@ -39,6 +40,6 @@ protected ESIndexBuilder getIndexBuilder() { @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/graph/search/opensearch/SearchGraphServiceOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/graph/search/opensearch/SearchGraphServiceOpenSearchTest.java index 28b545f8175391..08a6ea4ef2c9c7 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/graph/search/opensearch/SearchGraphServiceOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/graph/search/opensearch/SearchGraphServiceOpenSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.graph.search.opensearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.graph.search.SearchGraphServiceTestBase; import com.linkedin.metadata.search.elasticsearch.indexbuilder.ESIndexBuilder; import com.linkedin.metadata.search.elasticsearch.update.ESBulkProcessor; @@ -9,7 +11,6 @@ import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({OpenSearchSuite.class, SearchTestContainerConfiguration.class}) @@ -39,6 +40,6 @@ protected ESIndexBuilder getIndexBuilder() { @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/LineageSearchResultCacheKeyTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/LineageSearchResultCacheKeyTest.java index 1d4a545fc06a2b..d2d27bc2de27c5 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/LineageSearchResultCacheKeyTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/LineageSearchResultCacheKeyTest.java @@ -1,7 +1,7 @@ package com.linkedin.metadata.search; -import static org.testng.AssertJUnit.assertEquals; -import static org.testng.AssertJUnit.assertNotSame; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotSame; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.annotations.Test; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/LineageServiceTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/search/LineageServiceTestBase.java index d9268c1b50efeb..39fb6001eeb952 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/LineageServiceTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/LineageServiceTestBase.java @@ -35,7 +35,6 @@ import com.linkedin.metadata.config.cache.SearchCacheConfiguration; import com.linkedin.metadata.config.cache.SearchLineageCacheConfiguration; import com.linkedin.metadata.config.search.SearchConfiguration; -import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.graph.EntityLineageResult; import com.linkedin.metadata.graph.GraphService; import com.linkedin.metadata.graph.LineageDirection; @@ -103,9 +102,6 @@ public abstract class LineageServiceTestBase extends AbstractTestNGSpringContext @Nonnull protected abstract SearchConfiguration getSearchConfiguration(); - @Nonnull - protected abstract CustomSearchConfiguration getCustomSearchConfiguration(); - private SettingsBuilder settingsBuilder; private ElasticSearchService elasticSearchService; private GraphService graphService; @@ -211,10 +207,7 @@ private ElasticSearchService buildEntitySearchService() { QueryFilterRewriteChain.EMPTY); ESBrowseDAO browseDAO = new ESBrowseDAO( - searchClientSpy, - getSearchConfiguration(), - getCustomSearchConfiguration(), - QueryFilterRewriteChain.EMPTY); + searchClientSpy, getSearchConfiguration(), null, QueryFilterRewriteChain.EMPTY); ESWriteDAO writeDAO = new ESWriteDAO(searchClientSpy, getBulkProcessor(), 1); return new ElasticSearchService(indexBuilders, searchDAO, browseDAO, writeDAO); } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/SearchServiceTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/search/SearchServiceTestBase.java index ba83a381916c29..b20326deeb9458 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/SearchServiceTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/SearchServiceTestBase.java @@ -14,7 +14,6 @@ import com.linkedin.common.urn.Urn; import com.linkedin.metadata.config.cache.EntityDocCountCacheConfiguration; import com.linkedin.metadata.config.search.SearchConfiguration; -import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.models.registry.SnapshotEntityRegistry; import com.linkedin.metadata.query.filter.Condition; import com.linkedin.metadata.query.filter.ConjunctiveCriterion; @@ -64,9 +63,6 @@ public abstract class SearchServiceTestBase extends AbstractTestNGSpringContextT @Nonnull protected abstract SearchConfiguration getSearchConfiguration(); - @Nonnull - protected abstract CustomSearchConfiguration getCustomSearchConfiguration(); - protected OperationContext operationContext; private SettingsBuilder settingsBuilder; private ElasticSearchService elasticSearchService; @@ -136,10 +132,7 @@ private ElasticSearchService buildEntitySearchService() { QueryFilterRewriteChain.EMPTY); ESBrowseDAO browseDAO = new ESBrowseDAO( - getSearchClient(), - getSearchConfiguration(), - getCustomSearchConfiguration(), - QueryFilterRewriteChain.EMPTY); + getSearchClient(), getSearchConfiguration(), null, QueryFilterRewriteChain.EMPTY); ESWriteDAO writeDAO = new ESWriteDAO(getSearchClient(), getBulkProcessor(), 1); return new ElasticSearchService(indexBuilders, searchDAO, browseDAO, writeDAO); } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/TestEntityTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/search/TestEntityTestBase.java index 7b6fcd46333d2d..206b97ce6c1045 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/TestEntityTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/TestEntityTestBase.java @@ -13,7 +13,6 @@ import com.linkedin.metadata.browse.BrowseResult; import com.linkedin.metadata.browse.BrowseResultV2; import com.linkedin.metadata.config.search.SearchConfiguration; -import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.models.registry.SnapshotEntityRegistry; import com.linkedin.metadata.search.elasticsearch.ElasticSearchService; import com.linkedin.metadata.search.elasticsearch.indexbuilder.ESIndexBuilder; @@ -53,9 +52,6 @@ public abstract class TestEntityTestBase extends AbstractTestNGSpringContextTest @Nonnull protected abstract SearchConfiguration getSearchConfiguration(); - @Nonnull - protected abstract CustomSearchConfiguration getCustomSearchConfiguration(); - private SettingsBuilder settingsBuilder; private ElasticSearchService elasticSearchService; private OperationContext opContext; @@ -102,10 +98,7 @@ private ElasticSearchService buildService() { QueryFilterRewriteChain.EMPTY); ESBrowseDAO browseDAO = new ESBrowseDAO( - getSearchClient(), - getSearchConfiguration(), - getCustomSearchConfiguration(), - QueryFilterRewriteChain.EMPTY); + getSearchClient(), getSearchConfiguration(), null, QueryFilterRewriteChain.EMPTY); ESWriteDAO writeDAO = new ESWriteDAO(getSearchClient(), getBulkProcessor(), 1); ElasticSearchService searchService = new ElasticSearchService(indexBuilders, searchDAO, browseDAO, writeDAO); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/GoldenElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/GoldenElasticSearchTest.java index 29f5964c853f13..ad30c9d0229aa6 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/GoldenElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/GoldenElasticSearchTest.java @@ -1,6 +1,6 @@ package com.linkedin.metadata.search.elasticsearch; -import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.Assert.assertNotNull; import com.linkedin.metadata.search.SearchService; import com.linkedin.metadata.search.fixtures.GoldenTestBase; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/IndexBuilderElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/IndexBuilderElasticSearchTest.java index 911a21767bdeaf..af0b7003bd1d15 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/IndexBuilderElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/IndexBuilderElasticSearchTest.java @@ -1,6 +1,6 @@ package com.linkedin.metadata.search.elasticsearch; -import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.Assert.assertNotNull; import com.linkedin.metadata.search.indexbuilder.IndexBuilderTestBase; import io.datahubproject.test.search.config.SearchTestContainerConfiguration; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/LineageDataFixtureElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/LineageDataFixtureElasticSearchTest.java index 143ae80abc52d3..fe992f61d311d6 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/LineageDataFixtureElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/LineageDataFixtureElasticSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.elasticsearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.search.LineageSearchService; import com.linkedin.metadata.search.SearchService; import com.linkedin.metadata.search.fixtures.LineageDataFixtureTestBase; @@ -10,7 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Getter @@ -35,6 +36,6 @@ public class LineageDataFixtureElasticSearchTest extends LineageDataFixtureTestB @Test public void initTest() { - AssertJUnit.assertNotNull(lineageService); + assertNotNull(lineageService); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/LineageServiceElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/LineageServiceElasticSearchTest.java index 8c4195f9ff5343..7ccf7605432eeb 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/LineageServiceElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/LineageServiceElasticSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.elasticsearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.search.LineageServiceTestBase; @@ -10,8 +12,8 @@ import org.jetbrains.annotations.NotNull; import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ @@ -25,7 +27,10 @@ public class LineageServiceElasticSearchTest extends LineageServiceTestBase { @Autowired private ESBulkProcessor _bulkProcessor; @Autowired private ESIndexBuilder _esIndexBuilder; @Autowired private SearchConfiguration _searchConfiguration; - @Autowired private CustomSearchConfiguration _customSearchConfiguration; + + @Autowired + @Qualifier("defaultTestCustomSearchConfig") + private CustomSearchConfiguration _customSearchConfiguration; @NotNull @Override @@ -51,14 +56,8 @@ protected SearchConfiguration getSearchConfiguration() { return _searchConfiguration; } - @NotNull - @Override - protected CustomSearchConfiguration getCustomSearchConfiguration() { - return _customSearchConfiguration; - } - @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SampleDataFixtureElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SampleDataFixtureElasticSearchTest.java index e256f75242a42f..68b68b289dd2c0 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SampleDataFixtureElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SampleDataFixtureElasticSearchTest.java @@ -1,8 +1,9 @@ package com.linkedin.metadata.search.elasticsearch; -import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.Assert.assertNotNull; import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.search.SearchService; import com.linkedin.metadata.search.fixtures.SampleDataFixtureTestBase; import io.datahubproject.metadata.context.OperationContext; @@ -37,6 +38,11 @@ public class SampleDataFixtureElasticSearchTest extends SampleDataFixtureTestBas @Qualifier("sampleDataOperationContext") protected OperationContext operationContext; + @Getter + @Autowired + @Qualifier("fixtureCustomSearchConfig") + protected CustomSearchConfiguration customSearchConfiguration; + @Test public void initTest() { assertNotNull(searchClient); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SearchDAOElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SearchDAOElasticSearchTest.java index a6a8279fe86de2..3fc49a4d624fa5 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SearchDAOElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SearchDAOElasticSearchTest.java @@ -1,8 +1,10 @@ package com.linkedin.metadata.search.elasticsearch; -import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.Assert.assertNotNull; import com.linkedin.metadata.config.search.SearchConfiguration; +import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; +import com.linkedin.metadata.search.elasticsearch.query.ESSearchDAO; import com.linkedin.metadata.search.query.SearchDAOTestBase; import io.datahubproject.metadata.context.OperationContext; import io.datahubproject.test.fixtures.search.SampleDataFixtureConfiguration; @@ -28,6 +30,20 @@ public class SearchDAOElasticSearchTest extends SearchDAOTestBase { @Qualifier("sampleDataOperationContext") protected OperationContext operationContext; + @Autowired + @Qualifier("sampleDataEntitySearchService") + protected ElasticSearchService entitySearchService; + + @Getter + @Autowired + @Qualifier("fixtureCustomSearchConfig") + protected CustomSearchConfiguration customSearchConfiguration; + + @Override + protected ESSearchDAO getESSearchDao() { + return entitySearchService.getEsSearchDAO(); + } + @Test public void initTest() { assertNotNull(searchClient); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SearchServiceElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SearchServiceElasticSearchTest.java index 7133971847f982..92dfa18d4feeba 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SearchServiceElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SearchServiceElasticSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.elasticsearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.search.SearchServiceTestBase; @@ -10,8 +12,8 @@ import org.jetbrains.annotations.NotNull; import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ @@ -25,7 +27,10 @@ public class SearchServiceElasticSearchTest extends SearchServiceTestBase { @Autowired private ESBulkProcessor _bulkProcessor; @Autowired private ESIndexBuilder _esIndexBuilder; @Autowired private SearchConfiguration _searchConfiguration; - @Autowired private CustomSearchConfiguration _customSearchConfiguration; + + @Autowired + @Qualifier("defaultTestCustomSearchConfig") + private CustomSearchConfiguration _customSearchConfiguration; @NotNull @Override @@ -51,14 +56,8 @@ protected SearchConfiguration getSearchConfiguration() { return _searchConfiguration; } - @NotNull - @Override - protected CustomSearchConfiguration getCustomSearchConfiguration() { - return _customSearchConfiguration; - } - @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SystemMetadataServiceElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SystemMetadataServiceElasticSearchTest.java index a23cd5b051ecbb..b4093459ab3f1b 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SystemMetadataServiceElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/SystemMetadataServiceElasticSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.elasticsearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.search.elasticsearch.indexbuilder.ESIndexBuilder; import com.linkedin.metadata.search.elasticsearch.update.ESBulkProcessor; import com.linkedin.metadata.systemmetadata.SystemMetadataServiceTestBase; @@ -8,7 +10,6 @@ import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ElasticSearchSuite.class, SearchTestContainerConfiguration.class}) @@ -38,6 +39,6 @@ protected ESIndexBuilder getIndexBuilder() { @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/TestEntityElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/TestEntityElasticSearchTest.java index 5ad7b1218a5bf4..ec6f2e2b3f07a5 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/TestEntityElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/TestEntityElasticSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.elasticsearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.search.TestEntityTestBase; @@ -10,8 +12,8 @@ import org.jetbrains.annotations.NotNull; import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ @@ -25,7 +27,10 @@ public class TestEntityElasticSearchTest extends TestEntityTestBase { @Autowired private ESBulkProcessor bulkProcessor; @Autowired private ESIndexBuilder esIndexBuilder; @Autowired private SearchConfiguration searchConfiguration; - @Autowired private CustomSearchConfiguration customSearchConfiguration; + + @Autowired + @Qualifier("defaultTestCustomSearchConfig") + private CustomSearchConfiguration customSearchConfiguration; @NotNull @Override @@ -51,14 +56,8 @@ protected SearchConfiguration getSearchConfiguration() { return searchConfiguration; } - @NotNull - @Override - protected CustomSearchConfiguration getCustomSearchConfiguration() { - return customSearchConfiguration; - } - @Test public void initTest() { - AssertJUnit.assertNotNull(searchClient); + assertNotNull(searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/TimeseriesAspectServiceElasticSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/TimeseriesAspectServiceElasticSearchTest.java index 1f51d463a2963a..8a5f5d673aa7b9 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/TimeseriesAspectServiceElasticSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/TimeseriesAspectServiceElasticSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.elasticsearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.search.elasticsearch.indexbuilder.ESIndexBuilder; import com.linkedin.metadata.search.elasticsearch.update.ESBulkProcessor; import com.linkedin.metadata.timeseries.search.TimeseriesAspectServiceTestBase; @@ -9,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ElasticSearchSuite.class, SearchTestContainerConfiguration.class}) @@ -42,6 +43,6 @@ protected ESIndexBuilder getIndexBuilder() { @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureSetupTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureSetupTest.java deleted file mode 100644 index b908933fcc8e37..00000000000000 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureSetupTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.linkedin.metadata.search.fixtures; - -import static org.testng.AssertJUnit.assertEquals; - -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; -import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Map; -import org.springframework.core.io.ClassPathResource; -import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; -import org.testng.annotations.Test; - -public class SampleDataFixtureSetupTest extends AbstractTestNGSpringContextTests { - private static final String DEFAULT_CONFIG = "search_config.yaml"; - private static final String TEST_FIXTURE_CONFIG = "search_config_fixture_test.yml"; - private static final YAMLMapper MAPPER = new YAMLMapper(); - - /** - * Ensure default search configuration matches the test fixture configuration (allowing for some - * differences) - */ - @Test - public void testConfig() throws IOException { - final CustomSearchConfiguration defaultConfig; - final CustomSearchConfiguration fixtureConfig; - - try (InputStream stream = new ClassPathResource(DEFAULT_CONFIG).getInputStream()) { - defaultConfig = MAPPER.readValue(stream, CustomSearchConfiguration.class); - } - try (InputStream stream = new ClassPathResource(TEST_FIXTURE_CONFIG).getInputStream()) { - fixtureConfig = MAPPER.readValue(stream, CustomSearchConfiguration.class); - - // test specifics - ((List>) - fixtureConfig.getQueryConfigurations().get(1).getFunctionScore().get("functions")) - .remove(1); - - ((List>) - fixtureConfig.getQueryConfigurations().get(2).getFunctionScore().get("functions")) - .remove(1); - } - - assertEquals(fixtureConfig, defaultConfig); - } -} diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureTestBase.java index 8cb0678180ccbf..bc3c892e07b1bb 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureTestBase.java @@ -12,6 +12,7 @@ import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.linkedin.common.urn.Urn; @@ -22,6 +23,7 @@ import com.linkedin.datahub.graphql.types.corpuser.CorpUserType; import com.linkedin.datahub.graphql.types.dataset.DatasetType; import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.models.EntitySpec; import com.linkedin.metadata.models.SearchableFieldSpec; import com.linkedin.metadata.models.registry.EntityRegistry; @@ -43,6 +45,7 @@ import com.linkedin.r2.RemoteInvocationException; import io.datahubproject.metadata.context.OperationContext; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -63,11 +66,13 @@ import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.search.sort.FieldSortBuilder; import org.opensearch.search.sort.SortBuilder; +import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; -import org.testng.AssertJUnit; import org.testng.annotations.Test; public abstract class SampleDataFixtureTestBase extends AbstractTestNGSpringContextTests { + public static final String DEFAULT_CONFIG = "search_config.yaml"; + public static final YAMLMapper MAPPER = new YAMLMapper(); @Nonnull protected abstract SearchService getSearchService(); @@ -81,6 +86,9 @@ public abstract class SampleDataFixtureTestBase extends AbstractTestNGSpringCont @Nonnull protected abstract OperationContext getOperationContext(); + @Nonnull + protected abstract CustomSearchConfiguration getCustomSearchConfiguration(); + @Test public void testSearchFieldConfig() throws IOException { /* @@ -971,8 +979,13 @@ public void testSmokeTestQueries() { actualCount, expectedCount, String.format( - "Search term `%s` has %s fulltext results, expected %s results.", - key, actualCount, expectedCount)); + "Search term `%s` has %s fulltext results, expected %s results. Results: %s", + key, + actualCount, + expectedCount, + value.getEntities().stream() + .map(SearchEntity::getEntity) + .collect(Collectors.toList()))); }); Map expectedStructuredMinimums = @@ -998,8 +1011,13 @@ public void testSmokeTestQueries() { actualCount, expectedCount, String.format( - "Search term `%s` has %s structured results, expected %s results.", - key, actualCount, expectedCount)); + "Search term `%s` has %s structured results, expected %s results. Results: %s", + key, + actualCount, + expectedCount, + value.getEntities().stream() + .map(SearchEntity::getEntity) + .collect(Collectors.toList()))); }); } @@ -1318,6 +1336,7 @@ public void testScrollAcrossEntities() throws IOException { String query = "logging_events"; final int batchSize = 1; int totalResults = 0; + List resultUrns = new ArrayList<>(); String scrollId = null; do { ScrollResult result = @@ -1325,10 +1344,11 @@ public void testScrollAcrossEntities() throws IOException { int numResults = result.hasEntities() ? result.getEntities().size() : 0; assertTrue(numResults <= batchSize); totalResults += numResults; + resultUrns.addAll(result.getEntities().stream().map(SearchEntity::getEntity).toList()); scrollId = result.getScrollId(); } while (scrollId != null); // expect 2 total matching results - assertEquals(totalResults, 2); + assertEquals(totalResults, 2, String.format("query `%s` Results: %s", query, resultUrns)); } @Test @@ -1703,7 +1723,15 @@ public void testOr() { assertTrue( result.getEntities().stream().noneMatch(e -> e.getMatchedFields().isEmpty()), String.format("%s - Expected search results to include matched fields", query)); - assertEquals(result.getEntities().size(), 2); + assertEquals( + result.getEntities().size(), + 2, + String.format( + "Query: `%s` Results: %s", + query, + result.getEntities().stream() + .map(SearchEntity::getEntity) + .collect(Collectors.toList()))); } @Test @@ -1726,7 +1754,15 @@ public void testNegate() { assertTrue( result.getEntities().stream().noneMatch(e -> e.getMatchedFields().isEmpty()), String.format("%s - Expected search results to include matched fields", query)); - assertEquals(result.getEntities().size(), 2); + assertEquals( + result.getEntities().size(), + 2, + String.format( + "Query: `%s` Results: %s", + query, + result.getEntities().stream() + .map(SearchEntity::getEntity) + .collect(Collectors.toList()))); } @Test @@ -1896,7 +1932,15 @@ public void testPrefixVsExact() { result.getEntities().stream().noneMatch(e -> e.getMatchedFields().isEmpty()), String.format("%s - Expected search results to include matched fields", query)); - assertEquals(result.getEntities().size(), 2); + assertEquals( + result.getEntities().size(), + 2, + String.format( + "Query: `%s` Results: %s", + query, + result.getEntities().stream() + .map(SearchEntity::getEntity) + .collect(Collectors.toList()))); assertEquals( result.getEntities().get(0).getEntity().toString(), "urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.customers,PROD)", @@ -1988,7 +2032,7 @@ public void testSortOrdering() { @Test public void testFilterOnHasValuesField() { - AssertJUnit.assertNotNull(getSearchService()); + assertNotNull(getSearchService()); Filter filter = new Filter() .setOr( @@ -2010,7 +2054,7 @@ public void testFilterOnHasValuesField() { @Test public void testFilterOnNumValuesField() { - AssertJUnit.assertNotNull(getSearchService()); + assertNotNull(getSearchService()); Filter filter = new Filter() .setOr( @@ -2030,6 +2074,34 @@ public void testFilterOnNumValuesField() { assertEquals(searchResult.getEntities().size(), 4); } + /** + * Ensure default search configuration matches the test fixture configuration (allowing for some + * differences) + */ + @Test + public void testConfig() throws IOException { + final CustomSearchConfiguration defaultConfig; + try (InputStream stream = new ClassPathResource(DEFAULT_CONFIG).getInputStream()) { + defaultConfig = MAPPER.readValue(stream, CustomSearchConfiguration.class); + } + + final CustomSearchConfiguration fixtureConfig = + MAPPER.readValue( + MAPPER.writeValueAsBytes(getCustomSearchConfiguration()), + CustomSearchConfiguration.class); + + // test specifics + ((List>) + fixtureConfig.getQueryConfigurations().get(1).getFunctionScore().get("functions")) + .remove(1); + + ((List>) + fixtureConfig.getQueryConfigurations().get(2).getFunctionScore().get("functions")) + .remove(1); + + assertEquals(fixtureConfig, defaultConfig); + } + private Stream getTokens(AnalyzeRequest request) throws IOException { return getSearchClient() diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/GoldenOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/GoldenOpenSearchTest.java index db39531bba08c6..9e105a69de5d14 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/GoldenOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/GoldenOpenSearchTest.java @@ -1,6 +1,6 @@ package com.linkedin.metadata.search.opensearch; -import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.Assert.assertNotNull; import com.linkedin.metadata.search.SearchService; import com.linkedin.metadata.search.fixtures.GoldenTestBase; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/IndexBuilderOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/IndexBuilderOpenSearchTest.java index ef1ed51eb47991..01a9fc84c83a6f 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/IndexBuilderOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/IndexBuilderOpenSearchTest.java @@ -1,6 +1,6 @@ package com.linkedin.metadata.search.opensearch; -import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.Assert.assertNotNull; import com.linkedin.metadata.search.indexbuilder.IndexBuilderTestBase; import io.datahubproject.test.search.config.SearchTestContainerConfiguration; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/LineageDataFixtureOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/LineageDataFixtureOpenSearchTest.java index 98ac4013443524..ec1c485cc0f551 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/LineageDataFixtureOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/LineageDataFixtureOpenSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.opensearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.search.LineageSearchService; import com.linkedin.metadata.search.SearchService; import com.linkedin.metadata.search.fixtures.LineageDataFixtureTestBase; @@ -10,7 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Getter @@ -35,6 +36,6 @@ public class LineageDataFixtureOpenSearchTest extends LineageDataFixtureTestBase @Test public void initTest() { - AssertJUnit.assertNotNull(lineageService); + assertNotNull(lineageService); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/LineageServiceOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/LineageServiceOpenSearchTest.java index 26c2cf28cdecad..d24501d118925e 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/LineageServiceOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/LineageServiceOpenSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.opensearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.search.LineageServiceTestBase; @@ -10,8 +12,8 @@ import org.jetbrains.annotations.NotNull; import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ @@ -25,7 +27,10 @@ public class LineageServiceOpenSearchTest extends LineageServiceTestBase { @Autowired private ESBulkProcessor _bulkProcessor; @Autowired private ESIndexBuilder _esIndexBuilder; @Autowired private SearchConfiguration _searchConfiguration; - @Autowired private CustomSearchConfiguration _customSearchConfiguration; + + @Autowired + @Qualifier("defaultTestCustomSearchConfig") + private CustomSearchConfiguration _customSearchConfiguration; @NotNull @Override @@ -51,14 +56,8 @@ protected SearchConfiguration getSearchConfiguration() { return _searchConfiguration; } - @NotNull - @Override - protected CustomSearchConfiguration getCustomSearchConfiguration() { - return _customSearchConfiguration; - } - @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SampleDataFixtureOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SampleDataFixtureOpenSearchTest.java index 5d47e6ffd6fa5c..4bf8465b14ac9d 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SampleDataFixtureOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SampleDataFixtureOpenSearchTest.java @@ -1,8 +1,9 @@ package com.linkedin.metadata.search.opensearch; -import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.Assert.assertNotNull; import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.search.SearchService; import com.linkedin.metadata.search.fixtures.SampleDataFixtureTestBase; import io.datahubproject.metadata.context.OperationContext; @@ -37,6 +38,11 @@ public class SampleDataFixtureOpenSearchTest extends SampleDataFixtureTestBase { @Qualifier("sampleDataOperationContext") protected OperationContext operationContext; + @Getter + @Autowired + @Qualifier("fixtureCustomSearchConfig") + protected CustomSearchConfiguration customSearchConfiguration; + @Test public void initTest() { assertNotNull(searchClient); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SearchDAOOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SearchDAOOpenSearchTest.java index a3a767807d7b9d..1512e146948ac7 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SearchDAOOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SearchDAOOpenSearchTest.java @@ -1,8 +1,11 @@ package com.linkedin.metadata.search.opensearch; -import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.Assert.assertNotNull; import com.linkedin.metadata.config.search.SearchConfiguration; +import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; +import com.linkedin.metadata.search.elasticsearch.ElasticSearchService; +import com.linkedin.metadata.search.elasticsearch.query.ESSearchDAO; import com.linkedin.metadata.search.query.SearchDAOTestBase; import io.datahubproject.metadata.context.OperationContext; import io.datahubproject.test.fixtures.search.SampleDataFixtureConfiguration; @@ -28,6 +31,20 @@ public class SearchDAOOpenSearchTest extends SearchDAOTestBase { @Qualifier("sampleDataOperationContext") protected OperationContext operationContext; + @Autowired + @Qualifier("sampleDataEntitySearchService") + protected ElasticSearchService entitySearchService; + + @Getter + @Autowired + @Qualifier("fixtureCustomSearchConfig") + protected CustomSearchConfiguration customSearchConfiguration; + + @Override + protected ESSearchDAO getESSearchDao() { + return entitySearchService.getEsSearchDAO(); + } + @Test public void initTest() { assertNotNull(searchClient); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SearchServiceOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SearchServiceOpenSearchTest.java index 1127ba2089a91b..ab1137c94f2f43 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SearchServiceOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SearchServiceOpenSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.opensearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.search.SearchServiceTestBase; @@ -10,8 +12,8 @@ import org.jetbrains.annotations.NotNull; import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ @@ -25,7 +27,10 @@ public class SearchServiceOpenSearchTest extends SearchServiceTestBase { @Autowired private ESBulkProcessor _bulkProcessor; @Autowired private ESIndexBuilder _esIndexBuilder; @Autowired private SearchConfiguration _searchConfiguration; - @Autowired private CustomSearchConfiguration _customSearchConfiguration; + + @Autowired + @Qualifier("defaultTestCustomSearchConfig") + private CustomSearchConfiguration _customSearchConfiguration; @NotNull @Override @@ -51,14 +56,8 @@ protected SearchConfiguration getSearchConfiguration() { return _searchConfiguration; } - @NotNull - @Override - protected CustomSearchConfiguration getCustomSearchConfiguration() { - return _customSearchConfiguration; - } - @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SystemMetadataServiceOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SystemMetadataServiceOpenSearchTest.java index 7ba90319cf1d3e..46bf30fcedafb8 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SystemMetadataServiceOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/SystemMetadataServiceOpenSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.opensearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.search.elasticsearch.indexbuilder.ESIndexBuilder; import com.linkedin.metadata.search.elasticsearch.update.ESBulkProcessor; import com.linkedin.metadata.systemmetadata.SystemMetadataServiceTestBase; @@ -8,7 +10,6 @@ import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({OpenSearchSuite.class, SearchTestContainerConfiguration.class}) @@ -38,6 +39,6 @@ protected ESIndexBuilder getIndexBuilder() { @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/TestEntityOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/TestEntityOpenSearchTest.java index 80db8864014c32..96adf052cde456 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/TestEntityOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/TestEntityOpenSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.opensearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.search.TestEntityTestBase; @@ -10,8 +12,8 @@ import org.jetbrains.annotations.NotNull; import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({ @@ -25,7 +27,10 @@ public class TestEntityOpenSearchTest extends TestEntityTestBase { @Autowired private ESBulkProcessor _bulkProcessor; @Autowired private ESIndexBuilder _esIndexBuilder; @Autowired private SearchConfiguration _searchConfiguration; - @Autowired private CustomSearchConfiguration _customSearchConfiguration; + + @Autowired + @Qualifier("defaultTestCustomSearchConfig") + private CustomSearchConfiguration _customSearchConfiguration; @NotNull @Override @@ -51,14 +56,8 @@ protected SearchConfiguration getSearchConfiguration() { return _searchConfiguration; } - @NotNull - @Override - protected CustomSearchConfiguration getCustomSearchConfiguration() { - return _customSearchConfiguration; - } - @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/TimeseriesAspectServiceOpenSearchTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/TimeseriesAspectServiceOpenSearchTest.java index 16ac03415ee5c2..b60ba08d9785bf 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/TimeseriesAspectServiceOpenSearchTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/opensearch/TimeseriesAspectServiceOpenSearchTest.java @@ -1,5 +1,7 @@ package com.linkedin.metadata.search.opensearch; +import static org.testng.Assert.assertNotNull; + import com.linkedin.metadata.search.elasticsearch.indexbuilder.ESIndexBuilder; import com.linkedin.metadata.search.elasticsearch.update.ESBulkProcessor; import com.linkedin.metadata.timeseries.search.TimeseriesAspectServiceTestBase; @@ -9,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; -import org.testng.AssertJUnit; import org.testng.annotations.Test; @Import({OpenSearchSuite.class, SearchTestContainerConfiguration.class}) @@ -42,6 +43,6 @@ protected ESIndexBuilder getIndexBuilder() { @Test public void initTest() { - AssertJUnit.assertNotNull(_searchClient); + assertNotNull(_searchClient); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/BrowseDAOTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/BrowseDAOTest.java index 9c3d515f9322fb..e71865921678bb 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/BrowseDAOTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/BrowseDAOTest.java @@ -27,6 +27,7 @@ import org.opensearch.search.SearchHit; import org.opensearch.search.SearchHits; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.annotations.BeforeMethod; @@ -39,7 +40,10 @@ public class BrowseDAOTest extends AbstractTestNGSpringContextTests { private OperationContext opContext; @Autowired private SearchConfiguration searchConfiguration; - @Autowired private CustomSearchConfiguration customSearchConfiguration; + + @Autowired + @Qualifier("defaultTestCustomSearchConfig") + private CustomSearchConfiguration customSearchConfiguration; @BeforeMethod public void setup() throws RemoteInvocationException, URISyntaxException { diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/SearchDAOTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/SearchDAOTestBase.java index eafe5c7b5c3103..6779b8f3d825c4 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/SearchDAOTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/SearchDAOTestBase.java @@ -1,6 +1,8 @@ package com.linkedin.metadata.search.query; import static com.linkedin.metadata.Constants.*; +import static com.linkedin.metadata.search.fixtures.SampleDataFixtureTestBase.DEFAULT_CONFIG; +import static com.linkedin.metadata.search.fixtures.SampleDataFixtureTestBase.MAPPER; import static com.linkedin.metadata.utils.CriterionUtils.buildCriterion; import static com.linkedin.metadata.utils.SearchUtil.AGGREGATION_SEPARATOR_CHAR; import static com.linkedin.metadata.utils.SearchUtil.ES_INDEX_FIELD; @@ -12,6 +14,7 @@ import com.linkedin.data.template.LongMap; import com.linkedin.metadata.config.search.SearchConfiguration; +import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.query.filter.Condition; import com.linkedin.metadata.query.filter.ConjunctiveCriterion; import com.linkedin.metadata.query.filter.ConjunctiveCriterionArray; @@ -27,15 +30,17 @@ import com.linkedin.metadata.search.elasticsearch.ElasticSearchService; import com.linkedin.metadata.search.elasticsearch.query.ESSearchDAO; import com.linkedin.metadata.search.elasticsearch.query.filter.QueryFilterRewriteChain; -import com.linkedin.metadata.search.opensearch.SearchDAOOpenSearchTest; import com.linkedin.metadata.utils.SearchUtil; import io.datahubproject.metadata.context.OperationContext; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.opensearch.action.explain.ExplainResponse; import org.opensearch.client.RestHighLevelClient; +import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.annotations.Test; @@ -47,6 +52,10 @@ public abstract class SearchDAOTestBase extends AbstractTestNGSpringContextTests protected abstract OperationContext getOperationContext(); + protected abstract ESSearchDAO getESSearchDao(); + + protected abstract CustomSearchConfiguration getCustomSearchConfiguration(); + @Test public void testTransformFilterForEntitiesNoChange() { Criterion c = @@ -413,30 +422,21 @@ public void testTransformIndexIntoEntityNameNested() { @Test public void testExplain() { - ESSearchDAO searchDAO = - new ESSearchDAO( - getSearchClient(), - false, - this instanceof SearchDAOOpenSearchTest - ? ELASTICSEARCH_IMPLEMENTATION_OPENSEARCH - : ELASTICSEARCH_IMPLEMENTATION_ELASTICSEARCH, - getSearchConfiguration(), - null, - QueryFilterRewriteChain.EMPTY); ExplainResponse explainResponse = - searchDAO.explain( - getOperationContext() - .withSearchFlags(flags -> ElasticSearchService.DEFAULT_SERVICE_SEARCH_FLAGS), - "*", - "urn:li:dataset:(urn:li:dataPlatform:bigquery,bigquery-public-data.covid19_geotab_mobility_impact." - + "ca_border_wait_times,PROD)", - DATASET_ENTITY_NAME, - null, - null, - null, - null, - 10, - null); + getESSearchDao() + .explain( + getOperationContext() + .withSearchFlags(flags -> ElasticSearchService.DEFAULT_SERVICE_SEARCH_FLAGS), + "*", + "urn:li:dataset:(urn:li:dataPlatform:bigquery,bigquery-public-data.covid19_geotab_mobility_impact." + + "ca_border_wait_times,PROD)", + DATASET_ENTITY_NAME, + null, + null, + null, + null, + 10, + null); assertNotNull(explainResponse); assertEquals(explainResponse.getIndex(), "smpldat_datasetindex_v2"); @@ -444,6 +444,34 @@ public void testExplain() { explainResponse.getId(), "urn:li:dataset:(urn:li:dataPlatform:bigquery,bigquery-public-data.covid19_geotab_mobility_impact.ca_border_wait_times,PROD)"); assertTrue(explainResponse.isExists()); - assertEquals(explainResponse.getExplanation().getValue(), 18.0f); + assertEquals(explainResponse.getExplanation().getValue(), 1.25f); + } + + /** + * Ensure default search configuration matches the test fixture configuration (allowing for some + * differences) + */ + @Test + public void testConfig() throws IOException { + final CustomSearchConfiguration defaultConfig; + try (InputStream stream = new ClassPathResource(DEFAULT_CONFIG).getInputStream()) { + defaultConfig = MAPPER.readValue(stream, CustomSearchConfiguration.class); + } + + final CustomSearchConfiguration fixtureConfig = + MAPPER.readValue( + MAPPER.writeValueAsBytes(getCustomSearchConfiguration()), + CustomSearchConfiguration.class); + + // test specifics + ((List>) + fixtureConfig.getQueryConfigurations().get(1).getFunctionScore().get("functions")) + .remove(1); + + ((List>) + fixtureConfig.getQueryConfigurations().get(2).getFunctionScore().get("functions")) + .remove(1); + + assertEquals(fixtureConfig, defaultConfig); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/AggregationQueryBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/AggregationQueryBuilderTest.java index 0ea2340ae82173..1381e9560b7e53 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/AggregationQueryBuilderTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/AggregationQueryBuilderTest.java @@ -41,7 +41,7 @@ public class AggregationQueryBuilderTest { private static AspectRetriever aspectRetrieverV1; @BeforeClass - public static void setup() throws RemoteInvocationException, URISyntaxException { + public void setup() throws RemoteInvocationException, URISyntaxException { Urn helloUrn = Urn.createFromString("urn:li:structuredProperty:hello"); Urn abFghTenUrn = Urn.createFromString("urn:li:structuredProperty:ab.fgh.ten"); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchQueryBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchQueryBuilderTest.java index 8d83317449a1ea..374a69ee9a5536 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchQueryBuilderTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchQueryBuilderTest.java @@ -61,6 +61,10 @@ public class SearchQueryBuilderTest extends AbstractTestNGSpringContextTests { @Qualifier("queryOperationContext") private OperationContext operationContext; + @Autowired + @Qualifier("defaultTestCustomSearchConfig") + private CustomSearchConfiguration customSearchConfiguration; + public static SearchConfiguration testQueryConfig; static { diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java index a90c0291f53b8f..a3ef62760d7972 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java @@ -1,5 +1,6 @@ package com.linkedin.metadata.search.query.request; +import static com.linkedin.datahub.graphql.resolvers.search.SearchUtils.SEARCHABLE_ENTITY_TYPES; import static com.linkedin.metadata.utils.CriterionUtils.buildCriterion; import static com.linkedin.metadata.utils.CriterionUtils.buildExistsCriterion; import static com.linkedin.metadata.utils.CriterionUtils.buildIsNullCriterion; @@ -8,7 +9,10 @@ import static org.testng.Assert.*; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.linkedin.data.template.StringArray; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.types.entitytype.EntityTypeMapper; import com.linkedin.metadata.TestEntitySpecBuilder; import com.linkedin.metadata.config.search.ExactMatchConfiguration; import com.linkedin.metadata.config.search.PartialConfiguration; @@ -35,6 +39,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.opensearch.action.search.SearchRequest; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.ExistsQueryBuilder; @@ -628,6 +633,116 @@ public void testBrowsePathQueryFilter() { assertEquals(((ExistsQueryBuilder) mustHaveV1.must().get(0)).fieldName(), "browsePaths"); } + @Test + public void testQueryByDefault() { + final Set COMMON = + Set.of( + "container", + "fieldDescriptions", + "description", + "platform", + "fieldPaths", + "editedFieldGlossaryTerms", + "editedFieldDescriptions", + "fieldTags", + "id", + "editedDescription", + "qualifiedName", + "domains", + "platformInstance", + "tags", + "urn", + "customProperties", + "fieldGlossaryTerms", + "editedName", + "name", + "fieldLabels", + "glossaryTerms", + "editedFieldTags", + "displayName", + "title"); + + Map> expectedQueryByDefault = + ImmutableMap.>builder() + .put( + EntityType.DASHBOARD, + Stream.concat(COMMON.stream(), Stream.of("tool")).collect(Collectors.toSet())) + .put( + EntityType.CHART, + Stream.concat(COMMON.stream(), Stream.of("tool")).collect(Collectors.toSet())) + .put( + EntityType.MLMODEL, + Stream.concat(COMMON.stream(), Stream.of("type")).collect(Collectors.toSet())) + .put( + EntityType.MLFEATURE_TABLE, + Stream.concat(COMMON.stream(), Stream.of("features", "primaryKeys")) + .collect(Collectors.toSet())) + .put( + EntityType.MLFEATURE, + Stream.concat(COMMON.stream(), Stream.of("featureNamespace")) + .collect(Collectors.toSet())) + .put( + EntityType.MLPRIMARY_KEY, + Stream.concat(COMMON.stream(), Stream.of("featureNamespace")) + .collect(Collectors.toSet())) + .put( + EntityType.DATA_FLOW, + Stream.concat(COMMON.stream(), Stream.of("cluster", "orchestrator", "flowId")) + .collect(Collectors.toSet())) + .put( + EntityType.DATA_JOB, + Stream.concat(COMMON.stream(), Stream.of("jobId")).collect(Collectors.toSet())) + .put( + EntityType.GLOSSARY_TERM, + Stream.concat( + COMMON.stream(), + Stream.of("values", "parentNode", "relatedTerms", "definition")) + .collect(Collectors.toSet())) + .put( + EntityType.GLOSSARY_NODE, + Stream.concat(COMMON.stream(), Stream.of("definition", "parentNode")) + .collect(Collectors.toSet())) + .put( + EntityType.CORP_USER, + Stream.concat( + COMMON.stream(), Stream.of("skills", "teams", "ldap", "fullName", "email")) + .collect(Collectors.toSet())) + .put( + EntityType.DOMAIN, + Stream.concat(COMMON.stream(), Stream.of("parentDomain")) + .collect(Collectors.toSet())) + .put( + EntityType.SCHEMA_FIELD, + Stream.concat(COMMON.stream(), Stream.of("schemaFieldAliases", "parent")) + .collect(Collectors.toSet())) + .build(); + + for (EntityType entityType : SEARCHABLE_ENTITY_TYPES) { + Set expectedEntityQueryByDefault = + expectedQueryByDefault.getOrDefault(entityType, COMMON); + assertFalse(expectedEntityQueryByDefault.isEmpty()); + + EntitySpec entitySpec = + operationContext.getEntityRegistry().getEntitySpec(EntityTypeMapper.getName(entityType)); + SearchRequestHandler handler = + SearchRequestHandler.getBuilder( + operationContext.getEntityRegistry(), + entitySpec, + testQueryConfig, + null, + QueryFilterRewriteChain.EMPTY); + + Set unexpected = new HashSet<>(handler.getDefaultQueryFieldNames()); + unexpected.removeAll(expectedEntityQueryByDefault); + + assertTrue( + unexpected.isEmpty(), + String.format( + "Consider whether these field(s) for entity %s should be included for general search. Fields: %s If yes, please update the test expectations. If no, please annotate the PDL model with \"queryByDefault\": false", + entityType, unexpected)); + } + } + private BoolQueryBuilder getQuery(final Criterion filterCriterion) { final Filter filter = new Filter() diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java index c5f9986284627d..03d104b9e7bfb3 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java @@ -38,7 +38,7 @@ public class ESUtilsTest { private static AspectRetriever aspectRetrieverV1; @BeforeClass - public static void setup() throws RemoteInvocationException, URISyntaxException { + public void setup() throws RemoteInvocationException, URISyntaxException { Urn abFghTenUrn = Urn.createFromString("urn:li:structuredProperty:ab.fgh.ten"); // legacy diff --git a/metadata-io/src/test/java/com/linkedin/metadata/structuredproperties/validators/PropertyDefinitionValidatorTest.java b/metadata-io/src/test/java/com/linkedin/metadata/structuredproperties/validators/PropertyDefinitionValidatorTest.java index 22224f16f2210b..2af731a51145e3 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/structuredproperties/validators/PropertyDefinitionValidatorTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/structuredproperties/validators/PropertyDefinitionValidatorTest.java @@ -2,7 +2,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.testng.AssertJUnit.assertEquals; +import static org.testng.Assert.assertEquals; import com.linkedin.common.UrnArray; import com.linkedin.common.urn.Urn; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java b/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java index d8d33f4c356bb8..772ef374af18b0 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java @@ -1,6 +1,6 @@ package com.linkedin.metadata.timeline.eventgenerator; -import static org.testng.AssertJUnit.assertEquals; +import static org.testng.Assert.assertEquals; import com.linkedin.common.AuditStamp; import com.linkedin.common.urn.Urn; diff --git a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java index 781201f3478f98..e47cdf80281c9a 100644 --- a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java +++ b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java @@ -6,12 +6,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.client.JavaEntityClient; import com.linkedin.metadata.config.PreProcessHooks; import com.linkedin.metadata.config.cache.EntityDocCountCacheConfiguration; -import com.linkedin.metadata.config.search.CustomConfiguration; import com.linkedin.metadata.config.search.ElasticSearchConfiguration; import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; @@ -73,7 +71,9 @@ public class SampleDataFixtureConfiguration { @Autowired private SearchConfiguration _searchConfiguration; - @Autowired private CustomSearchConfiguration _customSearchConfiguration; + @Autowired + @Qualifier("fixtureCustomSearchConfig") + private CustomSearchConfiguration _customSearchConfiguration; @Autowired private QueryFilterRewriteChain queryFilterRewriteChain; @@ -188,11 +188,6 @@ protected ElasticSearchService longTailEntitySearchService( protected ElasticSearchService entitySearchServiceHelper(EntityIndexBuilders indexBuilders) throws IOException { - CustomConfiguration customConfiguration = new CustomConfiguration(); - customConfiguration.setEnabled(true); - customConfiguration.setFile("search_config_fixture_test.yml"); - CustomSearchConfiguration customSearchConfiguration = - customConfiguration.resolve(new YAMLMapper()); ESSearchDAO searchDAO = new ESSearchDAO( @@ -200,8 +195,9 @@ protected ElasticSearchService entitySearchServiceHelper(EntityIndexBuilders ind false, ELASTICSEARCH_IMPLEMENTATION_ELASTICSEARCH, _searchConfiguration, - customSearchConfiguration, - queryFilterRewriteChain); + _customSearchConfiguration, + queryFilterRewriteChain, + true); ESBrowseDAO browseDAO = new ESBrowseDAO( _searchClient, diff --git a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java index a7603f97792e70..889473d32d1a35 100644 --- a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java +++ b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java @@ -67,7 +67,9 @@ public class SearchLineageFixtureConfiguration { @Autowired private SearchConfiguration searchConfiguration; - @Autowired private CustomSearchConfiguration customSearchConfiguration; + @Autowired + @Qualifier("fixtureCustomSearchConfig") + private CustomSearchConfiguration customSearchConfiguration; @Bean(name = "searchLineagePrefix") protected String indexPrefix() { @@ -141,7 +143,7 @@ protected ElasticSearchService entitySearchService( false, ELASTICSEARCH_IMPLEMENTATION_ELASTICSEARCH, searchConfiguration, - null, + customSearchConfiguration, queryFilterRewriteChain); ESBrowseDAO browseDAO = new ESBrowseDAO( diff --git a/metadata-io/src/test/java/io/datahubproject/test/search/config/SearchCommonTestConfiguration.java b/metadata-io/src/test/java/io/datahubproject/test/search/config/SearchCommonTestConfiguration.java index 547ab1d746dbe7..e84ecf677e3a6d 100644 --- a/metadata-io/src/test/java/io/datahubproject/test/search/config/SearchCommonTestConfiguration.java +++ b/metadata-io/src/test/java/io/datahubproject/test/search/config/SearchCommonTestConfiguration.java @@ -44,14 +44,22 @@ public SearchConfiguration searchConfiguration() { return searchConfiguration; } - @Bean - public CustomSearchConfiguration customSearchConfiguration() throws Exception { + @Bean("defaultTestCustomSearchConfig") + public CustomSearchConfiguration defaultTestCustomSearchConfig() throws Exception { CustomConfiguration customConfiguration = new CustomConfiguration(); customConfiguration.setEnabled(true); customConfiguration.setFile("search_config_builder_test.yml"); return customConfiguration.resolve(new YAMLMapper()); } + @Bean("fixtureCustomSearchConfig") + public CustomSearchConfiguration fixtureCustomSearchConfig() throws Exception { + CustomConfiguration customConfiguration = new CustomConfiguration(); + customConfiguration.setEnabled(true); + customConfiguration.setFile("search_config_fixture_test.yml"); + return customConfiguration.resolve(new YAMLMapper()); + } + @Bean(name = "queryOperationContext") public OperationContext queryOperationContext() { return TestOperationContexts.systemContextNoSearchAuthorization(); diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/DocumentationAssociation.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/DocumentationAssociation.pdl index 19404346797bb0..bee331f15b6797 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/DocumentationAssociation.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/DocumentationAssociation.pdl @@ -15,15 +15,18 @@ record DocumentationAssociation { @Searchable = { "/time": { "fieldName": "documentationAttributionDates", - "fieldType": "DATETIME" + "fieldType": "DATETIME", + "queryByDefault": false, }, "/actor": { "fieldName": "documentationAttributionActors", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, "/source": { "fieldName": "documentationAttributionSources", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, } attribution: optional MetadataAttribution diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/Forms.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/Forms.pdl index 0a97c7d5099ed8..3c05c00fd6fb97 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/Forms.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/Forms.pdl @@ -13,19 +13,23 @@ record Forms { @Searchable = { "/*/urn": { "fieldType": "URN", - "fieldName": "incompleteForms" + "fieldName": "incompleteForms", + "queryByDefault": false, }, "/*/completedPrompts/*/id" : { "fieldType": "KEYWORD", "fieldName": "incompleteFormsCompletedPromptIds", + "queryByDefault": false, }, "/*/incompletePrompts/*/id" : { "fieldType": "KEYWORD", "fieldName": "incompleteFormsIncompletePromptIds", + "queryByDefault": false, }, "/*/completedPrompts/*/lastModified/time" : { "fieldType": "DATETIME", "fieldName": "incompleteFormsCompletedPromptResponseTimes", + "queryByDefault": false, } } incompleteForms: array[FormAssociation] @@ -36,19 +40,23 @@ record Forms { @Searchable = { "/*/urn": { "fieldType": "URN", - "fieldName": "completedForms" + "fieldName": "completedForms", + "queryByDefault": false }, "/*/completedPrompts/*/id" : { "fieldType": "KEYWORD", "fieldName": "completedFormsCompletedPromptIds", + "queryByDefault": false, }, "/*/incompletePrompts/*/id" : { "fieldType": "KEYWORD", "fieldName": "completedFormsIncompletePromptIds", + "queryByDefault": false, }, "/*/completedPrompts/*/lastModified/time" : { "fieldType": "DATETIME", "fieldName": "completedFormsCompletedPromptResponseTimes", + "queryByDefault": false, } } completedForms: array[FormAssociation] @@ -59,7 +67,8 @@ record Forms { @Searchable = { "/*/form": { "fieldType": "URN", - "fieldName": "verifiedForms" + "fieldName": "verifiedForms", + "queryByDefault": false, } } verifications: array[FormVerificationAssociation] = [] diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/GlossaryTermAssociation.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/GlossaryTermAssociation.pdl index a5267bbc635e43..58423ccc2228db 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/GlossaryTermAssociation.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/GlossaryTermAssociation.pdl @@ -36,15 +36,18 @@ record GlossaryTermAssociation { @Searchable = { "/time": { "fieldName": "termAttributionDates", - "fieldType": "DATETIME" + "fieldType": "DATETIME", + "queryByDefault": false, }, "/actor": { "fieldName": "termAttributionActors", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, "/source": { "fieldName": "termAttributionSources", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, } attribution: optional MetadataAttribution diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/IncidentsSummary.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/IncidentsSummary.pdl index e1367a326e24bb..9e4e81656f7c9a 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/IncidentsSummary.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/IncidentsSummary.pdl @@ -29,7 +29,8 @@ record IncidentsSummary { "fieldType": "URN", "fieldName": "resolvedIncidents", "hasValuesFieldName": "hasResolvedIncidents", - "numValuesFieldName": "numResolvedIncidents" + "numValuesFieldName": "numResolvedIncidents", + "queryByDefault": false, }, "/*/type" : { "fieldType": "KEYWORD", @@ -65,7 +66,8 @@ record IncidentsSummary { "fieldName": "activeIncidents", "hasValuesFieldName": "hasActiveIncidents", "numValuesFieldName": "numActiveIncidents", - "addHasValuesToFilters": true + "addHasValuesToFilters": true, + "queryByDefault": false, }, "/*/type" : { "fieldType": "KEYWORD", diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/RoleAssociation.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/RoleAssociation.pdl index ddd63ed64014b0..05c46dfdf69d76 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/RoleAssociation.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/RoleAssociation.pdl @@ -18,7 +18,8 @@ record RoleAssociation { "fieldType": "URN", "hasValuesFieldName": "hasRoles", "addToFilters": true, - "filterNameOverride": "Role" + "filterNameOverride": "Role", + "queryByDefault": false, } urn: Urn } \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/SubTypes.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/SubTypes.pdl index a1063afe1eae94..1f2ff275c7ae9f 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/SubTypes.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/SubTypes.pdl @@ -17,7 +17,7 @@ record SubTypes { "fieldType": "KEYWORD", "addToFilters": true, "filterNameOverride": "Sub Type", - "queryByDefault": true + "queryByDefault": false, } } typeNames: array[string] diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/TagAssociation.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/TagAssociation.pdl index 8a58ca97de1956..caed4961272ded 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/TagAssociation.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/TagAssociation.pdl @@ -21,15 +21,18 @@ record TagAssociation { @Searchable = { "/time": { "fieldName": "tagAttributionDates", - "fieldType": "DATETIME" + "fieldType": "DATETIME", + "queryByDefault": false, }, "/actor": { "fieldName": "tagAttributionActors", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, "/source": { "fieldName": "tagAttributionSources", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, } attribution: optional MetadataAttribution diff --git a/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryRelatedTerms.pdl b/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryRelatedTerms.pdl index 5e10219235347a..9f5f312a6bccec 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryRelatedTerms.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryRelatedTerms.pdl @@ -24,7 +24,7 @@ record GlossaryRelatedTerms { "/*": { "fieldName": "isRelatedTerms", "fieldType": "URN", - "boostScore": 2.0 + "queryByDefault": false, } } isRelatedTerms: optional array[GlossaryTermUrn] @@ -42,7 +42,7 @@ record GlossaryRelatedTerms { "/*": { "fieldName": "hasRelatedTerms", "fieldType": "URN", - "boostScore": 2.0 + "queryByDefault": false, } } hasRelatedTerms: optional array[GlossaryTermUrn] diff --git a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserInfo.pdl index 382b120fa942a7..53c31daeca4373 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserInfo.pdl @@ -62,7 +62,7 @@ record CorpUserInfo includes CustomProperties { @Searchable = { "fieldName": "managerLdap", "fieldType": "URN", - "queryByDefault": true + "queryByDefault": false, } managerUrn: optional CorpuserUrn diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl index 816277bd1e0c96..048c2dcd9f58f6 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl @@ -43,11 +43,13 @@ record EditableSchemaFieldInfo { }, "/tags/*/attribution/actor": { "fieldName": "editedFieldTagAttributionActors", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, "/tags/*/attribution/source": { "fieldName": "editedFieldTagAttributionSources", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, } globalTags: optional GlobalTags @@ -73,11 +75,13 @@ record EditableSchemaFieldInfo { }, "/terms/*/attribution/actor": { "fieldName": "editedFieldTermAttributionActors", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, "/terms/*/attribution/source": { "fieldName": "editedFieldTermAttributionSources", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, } glossaryTerms: optional GlossaryTerms diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/SchemaField.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/SchemaField.pdl index f91e2004401cf9..0b72d376b0be49 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/schema/SchemaField.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/schema/SchemaField.pdl @@ -99,15 +99,18 @@ record SchemaField { }, "/tags/*/attribution/time": { "fieldName": "fieldTagAttributionDates", - "fieldType": "DATETIME" + "fieldType": "DATETIME", + "queryByDefault": false, }, "/tags/*/attribution/actor": { "fieldName": "fieldTagAttributionActors", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, "/tags/*/attribution/source": { "fieldName": "fieldTagAttributionSources", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, } globalTags: optional GlobalTags @@ -129,15 +132,18 @@ record SchemaField { }, "/terms/*/attribution/time": { "fieldName": "fieldTermAttributionDates", - "fieldType": "DATETIME" + "fieldType": "DATETIME", + "queryByDefault": false, }, "/terms/*/attribution/actor": { "fieldName": "fieldTermAttributionActors", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, "/terms/*/attribution/source": { "fieldName": "fieldTermAttributionSources", - "fieldType": "URN" + "fieldType": "URN", + "queryByDefault": false, }, } glossaryTerms: optional GlossaryTerms diff --git a/metadata-models/src/main/pegasus/com/linkedin/test/TestResults.pdl b/metadata-models/src/main/pegasus/com/linkedin/test/TestResults.pdl index 6f210abf0597f4..c8a99faef88a0d 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/test/TestResults.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/test/TestResults.pdl @@ -15,6 +15,7 @@ record TestResults { "fieldType": "URN", "fieldName": "failingTests" "hasValuesFieldName": "hasFailingTests", + "queryByDefault": false, } } @Relationship = { @@ -33,6 +34,7 @@ record TestResults { "fieldType": "URN", "fieldName": "passingTests", "hasValuesFieldName": "hasPassingTests", + "queryByDefault": false, } } @Relationship = { From fcd6365af3ca1b4cc5a199515f852dec524c95ca Mon Sep 17 00:00:00 2001 From: david-leifker <114954101+david-leifker@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:55:32 -0500 Subject: [PATCH 06/25] fix(search): Fix empty description (#11514) --- .../SearchDocumentTransformer.java | 11 ++- .../SearchDocumentTransformerTest.java | 81 ++++++++++++++----- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java index b6d9357ecd65e8..4bb8e0630de480 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java @@ -210,8 +210,13 @@ public void setSearchableValue( fieldName, JsonNodeFactory.instance.booleanNode((Boolean) firstValue.orElse(false))); } else { - searchDocument.set( - fieldName, JsonNodeFactory.instance.booleanNode(!fieldValues.isEmpty())); + final boolean hasValue; + if (DataSchema.Type.STRING.equals(valueType)) { + hasValue = firstValue.isPresent() && !String.valueOf(firstValue.get()).isEmpty(); + } else { + hasValue = !fieldValues.isEmpty(); + } + searchDocument.set(fieldName, JsonNodeFactory.instance.booleanNode(hasValue)); } }); @@ -390,7 +395,7 @@ private Optional getNodeForValue( default: String value = fieldValue.toString(); return value.isEmpty() - ? Optional.empty() + ? Optional.of(JsonNodeFactory.instance.nullNode()) : Optional.of(JsonNodeFactory.instance.textNode(value)); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformerTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformerTest.java index ef000b01a64e55..2c5bcd1294fa15 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformerTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformerTest.java @@ -15,7 +15,10 @@ import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; import com.linkedin.data.DataMapBuilder; +import com.linkedin.dataset.DatasetProperties; +import com.linkedin.dataset.EditableDatasetProperties; import com.linkedin.entity.Aspect; import com.linkedin.metadata.TestEntitySpecBuilder; import com.linkedin.metadata.TestEntityUtil; @@ -39,6 +42,17 @@ public class SearchDocumentTransformerTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final EntityRegistry ENTITY_REGISTRY = + TestOperationContexts.defaultEntityRegistry(); + private static final EntityRegistry TEST_ENTITY_REGISTRY; + + static { + TEST_ENTITY_REGISTRY = + new ConfigEntityRegistry( + TestSearchFieldConfig.class + .getClassLoader() + .getResourceAsStream("test-entity-registry.yaml")); + } static { int maxSize = @@ -214,7 +228,6 @@ public void testSetSearchableRefValue() throws URISyntaxException, RemoteInvocat SearchDocumentTransformer searchDocumentTransformer = new SearchDocumentTransformer(1000, 1000, 1000); - EntityRegistry entityRegistry = getTestEntityRegistry(); List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); DataMapBuilder dataMapBuilder = new DataMapBuilder(); @@ -225,10 +238,10 @@ public void testSetSearchableRefValue() throws URISyntaxException, RemoteInvocat ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); SearchableRefFieldSpec searchableRefFieldSpec = - entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + TEST_ENTITY_REGISTRY.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); // Mock Behaviour - Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(entityRegistry); + Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(TEST_ENTITY_REGISTRY); Mockito.when(aspectRetriever.getLatestAspectObject(any(), anyString())).thenReturn(aspect); OperationContext opContext = TestOperationContexts.systemContextNoSearchAuthorization( @@ -258,14 +271,13 @@ public void testSetSearchableRefValue_WithNonURNField() throws URISyntaxExceptio SearchDocumentTransformer searchDocumentTransformer = new SearchDocumentTransformer(1000, 1000, 1000); - EntityRegistry entityRegistry = getTestEntityRegistry(); OperationContext opContext = - TestOperationContexts.systemContextNoSearchAuthorization(entityRegistry); + TestOperationContexts.systemContextNoSearchAuthorization(TEST_ENTITY_REGISTRY); List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); SearchableRefFieldSpec searchableRefFieldSpecText = - entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(1); + TEST_ENTITY_REGISTRY.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(1); searchDocumentTransformer.setSearchableRefValue( opContext, searchableRefFieldSpecText, urnList, searchDocument, false); assertTrue(searchDocument.isEmpty()); @@ -278,10 +290,9 @@ public void testSetSearchableRefValue_RuntimeException() SearchDocumentTransformer searchDocumentTransformer = new SearchDocumentTransformer(1000, 1000, 1000); - EntityRegistry entityRegistry = getTestEntityRegistry(); List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); - Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(entityRegistry); + Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(TEST_ENTITY_REGISTRY); Mockito.when( aspectRetriever.getLatestAspectObject( eq(Urn.createFromString("urn:li:refEntity:1")), anyString())) @@ -296,7 +307,7 @@ public void testSetSearchableRefValue_RuntimeException() ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); SearchableRefFieldSpec searchableRefFieldSpec = - entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + TEST_ENTITY_REGISTRY.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); searchDocumentTransformer.setSearchableRefValue( opContext, searchableRefFieldSpec, urnList, searchDocument, false); assertTrue(searchDocument.isEmpty()); @@ -309,7 +320,6 @@ public void testSetSearchableRefValue_RuntimeException_URNExist() SearchDocumentTransformer searchDocumentTransformer = new SearchDocumentTransformer(1000, 1000, 1000); - EntityRegistry entityRegistry = getTestEntityRegistry(); List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); DataMapBuilder dataMapBuilder = new DataMapBuilder(); dataMapBuilder.addKVPair("fieldPath", "refEntityUrn"); @@ -317,7 +327,7 @@ public void testSetSearchableRefValue_RuntimeException_URNExist() dataMapBuilder.addKVPair("description", "refEntityUrn1 description details"); Aspect aspect = new Aspect(dataMapBuilder.convertToDataMap()); - Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(entityRegistry); + Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(TEST_ENTITY_REGISTRY); Mockito.when( aspectRetriever.getLatestAspectObject( eq(Urn.createFromString("urn:li:refEntity:1")), anyString())) @@ -333,7 +343,7 @@ public void testSetSearchableRefValue_RuntimeException_URNExist() ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); SearchableRefFieldSpec searchableRefFieldSpec = - entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + TEST_ENTITY_REGISTRY.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); searchDocumentTransformer.setSearchableRefValue( opContext, searchableRefFieldSpec, urnList, searchDocument, false); assertTrue(searchDocument.has("refEntityUrns")); @@ -349,13 +359,12 @@ void testSetSearchableRefValue_WithInvalidURN() SearchDocumentTransformer searchDocumentTransformer = new SearchDocumentTransformer(1000, 1000, 1000); - EntityRegistry entityRegistry = getTestEntityRegistry(); List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); - Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(entityRegistry); + Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(TEST_ENTITY_REGISTRY); Mockito.when(aspectRetriever.getLatestAspectObject(any(), anyString())).thenReturn(null); SearchableRefFieldSpec searchableRefFieldSpec = - entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + TEST_ENTITY_REGISTRY.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); OperationContext opContext = TestOperationContexts.systemContextNoSearchAuthorization( RetrieverContext.builder() @@ -371,10 +380,42 @@ void testSetSearchableRefValue_WithInvalidURN() assertTrue(searchDocument.get("refEntityUrns").getNodeType().equals(JsonNodeType.NULL)); } - private EntityRegistry getTestEntityRegistry() { - return new ConfigEntityRegistry( - TestSearchFieldConfig.class - .getClassLoader() - .getResourceAsStream("test-entity-registry.yaml")); + @Test + public void testEmptyDescription() throws RemoteInvocationException, URISyntaxException { + String entityUrn = "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD)"; + SearchDocumentTransformer test = new SearchDocumentTransformer(1000, 1000, 1000); + + // editedDescription - empty string + Optional transformed = + test.transformAspect( + mock(OperationContext.class), + UrnUtils.getUrn(entityUrn), + new EditableDatasetProperties().setDescription(""), + ENTITY_REGISTRY + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(EDITABLE_DATASET_PROPERTIES_ASPECT_NAME), + false); + + assertTrue(transformed.isPresent()); + assertEquals(transformed.get().get("urn").asText(), entityUrn); + assertTrue(transformed.get().has("editedDescription")); + assertTrue(transformed.get().get("editedDescription").isNull()); + + // description - empty string + transformed = + test.transformAspect( + mock(OperationContext.class), + UrnUtils.getUrn(entityUrn), + new DatasetProperties().setDescription(""), + ENTITY_REGISTRY + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(DATASET_PROPERTIES_ASPECT_NAME), + false); + + assertTrue(transformed.isPresent()); + assertEquals(transformed.get().get("urn").asText(), entityUrn); + assertTrue(transformed.get().has("description")); + assertTrue(transformed.get().get("description").isNull()); + assertFalse(transformed.get().get("hasDescription").asBoolean()); } } From 73d8a4649dd981d655715616e7d025087ec51965 Mon Sep 17 00:00:00 2001 From: udays-visa <162687523+udays-visa@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:14:10 +0530 Subject: [PATCH 07/25] Block http trace method on mce and mce consumers (#11513) --- .../kafka/CustomDispatcherServlet.java | 18 ++++++++++++++++++ .../kafka/CustomDispatcherServlet.java | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 metadata-jobs/mae-consumer-job/src/main/java/com/linkedin/metadata/kafka/CustomDispatcherServlet.java create mode 100644 metadata-jobs/mce-consumer-job/src/main/java/com/linkedin/metadata/kafka/CustomDispatcherServlet.java diff --git a/metadata-jobs/mae-consumer-job/src/main/java/com/linkedin/metadata/kafka/CustomDispatcherServlet.java b/metadata-jobs/mae-consumer-job/src/main/java/com/linkedin/metadata/kafka/CustomDispatcherServlet.java new file mode 100644 index 00000000000000..eea3087dce1212 --- /dev/null +++ b/metadata-jobs/mae-consumer-job/src/main/java/com/linkedin/metadata/kafka/CustomDispatcherServlet.java @@ -0,0 +1,18 @@ +package com.linkedin.metadata.kafka; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.DispatcherServlet; + +@Component("dispatcherServlet") +public class CustomDispatcherServlet extends DispatcherServlet { + + @Override + protected void doTrace(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } +} diff --git a/metadata-jobs/mce-consumer-job/src/main/java/com/linkedin/metadata/kafka/CustomDispatcherServlet.java b/metadata-jobs/mce-consumer-job/src/main/java/com/linkedin/metadata/kafka/CustomDispatcherServlet.java new file mode 100644 index 00000000000000..eea3087dce1212 --- /dev/null +++ b/metadata-jobs/mce-consumer-job/src/main/java/com/linkedin/metadata/kafka/CustomDispatcherServlet.java @@ -0,0 +1,18 @@ +package com.linkedin.metadata.kafka; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.DispatcherServlet; + +@Component("dispatcherServlet") +public class CustomDispatcherServlet extends DispatcherServlet { + + @Override + protected void doTrace(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } +} From 04349cb9cd4a59a4865c5fa8f414c9ee813afd2a Mon Sep 17 00:00:00 2001 From: david-leifker <114954101+david-leifker@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:57:42 -0500 Subject: [PATCH 08/25] feat(bootstrap): bootstrap template mcps (#11518) --- build.gradle | 1 + datahub-upgrade/build.gradle | 2 + .../datahub/upgrade/UpgradeManager.java | 2 +- .../upgrade/config/BootstrapMCPConfig.java | 30 + .../upgrade/config/SystemUpdateConfig.java | 24 +- .../upgrade/impl/DefaultUpgradeContext.java | 2 +- .../upgrade/impl/DefaultUpgradeManager.java | 3 +- .../datahub/upgrade/system/SystemUpdate.java | 17 +- .../upgrade/system/SystemUpdateBlocking.java | 8 +- .../system/SystemUpdateNonBlocking.java | 8 +- .../system/bootstrapmcps/BootstrapMCP.java | 38 + .../bootstrapmcps/BootstrapMCPStep.java | 95 +++ .../bootstrapmcps/BootstrapMCPUtil.java | 183 +++++ .../model/BootstrapMCPConfigFile.java | 40 + .../upgrade/DatahubUpgradeBlockingTest.java | 48 ++ .../DatahubUpgradeNonBlockingTest.java | 28 +- .../bootstrapmcps/BootstrapMCPUtilTest.java | 224 ++++++ .../system/bootstrapmcps/DataTypesTest.java | 79 ++ .../bootstrapmcp/datahub-test-mcp.yaml | 29 + .../src/test/resources/bootstrapmcp/test.yaml | 9 + .../test_data_types_invalid.yaml | 8 + .../test_data_types_valid.yaml | 8 + .../bootstrapmcp_datatypes/test_invalid.yaml | 5 + .../bootstrapmcp_datatypes/test_valid.yaml | 5 + docs-website/sidebars.js | 1 + docs/advanced/bootstrap-mcps.md | 157 ++++ docs/how/add-custom-data-platform.md | 2 +- docs/what-is-datahub/datahub-concepts.md | 2 +- metadata-ingestion/adding-source.md | 2 +- .../src/datahub/ingestion/source/metabase.py | 2 +- .../src/datahub/ingestion/source/mode.py | 2 +- .../source/tableau/tableau_common.py | 2 +- metadata-models/docs/entities/dataPlatform.md | 2 +- .../metadata/context/ObjectMapperContext.java | 31 +- .../metadata/context/OperationContext.java | 5 + .../src/main/resources/application.yaml | 4 +- .../src/main/resources/bootstrap_mcps.yaml | 36 + .../bootstrap_mcps/data-platforms.yaml | 709 ++++++++++++++++++ .../resources/bootstrap_mcps/data-types.yaml | 43 ++ .../bootstrap_mcps/ingestion-datahub-gc.yaml | 33 + .../bootstrap_mcps/ownership-types.yaml | 39 + .../main/resources/bootstrap_mcps/roles.yaml | 28 + .../resources/bootstrap_mcps/root-user.yaml | 8 + .../factories/BootstrapManagerFactory.java | 20 - .../boot/steps/IngestDataPlatformsStep.java | 117 --- .../boot/steps/IngestDataTypesStep.java | 108 --- .../boot/steps/IngestOwnershipTypesStep.java | 117 --- .../metadata/boot/steps/IngestRolesStep.java | 154 ---- .../boot/steps/IngestRootUserStep.java | 96 --- .../boot/steps/IngestDataTypesStepTest.java | 97 --- .../boot/test_data_types_invalid.json | 9 - .../resources/boot/test_data_types_valid.json | 10 - .../main/resources/boot/data_platforms.json | 708 ----------------- .../src/main/resources/boot/data_types.json | 42 -- .../main/resources/boot/ownership_types.json | 30 - .../war/src/main/resources/boot/roles.json | 26 - .../src/main/resources/boot/root_user.json | 8 - .../metadata/utils/GenericRecordUtils.java | 8 +- 58 files changed, 1969 insertions(+), 1585 deletions(-) create mode 100644 datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/BootstrapMCPConfig.java create mode 100644 datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCP.java create mode 100644 datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPStep.java create mode 100644 datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPUtil.java create mode 100644 datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/model/BootstrapMCPConfigFile.java create mode 100644 datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeBlockingTest.java create mode 100644 datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPUtilTest.java create mode 100644 datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/DataTypesTest.java create mode 100644 datahub-upgrade/src/test/resources/bootstrapmcp/datahub-test-mcp.yaml create mode 100644 datahub-upgrade/src/test/resources/bootstrapmcp/test.yaml create mode 100644 datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_data_types_invalid.yaml create mode 100644 datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_data_types_valid.yaml create mode 100644 datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_invalid.yaml create mode 100644 datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_valid.yaml create mode 100644 docs/advanced/bootstrap-mcps.md create mode 100644 metadata-service/configuration/src/main/resources/bootstrap_mcps.yaml create mode 100644 metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml create mode 100644 metadata-service/configuration/src/main/resources/bootstrap_mcps/data-types.yaml create mode 100644 metadata-service/configuration/src/main/resources/bootstrap_mcps/ingestion-datahub-gc.yaml create mode 100644 metadata-service/configuration/src/main/resources/bootstrap_mcps/ownership-types.yaml create mode 100644 metadata-service/configuration/src/main/resources/bootstrap_mcps/roles.yaml create mode 100644 metadata-service/configuration/src/main/resources/bootstrap_mcps/root-user.yaml delete mode 100644 metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestDataPlatformsStep.java delete mode 100644 metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestDataTypesStep.java delete mode 100644 metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestOwnershipTypesStep.java delete mode 100644 metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestRolesStep.java delete mode 100644 metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestRootUserStep.java delete mode 100644 metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataTypesStepTest.java delete mode 100644 metadata-service/factories/src/test/resources/boot/test_data_types_invalid.json delete mode 100644 metadata-service/factories/src/test/resources/boot/test_data_types_valid.json delete mode 100644 metadata-service/war/src/main/resources/boot/data_platforms.json delete mode 100644 metadata-service/war/src/main/resources/boot/data_types.json delete mode 100644 metadata-service/war/src/main/resources/boot/ownership_types.json delete mode 100644 metadata-service/war/src/main/resources/boot/roles.json delete mode 100644 metadata-service/war/src/main/resources/boot/root_user.json diff --git a/build.gradle b/build.gradle index 5857746a8fd919..2166c76dbd6d0d 100644 --- a/build.gradle +++ b/build.gradle @@ -276,6 +276,7 @@ project.ext.externalDependency = [ 'annotationApi': 'javax.annotation:javax.annotation-api:1.3.2', 'jakartaAnnotationApi': 'jakarta.annotation:jakarta.annotation-api:3.0.0', 'classGraph': 'io.github.classgraph:classgraph:4.8.172', + 'mustache': 'com.github.spullara.mustache.java:compiler:0.9.14' ] allprojects { diff --git a/datahub-upgrade/build.gradle b/datahub-upgrade/build.gradle index e808f9e87687c0..b783efa09713d1 100644 --- a/datahub-upgrade/build.gradle +++ b/datahub-upgrade/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation project(':metadata-dao-impl:kafka-producer') implementation externalDependency.charle + implementation externalDependency.mustache implementation externalDependency.javaxInject implementation(externalDependency.hadoopClient) { exclude group: 'net.minidev', module: 'json-smart' @@ -83,6 +84,7 @@ dependencies { testImplementation externalDependency.springBootTest testImplementation externalDependency.mockito testImplementation externalDependency.testng + testImplementation 'uk.org.webcompere:system-stubs-testng:2.1.7' testRuntimeOnly externalDependency.logbackClassic constraints { diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/UpgradeManager.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/UpgradeManager.java index 14f36e60d75b2d..75d92e38855420 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/UpgradeManager.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/UpgradeManager.java @@ -8,7 +8,7 @@ public interface UpgradeManager { /** Register an {@link Upgrade} with the manaager. */ - void register(Upgrade upgrade); + UpgradeManager register(Upgrade upgrade); /** Kick off an {@link Upgrade} by identifier. */ UpgradeResult execute( diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/BootstrapMCPConfig.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/BootstrapMCPConfig.java new file mode 100644 index 00000000000000..e7987ba449cfc0 --- /dev/null +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/BootstrapMCPConfig.java @@ -0,0 +1,30 @@ +package com.linkedin.datahub.upgrade.config; + +import com.linkedin.datahub.upgrade.system.bootstrapmcps.BootstrapMCP; +import com.linkedin.metadata.entity.EntityService; +import io.datahubproject.metadata.context.OperationContext; +import java.io.IOException; +import javax.annotation.Nonnull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BootstrapMCPConfig { + + @Nonnull + @Value("${systemUpdate.bootstrap.mcpConfig}") + private String bootstrapMCPConfig; + + @Bean(name = "bootstrapMCPNonBlocking") + public BootstrapMCP bootstrapMCPNonBlocking( + final OperationContext opContext, EntityService entityService) throws IOException { + return new BootstrapMCP(opContext, bootstrapMCPConfig, entityService, false); + } + + @Bean(name = "bootstrapMCPBlocking") + public BootstrapMCP bootstrapMCPBlocking( + final OperationContext opContext, EntityService entityService) throws IOException { + return new BootstrapMCP(opContext, bootstrapMCPConfig, entityService, true); + } +} diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java index 8b33e4e7c21649..f3a4c47c59f0b7 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java @@ -6,6 +6,7 @@ import com.linkedin.datahub.upgrade.system.SystemUpdate; import com.linkedin.datahub.upgrade.system.SystemUpdateBlocking; import com.linkedin.datahub.upgrade.system.SystemUpdateNonBlocking; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.BootstrapMCP; import com.linkedin.datahub.upgrade.system.elasticsearch.steps.DataHubStartupStep; import com.linkedin.gms.factory.config.ConfigurationProvider; import com.linkedin.gms.factory.kafka.DataHubKafkaProducerFactory; @@ -31,6 +32,7 @@ import io.datahubproject.metadata.services.RestrictedService; import java.util.List; import javax.annotation.Nonnull; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.avro.generic.IndexedRecord; import org.apache.kafka.clients.producer.KafkaProducer; @@ -54,21 +56,31 @@ public class SystemUpdateConfig { public SystemUpdate systemUpdate( final List blockingSystemUpgrades, final List nonBlockingSystemUpgrades, - final DataHubStartupStep dataHubStartupStep) { - return new SystemUpdate(blockingSystemUpgrades, nonBlockingSystemUpgrades, dataHubStartupStep); + final DataHubStartupStep dataHubStartupStep, + @Qualifier("bootstrapMCPBlocking") @NonNull final BootstrapMCP bootstrapMCPBlocking, + @Qualifier("bootstrapMCPNonBlocking") @NonNull final BootstrapMCP bootstrapMCPNonBlocking) { + return new SystemUpdate( + blockingSystemUpgrades, + nonBlockingSystemUpgrades, + dataHubStartupStep, + bootstrapMCPBlocking, + bootstrapMCPNonBlocking); } @Bean(name = "systemUpdateBlocking") public SystemUpdateBlocking systemUpdateBlocking( final List blockingSystemUpgrades, - final DataHubStartupStep dataHubStartupStep) { - return new SystemUpdateBlocking(blockingSystemUpgrades, List.of(), dataHubStartupStep); + final DataHubStartupStep dataHubStartupStep, + @Qualifier("bootstrapMCPBlocking") @NonNull final BootstrapMCP bootstrapMCPBlocking) { + return new SystemUpdateBlocking( + blockingSystemUpgrades, dataHubStartupStep, bootstrapMCPBlocking); } @Bean(name = "systemUpdateNonBlocking") public SystemUpdateNonBlocking systemUpdateNonBlocking( - final List nonBlockingSystemUpgrades) { - return new SystemUpdateNonBlocking(List.of(), nonBlockingSystemUpgrades, null); + final List nonBlockingSystemUpgrades, + @Qualifier("bootstrapMCPNonBlocking") @NonNull final BootstrapMCP bootstrapMCPNonBlocking) { + return new SystemUpdateNonBlocking(nonBlockingSystemUpgrades, bootstrapMCPNonBlocking); } @Value("#{systemEnvironment['DATAHUB_REVISION'] ?: '0'}") diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/impl/DefaultUpgradeContext.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/impl/DefaultUpgradeContext.java index c4cfad53624764..b7b86ca046dca5 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/impl/DefaultUpgradeContext.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/impl/DefaultUpgradeContext.java @@ -24,7 +24,7 @@ public class DefaultUpgradeContext implements UpgradeContext { private final List args; private final Map> parsedArgs; - DefaultUpgradeContext( + public DefaultUpgradeContext( @Nonnull OperationContext opContext, Upgrade upgrade, UpgradeReport report, diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/impl/DefaultUpgradeManager.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/impl/DefaultUpgradeManager.java index 27ba6abbc5ba93..443042049e8856 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/impl/DefaultUpgradeManager.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/impl/DefaultUpgradeManager.java @@ -26,8 +26,9 @@ public class DefaultUpgradeManager implements UpgradeManager { private final Map _upgrades = new HashMap<>(); @Override - public void register(@Nonnull Upgrade upgrade) { + public UpgradeManager register(@Nonnull Upgrade upgrade) { _upgrades.put(upgrade.id(), upgrade); + return this; } @Override diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdate.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdate.java index ad1c6c98fa3fd1..ba5ad4372d93f1 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdate.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdate.java @@ -3,6 +3,7 @@ import com.linkedin.datahub.upgrade.Upgrade; import com.linkedin.datahub.upgrade.UpgradeCleanupStep; import com.linkedin.datahub.upgrade.UpgradeStep; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.BootstrapMCP; import com.linkedin.datahub.upgrade.system.elasticsearch.steps.DataHubStartupStep; import java.util.LinkedList; import java.util.List; @@ -22,7 +23,9 @@ public class SystemUpdate implements Upgrade { public SystemUpdate( @NonNull final List blockingSystemUpgrades, @NonNull final List nonBlockingSystemUpgrades, - @Nullable final DataHubStartupStep dataHubStartupStep) { + @Nullable final DataHubStartupStep dataHubStartupStep, + @Nullable final BootstrapMCP bootstrapMCPBlocking, + @Nullable final BootstrapMCP bootstrapMCPNonBlocking) { steps = new LinkedList<>(); cleanupSteps = new LinkedList<>(); @@ -32,11 +35,23 @@ public SystemUpdate( cleanupSteps.addAll( blockingSystemUpgrades.stream().flatMap(up -> up.cleanupSteps().stream()).toList()); + // bootstrap blocking only + if (bootstrapMCPBlocking != null) { + steps.addAll(bootstrapMCPBlocking.steps()); + cleanupSteps.addAll(bootstrapMCPBlocking.cleanupSteps()); + } + // emit system update message if blocking upgrade(s) present if (dataHubStartupStep != null && !blockingSystemUpgrades.isEmpty()) { steps.add(dataHubStartupStep); } + // bootstrap non-blocking only + if (bootstrapMCPNonBlocking != null) { + steps.addAll(bootstrapMCPNonBlocking.steps()); + cleanupSteps.addAll(bootstrapMCPNonBlocking.cleanupSteps()); + } + // add non-blocking upgrades last steps.addAll(nonBlockingSystemUpgrades.stream().flatMap(up -> up.steps().stream()).toList()); cleanupSteps.addAll( diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdateBlocking.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdateBlocking.java index 32841149c467b3..e3b9baffa05688 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdateBlocking.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdateBlocking.java @@ -1,16 +1,16 @@ package com.linkedin.datahub.upgrade.system; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.BootstrapMCP; import com.linkedin.datahub.upgrade.system.elasticsearch.steps.DataHubStartupStep; import java.util.List; import lombok.NonNull; -import org.jetbrains.annotations.Nullable; public class SystemUpdateBlocking extends SystemUpdate { public SystemUpdateBlocking( @NonNull List blockingSystemUpgrades, - @NonNull List nonBlockingSystemUpgrades, - @Nullable DataHubStartupStep dataHubStartupStep) { - super(blockingSystemUpgrades, nonBlockingSystemUpgrades, dataHubStartupStep); + @NonNull DataHubStartupStep dataHubStartupStep, + @NonNull final BootstrapMCP bootstrapMCPBlocking) { + super(blockingSystemUpgrades, List.of(), dataHubStartupStep, bootstrapMCPBlocking, null); } } diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdateNonBlocking.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdateNonBlocking.java index 3309babc1f6cf2..fbc84c518b242e 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdateNonBlocking.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/SystemUpdateNonBlocking.java @@ -1,16 +1,14 @@ package com.linkedin.datahub.upgrade.system; -import com.linkedin.datahub.upgrade.system.elasticsearch.steps.DataHubStartupStep; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.BootstrapMCP; import java.util.List; import lombok.NonNull; -import org.jetbrains.annotations.Nullable; public class SystemUpdateNonBlocking extends SystemUpdate { public SystemUpdateNonBlocking( - @NonNull List blockingSystemUpgrades, @NonNull List nonBlockingSystemUpgrades, - @Nullable DataHubStartupStep dataHubStartupStep) { - super(blockingSystemUpgrades, nonBlockingSystemUpgrades, dataHubStartupStep); + final BootstrapMCP bootstrapMCPNonBlocking) { + super(List.of(), nonBlockingSystemUpgrades, null, null, bootstrapMCPNonBlocking); } } diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCP.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCP.java new file mode 100644 index 00000000000000..6264ee6076e6f8 --- /dev/null +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCP.java @@ -0,0 +1,38 @@ +package com.linkedin.datahub.upgrade.system.bootstrapmcps; + +import com.google.common.collect.ImmutableList; +import com.linkedin.datahub.upgrade.Upgrade; +import com.linkedin.datahub.upgrade.UpgradeStep; +import com.linkedin.metadata.entity.EntityService; +import io.datahubproject.metadata.context.OperationContext; +import java.io.IOException; +import java.util.List; +import javax.annotation.Nullable; + +public class BootstrapMCP implements Upgrade { + private final List _steps; + + public BootstrapMCP( + OperationContext opContext, + @Nullable String bootstrapMCPConfig, + EntityService entityService, + boolean isBlocking) + throws IOException { + if (bootstrapMCPConfig != null && !bootstrapMCPConfig.isEmpty()) { + _steps = + BootstrapMCPUtil.generateSteps(opContext, isBlocking, bootstrapMCPConfig, entityService); + } else { + _steps = ImmutableList.of(); + } + } + + @Override + public String id() { + return getClass().getSimpleName(); + } + + @Override + public List steps() { + return _steps; + } +} diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPStep.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPStep.java new file mode 100644 index 00000000000000..07835ecd6b3e29 --- /dev/null +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPStep.java @@ -0,0 +1,95 @@ +package com.linkedin.datahub.upgrade.system.bootstrapmcps; + +import static com.linkedin.metadata.Constants.DATA_HUB_UPGRADE_RESULT_ASPECT_NAME; + +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.upgrade.UpgradeContext; +import com.linkedin.datahub.upgrade.UpgradeStep; +import com.linkedin.datahub.upgrade.UpgradeStepResult; +import com.linkedin.datahub.upgrade.impl.DefaultUpgradeStepResult; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.model.BootstrapMCPConfigFile; +import com.linkedin.metadata.aspect.batch.AspectsBatch; +import com.linkedin.metadata.boot.BootstrapStep; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.upgrade.DataHubUpgradeState; +import io.datahubproject.metadata.context.OperationContext; +import java.io.IOException; +import java.util.List; +import java.util.function.Function; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * This bootstrap step is responsible for upgrading DataHub policy documents with new searchable + * fields in ES + */ +@Slf4j +public class BootstrapMCPStep implements UpgradeStep { + private final String upgradeId; + private final Urn upgradeIdUrn; + + private final OperationContext opContext; + private final EntityService entityService; + @Getter private final BootstrapMCPConfigFile.MCPTemplate mcpTemplate; + + public BootstrapMCPStep( + OperationContext opContext, + EntityService entityService, + BootstrapMCPConfigFile.MCPTemplate mcpTemplate) { + this.opContext = opContext; + this.entityService = entityService; + this.mcpTemplate = mcpTemplate; + this.upgradeId = + String.join("-", List.of("bootstrap", mcpTemplate.getName(), mcpTemplate.getVersion())); + this.upgradeIdUrn = BootstrapStep.getUpgradeUrn(this.upgradeId); + } + + @Override + public String id() { + return upgradeId; + } + + @Override + public Function executable() { + return (context) -> { + try { + AspectsBatch batch = BootstrapMCPUtil.generateAspectBatch(opContext, mcpTemplate); + log.info("Ingesting {} MCPs", batch.getItems().size()); + entityService.ingestProposal(opContext, batch, mcpTemplate.isAsync()); + } catch (IOException e) { + log.error("Error bootstrapping MCPs", e); + return new DefaultUpgradeStepResult(id(), DataHubUpgradeState.FAILED); + } + + BootstrapStep.setUpgradeResult(context.opContext(), upgradeIdUrn, entityService); + + return new DefaultUpgradeStepResult(id(), DataHubUpgradeState.SUCCEEDED); + }; + } + + /** + * Returns whether the upgrade should proceed if the step fails after exceeding the maximum + * retries. + */ + @Override + public boolean isOptional() { + return mcpTemplate.isOptional(); + } + + /** Returns whether the upgrade should be skipped. */ + @Override + public boolean skip(UpgradeContext context) { + if (!mcpTemplate.isForce()) { + boolean previouslyRun = + entityService.exists( + context.opContext(), upgradeIdUrn, DATA_HUB_UPGRADE_RESULT_ASPECT_NAME, true); + if (previouslyRun) { + log.info("{} was already run. Skipping.", id()); + } + return previouslyRun; + } else { + log.info("{} forced run.", id()); + return false; + } + } +} diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPUtil.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPUtil.java new file mode 100644 index 00000000000000..b8b7e828c16c6b --- /dev/null +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPUtil.java @@ -0,0 +1,183 @@ +package com.linkedin.datahub.upgrade.system.bootstrapmcps; + +import static com.linkedin.metadata.Constants.INGESTION_INFO_ASPECT_NAME; + +import com.datahub.util.RecordUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; +import com.linkedin.common.AuditStamp; +import com.linkedin.datahub.upgrade.UpgradeStep; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.model.BootstrapMCPConfigFile; +import com.linkedin.metadata.aspect.batch.AspectsBatch; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl; +import com.linkedin.metadata.utils.AuditStampUtils; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.GenericAspect; +import com.linkedin.mxe.MetadataChangeProposal; +import io.datahubproject.metadata.context.OperationContext; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; + +@Slf4j +public class BootstrapMCPUtil { + static final MustacheFactory MUSTACHE_FACTORY = new DefaultMustacheFactory(); + + private BootstrapMCPUtil() {} + + static List generateSteps( + @Nonnull OperationContext opContext, + boolean isBlocking, + @Nonnull String bootstrapMCPConfig, + @Nonnull EntityService entityService) + throws IOException { + List steps = + resolveYamlConf(opContext, bootstrapMCPConfig, BootstrapMCPConfigFile.class) + .getBootstrap() + .getTemplates() + .stream() + .filter(cfg -> cfg.isBlocking() == isBlocking) + .map(cfg -> new BootstrapMCPStep(opContext, entityService, cfg)) + .collect(Collectors.toList()); + + log.info( + "Generated {} {} BootstrapMCP steps", + steps.size(), + isBlocking ? "blocking" : "non-blocking"); + return steps; + } + + static AspectsBatch generateAspectBatch( + OperationContext opContext, BootstrapMCPConfigFile.MCPTemplate mcpTemplate) + throws IOException { + + final AuditStamp auditStamp = AuditStampUtils.createDefaultAuditStamp(); + + List mcps = + resolveMCPTemplate(opContext, mcpTemplate, auditStamp).stream() + .map( + mcpObjectNode -> { + ObjectNode aspect = (ObjectNode) mcpObjectNode.remove("aspect"); + + MetadataChangeProposal mcp = + opContext + .getObjectMapper() + .convertValue(mcpObjectNode, MetadataChangeProposal.class); + + try { + String jsonAspect = + opContext + .getObjectMapper() + .writeValueAsString( + convenienceConversions(opContext, mcp.getAspectName(), aspect)); + GenericAspect genericAspect = GenericRecordUtils.serializeAspect(jsonAspect); + mcp.setAspect(genericAspect); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return mcp; + }) + .collect(Collectors.toList()); + + return AspectsBatchImpl.builder() + .mcps(mcps, auditStamp, opContext.getRetrieverContext().get()) + .retrieverContext(opContext.getRetrieverContext().get()) + .build(); + } + + static List resolveMCPTemplate( + OperationContext opContext, + BootstrapMCPConfigFile.MCPTemplate mcpTemplate, + AuditStamp auditStamp) + throws IOException { + + String template = loadTemplate(mcpTemplate.getMcps_location()); + Mustache mustache = MUSTACHE_FACTORY.compile(new StringReader(template), mcpTemplate.getName()); + Map scopeValues = resolveValues(opContext, mcpTemplate, auditStamp); + StringWriter writer = new StringWriter(); + mustache.execute(writer, scopeValues); + + return opContext.getYamlMapper().readValue(writer.toString(), new TypeReference<>() {}); + } + + static Map resolveValues( + OperationContext opContext, + BootstrapMCPConfigFile.MCPTemplate mcpTemplate, + AuditStamp auditStamp) + throws IOException { + final Map scopeValues = new HashMap<>(); + + // built-in + scopeValues.put("auditStamp", RecordUtils.toJsonString(auditStamp)); + + if (mcpTemplate.getValues_env() != null + && !mcpTemplate.getValues_env().isEmpty() + && System.getenv().containsKey(mcpTemplate.getValues_env())) { + String envValue = System.getenv(mcpTemplate.getValues_env()); + scopeValues.putAll(opContext.getObjectMapper().readValue(envValue, new TypeReference<>() {})); + } + return scopeValues; + } + + private static String loadTemplate(String source) throws IOException { + log.info("Loading MCP template {}", source); + try (InputStream stream = new ClassPathResource(source).getInputStream()) { + log.info("Found in classpath: {}", source); + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } catch (FileNotFoundException e) { + log.info("{} was NOT found in the classpath.", source); + try (InputStream stream = new FileSystemResource(source).getInputStream()) { + log.info("Found in filesystem: {}", source); + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } catch (Exception e2) { + throw new IllegalArgumentException(String.format("Could not resolve %s", source)); + } + } + } + + static T resolveYamlConf(OperationContext opContext, String source, Class clazz) + throws IOException { + log.info("Resolving {} to {}", source, clazz.getSimpleName()); + try (InputStream stream = new ClassPathResource(source).getInputStream()) { + log.info("Found in classpath: {}", source); + return opContext.getYamlMapper().readValue(stream, clazz); + } catch (FileNotFoundException e) { + log.info("{} was NOT found in the classpath.", source); + try (InputStream stream = new FileSystemResource(source).getInputStream()) { + log.info("Found in filesystem: {}", source); + return opContext.getYamlMapper().readValue(stream, clazz); + } catch (Exception e2) { + throw new IllegalArgumentException(String.format("Could not resolve %s", source)); + } + } + } + + private static ObjectNode convenienceConversions( + OperationContext opContext, String aspectName, ObjectNode aspectObjectNode) + throws JsonProcessingException { + if (INGESTION_INFO_ASPECT_NAME.equals(aspectName)) { + ObjectNode config = (ObjectNode) aspectObjectNode.get("config"); + ObjectNode recipe = (ObjectNode) config.remove("recipe"); + config.put("recipe", opContext.getObjectMapper().writeValueAsString(recipe)); + } + return aspectObjectNode; + } +} diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/model/BootstrapMCPConfigFile.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/model/BootstrapMCPConfigFile.java new file mode 100644 index 00000000000000..8fd3dd7c7d8975 --- /dev/null +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/model/BootstrapMCPConfigFile.java @@ -0,0 +1,40 @@ +package com.linkedin.datahub.upgrade.system.bootstrapmcps.model; + +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class BootstrapMCPConfigFile { + private Bootstrap bootstrap; + + @AllArgsConstructor + @NoArgsConstructor + @Data + @Builder + public static class Bootstrap { + private List templates; + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + @Builder + public static class MCPTemplate { + @Nonnull private String name; + @Nonnull private String version; + @Builder.Default private boolean force = false; + @Builder.Default private boolean blocking = false; + @Builder.Default private boolean async = true; + @Builder.Default private boolean optional = false; + @Nonnull private String mcps_location; + @Nullable private String values_env; + } +} diff --git a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeBlockingTest.java b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeBlockingTest.java new file mode 100644 index 00000000000000..0672061c665c1a --- /dev/null +++ b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeBlockingTest.java @@ -0,0 +1,48 @@ +package com.linkedin.datahub.upgrade; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import static org.testng.AssertJUnit.assertNotNull; + +import com.linkedin.datahub.upgrade.system.SystemUpdateBlocking; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.BootstrapMCPStep; +import java.util.List; +import java.util.stream.Collectors; +import javax.inject.Named; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.Test; + +@ActiveProfiles("test") +@SpringBootTest( + classes = {UpgradeCliApplication.class, UpgradeCliApplicationTestConfiguration.class}, + args = {"-u", "SystemUpdateBlocking"}) +public class DatahubUpgradeBlockingTest extends AbstractTestNGSpringContextTests { + + @Autowired + @Named("systemUpdateBlocking") + private SystemUpdateBlocking systemUpdateBlocking; + + @Test + public void testNBlockingBootstrapMCP() { + assertNotNull(systemUpdateBlocking); + + List mcpTemplate = + systemUpdateBlocking.steps().stream() + .filter(update -> update instanceof BootstrapMCPStep) + .map(update -> (BootstrapMCPStep) update) + .toList(); + + assertFalse(mcpTemplate.isEmpty()); + assertTrue( + mcpTemplate.stream().allMatch(update -> update.getMcpTemplate().isBlocking()), + String.format( + "Found non-blocking step (expected blocking only): %s", + mcpTemplate.stream() + .filter(update -> !update.getMcpTemplate().isBlocking()) + .map(update -> update.getMcpTemplate().getName()) + .collect(Collectors.toSet()))); + } +} diff --git a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeNonBlockingTest.java b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeNonBlockingTest.java index df27d33f3a117e..845d8185273432 100644 --- a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeNonBlockingTest.java +++ b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeNonBlockingTest.java @@ -5,10 +5,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import static org.testng.AssertJUnit.assertNotNull; import com.linkedin.datahub.upgrade.impl.DefaultUpgradeManager; import com.linkedin.datahub.upgrade.system.SystemUpdateNonBlocking; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.BootstrapMCPStep; import com.linkedin.datahub.upgrade.system.graph.vianodes.ReindexDataJobViaNodesCLL; import com.linkedin.metadata.boot.kafka.MockSystemUpdateDeserializer; import com.linkedin.metadata.boot.kafka.MockSystemUpdateSerializer; @@ -22,6 +25,7 @@ import io.datahubproject.metadata.context.OperationContext; import io.datahubproject.test.metadata.context.TestOperationContexts; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Named; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -41,7 +45,7 @@ args = {"-u", "SystemUpdateNonBlocking"}) public class DatahubUpgradeNonBlockingTest extends AbstractTestNGSpringContextTests { - @Autowired(required = false) + @Autowired @Named("systemUpdateNonBlocking") private SystemUpdateNonBlocking systemUpdateNonBlocking; @@ -84,8 +88,7 @@ public void testReindexDataJobViaNodesCLLPaging() { ReindexDataJobViaNodesCLL cllUpgrade = new ReindexDataJobViaNodesCLL(opContext, mockService, mockAspectDao, true, 10, 0, 0); - SystemUpdateNonBlocking upgrade = - new SystemUpdateNonBlocking(List.of(), List.of(cllUpgrade), null); + SystemUpdateNonBlocking upgrade = new SystemUpdateNonBlocking(List.of(cllUpgrade), null); DefaultUpgradeManager manager = new DefaultUpgradeManager(); manager.register(upgrade); manager.execute( @@ -101,4 +104,23 @@ public void testReindexDataJobViaNodesCLLPaging() { .aspectName("dataJobInputOutput") .urnLike("urn:li:dataJob:%"))); } + + @Test + public void testNonBlockingBootstrapMCP() { + List mcpTemplate = + systemUpdateNonBlocking.steps().stream() + .filter(update -> update instanceof BootstrapMCPStep) + .map(update -> (BootstrapMCPStep) update) + .toList(); + + assertFalse(mcpTemplate.isEmpty()); + assertTrue( + mcpTemplate.stream().noneMatch(update -> update.getMcpTemplate().isBlocking()), + String.format( + "Found blocking step: %s (expected non-blocking only)", + mcpTemplate.stream() + .filter(update -> update.getMcpTemplate().isBlocking()) + .map(update -> update.getMcpTemplate().getName()) + .collect(Collectors.toSet()))); + } } diff --git a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPUtilTest.java b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPUtilTest.java new file mode 100644 index 00000000000000..68023a084bbd20 --- /dev/null +++ b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/BootstrapMCPUtilTest.java @@ -0,0 +1,224 @@ +package com.linkedin.datahub.upgrade.system.bootstrapmcps; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.upgrade.system.bootstrapmcps.model.BootstrapMCPConfigFile; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.ingestion.DataHubIngestionSourceInfo; +import com.linkedin.metadata.aspect.batch.AspectsBatch; +import com.linkedin.metadata.aspect.batch.MCPItem; +import com.linkedin.metadata.utils.AuditStampUtils; +import io.datahubproject.metadata.context.OperationContext; +import io.datahubproject.test.metadata.context.TestOperationContexts; +import java.io.IOException; +import java.util.List; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.testng.SystemStub; +import uk.org.webcompere.systemstubs.testng.SystemStubsListener; + +@Listeners(SystemStubsListener.class) +public class BootstrapMCPUtilTest { + static final OperationContext OP_CONTEXT = + TestOperationContexts.systemContextNoSearchAuthorization(); + private static final String DATAHUB_TEST_VALUES_ENV = "DATAHUB_TEST_VALUES_ENV"; + private static final AuditStamp TEST_AUDIT_STAMP = AuditStampUtils.createDefaultAuditStamp(); + + @SystemStub private EnvironmentVariables environmentVariables; + + @Test + public void testResolveYamlConf() throws IOException { + BootstrapMCPConfigFile initConfig = + BootstrapMCPUtil.resolveYamlConf( + OP_CONTEXT, "bootstrapmcp/test.yaml", BootstrapMCPConfigFile.class); + assertEquals(initConfig.getBootstrap().getTemplates().size(), 1); + + BootstrapMCPConfigFile.MCPTemplate template = initConfig.getBootstrap().getTemplates().get(0); + assertEquals(template.getName(), "datahub-test"); + assertEquals(template.getVersion(), "v10"); + assertFalse(template.isForce()); + assertFalse(template.isBlocking()); + assertTrue(template.isAsync()); + assertFalse(template.isOptional()); + assertEquals(template.getMcps_location(), "bootstrapmcp/datahub-test-mcp.yaml"); + assertEquals(template.getValues_env(), "DATAHUB_TEST_VALUES_ENV"); + } + + @Test + public void testResolveMCPTemplateDefaults() throws IOException { + environmentVariables.remove(DATAHUB_TEST_VALUES_ENV); + + BootstrapMCPConfigFile.MCPTemplate template = + BootstrapMCPUtil.resolveYamlConf( + OP_CONTEXT, "bootstrapmcp/test.yaml", BootstrapMCPConfigFile.class) + .getBootstrap() + .getTemplates() + .get(0); + + List mcpObjectNodes = + BootstrapMCPUtil.resolveMCPTemplate(OP_CONTEXT, template, TEST_AUDIT_STAMP); + assertEquals(mcpObjectNodes.size(), 1); + + ObjectNode mcp = mcpObjectNodes.get(0); + assertEquals(mcp.get("entityType").asText(), "dataHubIngestionSource"); + assertEquals(mcp.get("entityUrn").asText(), "urn:li:dataHubIngestionSource:datahub-test"); + assertEquals(mcp.get("aspectName").asText(), "dataHubIngestionSourceInfo"); + assertEquals(mcp.get("changeType").asText(), "UPSERT"); + + ObjectNode aspect = (ObjectNode) mcp.get("aspect"); + assertEquals(aspect.get("type").asText(), "datahub-gc"); + assertEquals(aspect.get("name").asText(), "datahub-test"); + + ObjectNode schedule = (ObjectNode) aspect.get("schedule"); + assertEquals(schedule.get("timezone").asText(), "UTC"); + assertEquals(schedule.get("interval").asText(), "0 0 * * *"); + + ObjectNode config = (ObjectNode) aspect.get("config"); + assertTrue(config.get("extraArgs").isObject()); + assertTrue(config.get("debugMode").isBoolean()); + assertEquals(config.get("executorId").asText(), "default"); + + ObjectNode recipe = (ObjectNode) config.get("recipe"); + ObjectNode source = (ObjectNode) recipe.get("source"); + assertEquals(source.get("type").asText(), "datahub-gc"); + + ObjectNode sourceConfig = (ObjectNode) source.get("config"); + assertFalse(sourceConfig.get("cleanup_expired_tokens").asBoolean()); + assertTrue(sourceConfig.get("truncate_indices").asBoolean()); + + ObjectNode dataprocessCleanup = (ObjectNode) sourceConfig.get("dataprocess_cleanup"); + assertEquals(dataprocessCleanup.get("retention_days").asInt(), 10); + assertTrue(dataprocessCleanup.get("delete_empty_data_jobs").asBoolean()); + assertTrue(dataprocessCleanup.get("delete_empty_data_flows").asBoolean()); + assertFalse(dataprocessCleanup.get("hard_delete_entities").asBoolean()); + assertEquals(dataprocessCleanup.get("keep_last_n").asInt(), 5); + + ObjectNode softDeletedEntitiesCleanup = + (ObjectNode) sourceConfig.get("soft_deleted_entities_cleanup"); + assertEquals(softDeletedEntitiesCleanup.get("retention_days").asInt(), 10); + + assertTrue(mcp.get("headers").isObject()); + } + + @Test + public void testResolveMCPTemplateOverride() throws IOException { + environmentVariables.set( + "DATAHUB_TEST_VALUES_ENV", + "{\n" + + " \"ingestion\": {\n" + + " \"name\": \"name-override\"\n" + + " },\n" + + " \"schedule\": {\n" + + " \"timezone\": \"America/Chicago\",\n" + + " \"interval\": \"9 9 * * *\"\n" + + " },\n" + + " \"cleanup_expired_tokens\": true,\n" + + " \"truncate_indices\": false,\n" + + " \"dataprocess_cleanup\": {\n" + + " \"retention_days\": 99,\n" + + " \"delete_empty_data_jobs\": false,\n" + + " \"delete_empty_data_flows\": false,\n" + + " \"hard_delete_entities\": true,\n" + + " \"keep_last_n\": 50\n" + + " },\n" + + " \"soft_deleted_entities_cleanup\": {\n" + + " \"retention_days\": 100\n" + + " }\n" + + "}"); + + BootstrapMCPConfigFile.MCPTemplate template = + BootstrapMCPUtil.resolveYamlConf( + OP_CONTEXT, "bootstrapmcp/test.yaml", BootstrapMCPConfigFile.class) + .getBootstrap() + .getTemplates() + .get(0); + + List mcpObjectNodes = + BootstrapMCPUtil.resolveMCPTemplate(OP_CONTEXT, template, TEST_AUDIT_STAMP); + assertEquals(mcpObjectNodes.size(), 1); + + ObjectNode mcp = mcpObjectNodes.get(0); + assertEquals(mcp.get("entityType").asText(), "dataHubIngestionSource"); + assertEquals(mcp.get("entityUrn").asText(), "urn:li:dataHubIngestionSource:datahub-test"); + assertEquals(mcp.get("aspectName").asText(), "dataHubIngestionSourceInfo"); + assertEquals(mcp.get("changeType").asText(), "UPSERT"); + + ObjectNode aspect = (ObjectNode) mcp.get("aspect"); + assertEquals(aspect.get("type").asText(), "datahub-gc"); + assertEquals(aspect.get("name").asText(), "name-override"); + + ObjectNode schedule = (ObjectNode) aspect.get("schedule"); + assertEquals(schedule.get("timezone").asText(), "America/Chicago"); + assertEquals(schedule.get("interval").asText(), "9 9 * * *"); + + ObjectNode config = (ObjectNode) aspect.get("config"); + assertTrue(config.get("extraArgs").isObject()); + assertTrue(config.get("debugMode").isBoolean()); + assertEquals(config.get("executorId").asText(), "default"); + + ObjectNode recipe = (ObjectNode) config.get("recipe"); + ObjectNode source = (ObjectNode) recipe.get("source"); + assertEquals(source.get("type").asText(), "datahub-gc"); + + ObjectNode sourceConfig = (ObjectNode) source.get("config"); + assertTrue(sourceConfig.get("cleanup_expired_tokens").asBoolean()); + assertFalse(sourceConfig.get("truncate_indices").asBoolean()); + + ObjectNode dataprocessCleanup = (ObjectNode) sourceConfig.get("dataprocess_cleanup"); + assertEquals(dataprocessCleanup.get("retention_days").asInt(), 99); + assertFalse(dataprocessCleanup.get("delete_empty_data_jobs").asBoolean()); + assertFalse(dataprocessCleanup.get("delete_empty_data_flows").asBoolean()); + assertTrue(dataprocessCleanup.get("hard_delete_entities").asBoolean()); + assertEquals(dataprocessCleanup.get("keep_last_n").asInt(), 50); + + ObjectNode softDeletedEntitiesCleanup = + (ObjectNode) sourceConfig.get("soft_deleted_entities_cleanup"); + assertEquals(softDeletedEntitiesCleanup.get("retention_days").asInt(), 100); + + assertTrue(mcp.get("headers").isObject()); + } + + @Test + public void testMCPBatch() throws IOException { + environmentVariables.remove(DATAHUB_TEST_VALUES_ENV); + + BootstrapMCPConfigFile.MCPTemplate template = + BootstrapMCPUtil.resolveYamlConf( + OP_CONTEXT, "bootstrapmcp/test.yaml", BootstrapMCPConfigFile.class) + .getBootstrap() + .getTemplates() + .get(0); + + AspectsBatch batch = BootstrapMCPUtil.generateAspectBatch(OP_CONTEXT, template); + assertEquals(batch.getMCPItems().size(), 1); + + MCPItem item = batch.getMCPItems().get(0); + assertEquals(item.getUrn(), UrnUtils.getUrn("urn:li:dataHubIngestionSource:datahub-test")); + assertEquals(item.getAspectName(), "dataHubIngestionSourceInfo"); + assertEquals(item.getChangeType(), ChangeType.UPSERT); + + DataHubIngestionSourceInfo ingestionSource = item.getAspect(DataHubIngestionSourceInfo.class); + + assertEquals(ingestionSource.getName(), "datahub-test"); + assertEquals(ingestionSource.getType(), "datahub-gc"); + + assertFalse(ingestionSource.getConfig().isDebugMode()); + assertEquals(ingestionSource.getConfig().getExecutorId(), "default"); + + assertEquals(ingestionSource.getSchedule().getTimezone(), "UTC"); + assertEquals(ingestionSource.getSchedule().getInterval(), "0 0 * * *"); + + assertEquals( + OP_CONTEXT.getObjectMapper().readTree(ingestionSource.getConfig().getRecipe()), + OP_CONTEXT + .getObjectMapper() + .readTree( + "{\"source\":{\"type\":\"datahub-gc\",\"config\":{\"cleanup_expired_tokens\":false,\"truncate_indices\":true,\"dataprocess_cleanup\":{\"retention_days\":10,\"delete_empty_data_jobs\":true,\"delete_empty_data_flows\":true,\"hard_delete_entities\":false,\"keep_last_n\":5},\"soft_deleted_entities_cleanup\":{\"retention_days\":10}}}}")); + } +} diff --git a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/DataTypesTest.java b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/DataTypesTest.java new file mode 100644 index 00000000000000..156b5347e544da --- /dev/null +++ b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/system/bootstrapmcps/DataTypesTest.java @@ -0,0 +1,79 @@ +package com.linkedin.datahub.upgrade.system.bootstrapmcps; + +import static com.linkedin.datahub.upgrade.system.bootstrapmcps.BootstrapMCPUtilTest.OP_CONTEXT; +import static com.linkedin.metadata.Constants.*; +import static org.mockito.Mockito.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.upgrade.Upgrade; +import com.linkedin.datahub.upgrade.UpgradeManager; +import com.linkedin.datahub.upgrade.UpgradeResult; +import com.linkedin.datahub.upgrade.impl.DefaultUpgradeManager; +import com.linkedin.datatype.DataTypeInfo; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl; +import com.linkedin.upgrade.DataHubUpgradeState; +import io.datahubproject.metadata.context.OperationContext; +import java.io.IOException; +import java.util.List; +import org.testng.annotations.Test; + +public class DataTypesTest { + + private static final Urn TEST_DATA_TYPE_URN = UrnUtils.getUrn("urn:li:dataType:datahub.test"); + + @Test + public void testExecuteValidDataTypesNoExistingDataTypes() throws Exception { + final EntityService entityService = mock(EntityService.class); + final UpgradeManager upgradeManager = + loadContext("bootstrapmcp_datatypes/test_valid.yaml", entityService); + + // run the upgrade + upgradeManager.execute(OP_CONTEXT, "BootstrapMCP", List.of()); + + DataTypeInfo expectedResult = new DataTypeInfo(); + expectedResult.setDescription("Test Description"); + expectedResult.setDisplayName("Test Name"); + expectedResult.setQualifiedName("datahub.test"); + + verify(entityService, times(1)) + .ingestProposal(any(OperationContext.class), any(AspectsBatchImpl.class), eq(true)); + } + + @Test + public void testExecuteInvalidJson() throws Exception { + final EntityService entityService = mock(EntityService.class); + final UpgradeManager upgradeManager = + loadContext("bootstrapmcp_datatypes/test_invalid.yaml", entityService); + + UpgradeResult upgradeResult = upgradeManager.execute(OP_CONTEXT, "BootstrapMCP", List.of()); + + assertEquals(upgradeResult.result(), DataHubUpgradeState.FAILED); + + // verify expected existence check + verify(entityService) + .exists( + any(OperationContext.class), + eq(UrnUtils.getUrn("urn:li:dataHubUpgrade:bootstrap-data-types-v1")), + eq("dataHubUpgradeResult"), + anyBoolean()); + + // Verify no additional interactions + verifyNoMoreInteractions(entityService); + } + + private static UpgradeManager loadContext(String configFile, EntityService entityService) + throws IOException { + // hasn't run + when(entityService.exists( + any(OperationContext.class), any(Urn.class), eq("dataHubUpgradeResult"), anyBoolean())) + .thenReturn(false); + + Upgrade bootstrapUpgrade = new BootstrapMCP(OP_CONTEXT, configFile, entityService, false); + assertFalse(bootstrapUpgrade.steps().isEmpty()); + return new DefaultUpgradeManager().register(bootstrapUpgrade); + } +} diff --git a/datahub-upgrade/src/test/resources/bootstrapmcp/datahub-test-mcp.yaml b/datahub-upgrade/src/test/resources/bootstrapmcp/datahub-test-mcp.yaml new file mode 100644 index 00000000000000..d049a807ac1d88 --- /dev/null +++ b/datahub-upgrade/src/test/resources/bootstrapmcp/datahub-test-mcp.yaml @@ -0,0 +1,29 @@ +- entityType: dataHubIngestionSource + entityUrn: urn:li:dataHubIngestionSource:datahub-test + aspectName: dataHubIngestionSourceInfo + changeType: UPSERT + aspect: + type: 'datahub-gc' + name: '{{ingestion.name}}{{^ingestion.name}}datahub-test{{/ingestion.name}}' + schedule: + timezone: '{{schedule.timezone}}{{^schedule.timezone}}UTC{{/schedule.timezone}}' + interval: '{{schedule.interval}}{{^schedule.interval}}0 0 * * *{{/schedule.interval}}' + config: + recipe: + source: + type: 'datahub-gc' + config: + cleanup_expired_tokens: {{cleanup_expired_tokens}}{{^cleanup_expired_tokens}}false{{/cleanup_expired_tokens}} + truncate_indices: {{truncate_indices}}{{^truncate_indices}}true{{/truncate_indices}} + dataprocess_cleanup: + retention_days: {{dataprocess_cleanup.retention_days}}{{^dataprocess_cleanup.retention_days}}10{{/dataprocess_cleanup.retention_days}} + delete_empty_data_jobs: {{dataprocess_cleanup.delete_empty_data_jobs}}{{^dataprocess_cleanup.delete_empty_data_jobs}}true{{/dataprocess_cleanup.delete_empty_data_jobs}} + delete_empty_data_flows: {{dataprocess_cleanup.delete_empty_data_flows}}{{^dataprocess_cleanup.delete_empty_data_flows}}true{{/dataprocess_cleanup.delete_empty_data_flows}} + hard_delete_entities: {{dataprocess_cleanup.hard_delete_entities}}{{^dataprocess_cleanup.hard_delete_entities}}false{{/dataprocess_cleanup.hard_delete_entities}} + keep_last_n: {{dataprocess_cleanup.keep_last_n}}{{^dataprocess_cleanup.keep_last_n}}5{{/dataprocess_cleanup.keep_last_n}} + soft_deleted_entities_cleanup: + retention_days: {{soft_deleted_entities_cleanup.retention_days}}{{^soft_deleted_entities_cleanup.retention_days}}10{{/soft_deleted_entities_cleanup.retention_days}} + extraArgs: {} + debugMode: false + executorId: default + headers: {} \ No newline at end of file diff --git a/datahub-upgrade/src/test/resources/bootstrapmcp/test.yaml b/datahub-upgrade/src/test/resources/bootstrapmcp/test.yaml new file mode 100644 index 00000000000000..649cc09632fc2a --- /dev/null +++ b/datahub-upgrade/src/test/resources/bootstrapmcp/test.yaml @@ -0,0 +1,9 @@ +bootstrap: + templates: + - name: datahub-test + version: v10 + # force: false + # blocking: false + # async: true + mcps_location: "bootstrapmcp/datahub-test-mcp.yaml" + values_env: "DATAHUB_TEST_VALUES_ENV" \ No newline at end of file diff --git a/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_data_types_invalid.yaml b/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_data_types_invalid.yaml new file mode 100644 index 00000000000000..4d4970e510380a --- /dev/null +++ b/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_data_types_invalid.yaml @@ -0,0 +1,8 @@ +- entityUrn: urn:li:dataType:datahub.test + entityType: dataType + aspectName: dataTypeInfo + changeType: UPSERT + aspect: + badField: + qualifiedName: datahub.test + description: Test Description \ No newline at end of file diff --git a/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_data_types_valid.yaml b/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_data_types_valid.yaml new file mode 100644 index 00000000000000..902315ab85dc8c --- /dev/null +++ b/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_data_types_valid.yaml @@ -0,0 +1,8 @@ +- entityUrn: urn:li:dataType:datahub.test + entityType: dataType + aspectName: dataTypeInfo + changeType: UPSERT + aspect: + qualifiedName: datahub.test + displayName: Test Name + description: Test Description \ No newline at end of file diff --git a/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_invalid.yaml b/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_invalid.yaml new file mode 100644 index 00000000000000..07654ff3c299ee --- /dev/null +++ b/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_invalid.yaml @@ -0,0 +1,5 @@ +bootstrap: + templates: + - name: data-types + version: v1 + mcps_location: "bootstrapmcp_datatypes/test_data_types_invalid.yaml" \ No newline at end of file diff --git a/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_valid.yaml b/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_valid.yaml new file mode 100644 index 00000000000000..05b769d22ddf37 --- /dev/null +++ b/datahub-upgrade/src/test/resources/bootstrapmcp_datatypes/test_valid.yaml @@ -0,0 +1,5 @@ +bootstrap: + templates: + - name: data-types + version: v1 + mcps_location: "bootstrapmcp_datatypes/test_data_types_valid.yaml" \ No newline at end of file diff --git a/docs-website/sidebars.js b/docs-website/sidebars.js index fe1ee9e6236ab7..12d279b6e1e815 100644 --- a/docs-website/sidebars.js +++ b/docs-website/sidebars.js @@ -635,6 +635,7 @@ module.exports = { "docs/advanced/browse-paths-upgrade", "docs/browseV2/browse-paths-v2", "docs/plugins", + "docs/advanced/bootstrap-mcps", ], }, { diff --git a/docs/advanced/bootstrap-mcps.md b/docs/advanced/bootstrap-mcps.md new file mode 100644 index 00000000000000..0aa4b7608740f8 --- /dev/null +++ b/docs/advanced/bootstrap-mcps.md @@ -0,0 +1,157 @@ +# Bootstrap MetadataChangeProposals (MCPs) + +Bootstrap MCPs are templated MCPs which are loaded when the `system-update` job runs. This allows adding +entities and aspects to DataHub at install time with the ability to customize them via environment variable +overrides. + +The built-in bootstrap MCP process can also be extended with custom MCPs. This can streamline deployment +scenarios where a set of standard ingestion recipes, data platforms, users groups, or other configuration +can be applied without the need for developing custom scripts. + +## Process Overview + +When DataHub is installed or upgraded, a job runs called `system-update`, this job is responsible for data +migration (particularly Elasticsearch indices) and ensuring the data is prepared for the next version of +DataHub. This is the job which will also apply the bootstrap MCPs. + +The `system-update` job, depending on configuration, can be split into two sequences of steps. If they are +not split, then all steps are blocking. + +1. An initial blocking sequence which is run prior to the new version of GMS and other components +2. Second sequence of steps where GMS and other components are allowed to run while additional data migration steps are +continued in the background + +When applying bootstrap MCPs `system-update` will perform the following steps: + +1. The `bootstrap_mcps.yaml` file is read, either from a default classpath location, `bootstrap_mcps.yaml`, or a filesystem location + provided by an environment variable, `SYSTEM_UPDATE_BOOTSTRAP_MCP_CONFIG`. +2. Depending on the mode of blocking or non-blocking each entry in the configuration file will be executed in sequence. +3. The template MCP file is loaded either from the classpath, or a filesystem location, and the template values are applied. +4. The rendered template MCPs are executed with the options specified in the `bootstrap_mcps.yaml`. + +## `bootstrap_mcps.yaml` Configuration + +The `bootstrap_mcps.yaml` file has the following format. + +```yaml +bootstrap: + templates: + - name: + version: + force: false + blocking: false + async: true + optional: false + mcps_location: + values_env: +``` + +Each entry in the list of templates points to a single yaml file which can contain one or more MCP objects. The +execution of the template MCPs is tracked by name and version to prevent re-execution. The MCP objects are executed once +unless `force=true` for each `name`/`version` combination. + +See the following table of options for descriptions of each field in the template configuration. + +| Field | Default | Required | Description | +|---------------|----------|-----------|------------------------------------------------------------------------------------------------------------| +| name | | `true` | The name for the collection of template MCPs. | +| version | | `true` | A string version for the collection of template MCPs. | +| force | `false` | `false` | Ignores the previous run history, will not skip execution if run previously. | +| blocking | `false` | `false` | Run before GMS and other components during upgrade/install if running in split blocking/non-blocking mode. | +| async | `true` | `false` | Controls whether the MCPs are executed for sync or async ingestion. | +| optional | `false` | `false` | Whether to ignore a failure or fail the entire `system-update` job. | +| mcps_location | | `true` | The location of the file which contains the template MCPs | +| values_env | | `false` | The environment variable which contains override template values. | + +## Template MCPs + +Template MCPs are stored in a yaml file which uses the mustache templating library to populate values from an optional environment +variable. Defaults can be provided inline making override only necessary when providing install/upgrade time configuration. + +In general the file contains a list of MCPs which follow the schema definition for MCPs exactly. Any valid field for an MCP +is accepted, including optional fields such as `headers`. + + +### Example: Native Group + +An example template MCP collection, configuration, and values environment variable is shown below which would create a native group. + +```yaml +- entityUrn: urn:li:corpGroup:{{group.id}} + entityType: corpGroup + aspectName: corpGroupInfo + changeType: UPSERT + aspect: + description: {{group.description}}{{^group.description}}Default description{{/group.description}} + displayName: {{group.displayName}} + created: {{&auditStamp}} + members: [] # required as part of the aspect's schema definition + groups: [] # required as part of the aspect's schema definition + admins: [] # required as part of the aspect's schema definition +- entityUrn: urn:li:corpGroup:{{group.id}} + entityType: corpGroup + aspectName: origin + changeType: UPSERT + aspect: + type: NATIVE +``` + +Creating an entry in the `bootstrap_mcps.yaml` to populate the values from the environment variable `DATAHUB_TEST_GROUP_VALUES` + +```yaml + - name: test-group + version: v1 + mcps_location: "bootstrap_mcps/test-group.yaml" + values_env: "DATAHUB_TEST_GROUP_VALUES" +``` + +An example json values are loaded from environment variable in `DATAHUB_TEST_GROUP_VALUES` might look like the following. + +```json +{"group":{"id":"mygroup", "displayName":"My Group", "description":"Description of the group"}} +``` + +Using standard mustache template semantics the values in the environment would be inserted into the yaml structure +and ingested when the `system-update` runs. + +#### Default values + +In the example above, the group's `description` if not provided would default to `Default description` if not specified +in the values contain in the environment variable override following the standard mustache template semantics. + +#### AuditStamp + +A special template reference, `{{&auditStamp}}` can be used to inject an `auditStamp` into the aspect. This can be used to +populate required fields of type `auditStamp` calculated from when the MCP is applied. This will insert an inline json representation +of the `auditStamp` into the location and avoid escaping html characters per standard mustache template indicated by the `&` character. + +### Ingestion Template MCPs + +Ingestion template MCPs are slightly more complicated since the ingestion `recipe` is stored as a json string within the aspect. +For ingestion recipes, special handling was added so that they can be described naturally in yaml instead of the normally encoded json string. + +This means that in the example below, the structure beneath the `aspect.config.recipe` path will be automatically converted +to the required json structure and stored as a string. + +```yaml +- entityType: dataHubIngestionSource + entityUrn: urn:li:dataHubIngestionSource:demo-data + aspectName: dataHubIngestionSourceInfo + changeType: UPSERT + aspect: + type: 'demo-data' + name: 'demo-data' + config: + recipe: + source: + type: 'datahub-gc' + config: {} + executorId: default +``` + +## Known Limitations + +* Supported change types: + * UPSERT + * CREATE + * CREATE_ENTITY diff --git a/docs/how/add-custom-data-platform.md b/docs/how/add-custom-data-platform.md index 5dcd423e775698..3ffb61c39e5bf5 100644 --- a/docs/how/add-custom-data-platform.md +++ b/docs/how/add-custom-data-platform.md @@ -12,7 +12,7 @@ your custom Data Platform will persist even between full cleans (nukes) of DataH ## Changing Default Data Platforms -Simply make a change to the [data_platforms.json](https://github.com/datahub-project/datahub/blob/master/metadata-service/war/src/main/resources/boot/data_platforms.json) +Simply make a change to the [data_platforms.yaml](https://github.com/datahub-project/datahub/blob/master/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml) file to add a custom Data Platform: ``` diff --git a/docs/what-is-datahub/datahub-concepts.md b/docs/what-is-datahub/datahub-concepts.md index 03b86fab0ede41..8741d445f10f7d 100644 --- a/docs/what-is-datahub/datahub-concepts.md +++ b/docs/what-is-datahub/datahub-concepts.md @@ -99,7 +99,7 @@ List of Data Platforms - Tableau - Vertica -Reference : [data_platforms.json](https://github.com/datahub-project/datahub/blob/master/metadata-service/war/src/main/resources/boot/data_platforms.json) +Reference : [data_platforms.yaml](https://github.com/datahub-project/datahub/blob/master/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml) diff --git a/metadata-ingestion/adding-source.md b/metadata-ingestion/adding-source.md index 6baddf6b2010dc..541f5437b9da85 100644 --- a/metadata-ingestion/adding-source.md +++ b/metadata-ingestion/adding-source.md @@ -240,7 +240,7 @@ in [sql_common.py](./src/datahub/ingestion/source/sql/sql_common.py) if the sour ### 9. Add logo for the platform -Add the logo image in [images folder](../datahub-web-react/src/images) and add it to be ingested at [startup](../metadata-service/war/src/main/resources/boot/data_platforms.json) +Add the logo image in [images folder](../datahub-web-react/src/images) and add it to be ingested at [startup](../metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml) ### 10. Update Frontend for UI-based ingestion diff --git a/metadata-ingestion/src/datahub/ingestion/source/metabase.py b/metadata-ingestion/src/datahub/ingestion/source/metabase.py index 49fa9dab5f1d8d..828bbd213a796f 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/metabase.py +++ b/metadata-ingestion/src/datahub/ingestion/source/metabase.py @@ -725,7 +725,7 @@ def get_datasource_from_id( return "", None, None, None # Map engine names to what datahub expects in - # https://github.com/datahub-project/datahub/blob/master/metadata-service/war/src/main/resources/boot/data_platforms.json + # https://github.com/datahub-project/datahub/blob/master/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml engine = dataset_json.get("engine", "") engine_mapping = { diff --git a/metadata-ingestion/src/datahub/ingestion/source/mode.py b/metadata-ingestion/src/datahub/ingestion/source/mode.py index 56b8ce00a4d1f2..e24cba9b193d31 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/mode.py +++ b/metadata-ingestion/src/datahub/ingestion/source/mode.py @@ -686,7 +686,7 @@ def construct_chart_custom_properties( def _get_datahub_friendly_platform(self, adapter, platform): # Map adaptor names to what datahub expects in - # https://github.com/datahub-project/datahub/blob/master/metadata-service/war/src/main/resources/boot/data_platforms.json + # https://github.com/datahub-project/datahub/blob/master/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml platform_mapping = { "jdbc:athena": "athena", diff --git a/metadata-ingestion/src/datahub/ingestion/source/tableau/tableau_common.py b/metadata-ingestion/src/datahub/ingestion/source/tableau/tableau_common.py index 1fbf31a48890d8..8d6746b6433a4e 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/tableau/tableau_common.py +++ b/metadata-ingestion/src/datahub/ingestion/source/tableau/tableau_common.py @@ -558,7 +558,7 @@ def get_platform(connection_type: str) -> str: # connection_type taken from # https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_connectiontype.htm # datahub platform mapping is found here - # https://github.com/datahub-project/datahub/blob/master/metadata-service/war/src/main/resources/boot/data_platforms.json + # https://github.com/datahub-project/datahub/blob/master/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml if connection_type in ("textscan", "textclean", "excel-direct", "excel", "csv"): platform = "external" diff --git a/metadata-models/docs/entities/dataPlatform.md b/metadata-models/docs/entities/dataPlatform.md index 58ca83c9c6bbc5..23b7c733cd03e6 100644 --- a/metadata-models/docs/entities/dataPlatform.md +++ b/metadata-models/docs/entities/dataPlatform.md @@ -6,4 +6,4 @@ Examples of data platforms are `redshift`, `hive`, `bigquery`, `looker`, `tablea ## Identity -Data Platforms are identified by the name of the technology. A complete list of currently supported data platforms is available [here](https://raw.githubusercontent.com/datahub-project/datahub/master/metadata-service/war/src/main/resources/boot/data_platforms.json). \ No newline at end of file +Data Platforms are identified by the name of the technology. A complete list of currently supported data platforms is available [here](https://github.com/datahub-project/datahub/blob/master/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml). \ No newline at end of file diff --git a/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ObjectMapperContext.java b/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ObjectMapperContext.java index 2e96e48338a661..a25deee23850a2 100644 --- a/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ObjectMapperContext.java +++ b/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ObjectMapperContext.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.StreamReadConstraints; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.linkedin.metadata.Constants; +import java.util.List; import java.util.Optional; import javax.annotation.Nonnull; import lombok.Builder; @@ -14,23 +16,29 @@ public class ObjectMapperContext implements ContextInterface { public static ObjectMapper defaultMapper = new ObjectMapper(); + public static ObjectMapper defaultYamlMapper = new ObjectMapper(new YAMLFactory()); static { defaultMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - int maxSize = - Integer.parseInt( - System.getenv() - .getOrDefault( - Constants.INGESTION_MAX_SERIALIZED_STRING_LENGTH, - Constants.MAX_JACKSON_STRING_SIZE)); - defaultMapper - .getFactory() - .setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(maxSize).build()); + + for (ObjectMapper mapper : List.of(defaultMapper, defaultYamlMapper)) { + int maxSize = + Integer.parseInt( + System.getenv() + .getOrDefault( + Constants.INGESTION_MAX_SERIALIZED_STRING_LENGTH, + Constants.MAX_JACKSON_STRING_SIZE)); + mapper + .getFactory() + .setStreamReadConstraints( + StreamReadConstraints.builder().maxStringLength(maxSize).build()); + } } public static ObjectMapperContext DEFAULT = ObjectMapperContext.builder().build(); @Nonnull private final ObjectMapper objectMapper; + @Nonnull private final ObjectMapper yamlMapper; @Override public Optional getCacheKeyComponent() { @@ -42,7 +50,10 @@ public ObjectMapperContext build() { if (this.objectMapper == null) { objectMapper(defaultMapper); } - return new ObjectMapperContext(this.objectMapper); + if (this.yamlMapper == null) { + yamlMapper(defaultYamlMapper); + } + return new ObjectMapperContext(this.objectMapper, this.yamlMapper); } } } diff --git a/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java b/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java index fc61ccf79544ff..61bf40f54817ee 100644 --- a/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java +++ b/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java @@ -402,6 +402,11 @@ public ObjectMapper getObjectMapper() { return objectMapperContext.getObjectMapper(); } + @Nonnull + public ObjectMapper getYamlMapper() { + return objectMapperContext.getYamlMapper(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml index aaeb8b53682826..3177a971251946 100644 --- a/metadata-service/configuration/src/main/resources/application.yaml +++ b/metadata-service/configuration/src/main/resources/application.yaml @@ -347,8 +347,6 @@ bootstrap: file: ${BOOTSTRAP_POLICIES_FILE:classpath:boot/policies.json} # eg for local file # file: "file:///datahub/datahub-gms/resources/custom-policies.json" - ownershipTypes: - file: ${BOOTSTRAP_OWNERSHIP_TYPES_FILE:classpath:boot/ownership_types.json} servlets: waitTimeout: ${BOOTSTRAP_SERVLETS_WAITTIMEOUT:60} # Total waiting time in seconds for servlets to initialize @@ -357,6 +355,8 @@ systemUpdate: maxBackOffs: ${BOOTSTRAP_SYSTEM_UPDATE_MAX_BACK_OFFS:50} backOffFactor: ${BOOTSTRAP_SYSTEM_UPDATE_BACK_OFF_FACTOR:2} # Multiplicative factor for back off, default values will result in waiting 5min 15s waitForSystemUpdate: ${BOOTSTRAP_SYSTEM_UPDATE_WAIT_FOR_SYSTEM_UPDATE:true} + bootstrap: + mcpConfig: ${SYSTEM_UPDATE_BOOTSTRAP_MCP_CONFIG:bootstrap_mcps.yaml} dataJobNodeCLL: enabled: ${BOOTSTRAP_SYSTEM_UPDATE_DATA_JOB_NODE_CLL_ENABLED:false} batchSize: ${BOOTSTRAP_SYSTEM_UPDATE_DATA_JOB_NODE_CLL_BATCH_SIZE:1000} diff --git a/metadata-service/configuration/src/main/resources/bootstrap_mcps.yaml b/metadata-service/configuration/src/main/resources/bootstrap_mcps.yaml new file mode 100644 index 00000000000000..b1612f95f92198 --- /dev/null +++ b/metadata-service/configuration/src/main/resources/bootstrap_mcps.yaml @@ -0,0 +1,36 @@ +bootstrap: + # Defaults + # force: false + # blocking: false + # async: true + # optional: false + templates: + # Bootstrap + - name: root-user + version: v1 + blocking: true + async: false + mcps_location: "bootstrap_mcps/root-user.yaml" + + - name: data-platforms + version: v1 + mcps_location: "bootstrap_mcps/data-platforms.yaml" + + - name: data-types + version: v1 + mcps_location: "bootstrap_mcps/data-types.yaml" + + - name: ownership-types + version: v1 + mcps_location: "bootstrap_mcps/ownership-types.yaml" + + - name: roles + version: v1 + mcps_location: "bootstrap_mcps/roles.yaml" + + # Ingestion Recipes + - name: ingestion-datahub-gc + version: v1 + optional: true + mcps_location: "bootstrap_mcps/ingestion-datahub-gc.yaml" + values_env: "DATAHUB_GC_BOOTSTRAP_VALUES" \ No newline at end of file diff --git a/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml b/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml new file mode 100644 index 00000000000000..24d5da22805cbe --- /dev/null +++ b/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-platforms.yaml @@ -0,0 +1,709 @@ +# Instructions to add additional entry +# 1. Add new entry to this list +# 2. Increment version in bootstrap_mcps.yaml for the entry referring to this file +- entityUrn: urn:li:dataPlatform:adlsGen1 + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "/" + name: adlsGen1 + displayName: Azure Data Lake (Gen 1) + type: FILE_SYSTEM + logoUrl: "/assets/platforms/adlslogo.png" +- entityUrn: urn:li:dataPlatform:adlsGen2 + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "/" + name: adlsGen2 + displayName: Azure Data Lake (Gen 2) + type: FILE_SYSTEM + logoUrl: "/assets/platforms/adlslogo.png" +- entityUrn: urn:li:dataPlatform:airflow + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: airflow + displayName: Airflow + type: OTHERS + logoUrl: "/assets/platforms/airflowlogo.png" +- entityUrn: urn:li:dataPlatform:ambry + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: ambry + displayName: Ambry + type: OBJECT_STORE +- entityUrn: urn:li:dataPlatform:clickhouse + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: clickhouse + displayName: ClickHouse + type: RELATIONAL_DB + logoUrl: "/assets/platforms/clickhouselogo.png" +- entityUrn: urn:li:dataPlatform:cockroachdb + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: cockroachdb + displayName: CockroachDb + type: RELATIONAL_DB + logoUrl: "/assets/platforms/cockroachdblogo.png" +- entityUrn: urn:li:dataPlatform:couchbase + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: couchbase + displayName: Couchbase + type: KEY_VALUE_STORE + logoUrl: "/assets/platforms/couchbaselogo.png" +- entityUrn: urn:li:dataPlatform:dagster + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "/" + name: dagster + displayName: Dagster + type: OTHERS + logoUrl: "/assets/platforms/dagsterlogo.svg" +- entityUrn: urn:li:dataPlatform:external + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: external + displayName: External Source + type: OTHERS +- entityUrn: urn:li:dataPlatform:hdfs + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "/" + name: hdfs + displayName: HDFS + type: FILE_SYSTEM + logoUrl: "/assets/platforms/hadooplogo.png" +- entityUrn: urn:li:dataPlatform:hana + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: hana + displayName: SAP HANA + type: RELATIONAL_DB + logoUrl: "/assets/platforms/hanalogo.png" +- entityUrn: urn:li:dataPlatform:hive + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: hive + displayName: Hive + type: FILE_SYSTEM + logoUrl: "/assets/platforms/hivelogo.png" +- entityUrn: urn:li:dataPlatform:iceberg + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: iceberg + displayName: Iceberg + type: FILE_SYSTEM + logoUrl: "/assets/platforms/iceberglogo.png" +- entityUrn: urn:li:dataPlatform:s3 + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "/" + name: s3 + displayName: AWS S3 + type: FILE_SYSTEM + logoUrl: "/assets/platforms/s3.png" +- entityUrn: urn:li:dataPlatform:kafka + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: kafka + displayName: Kafka + type: MESSAGE_BROKER + logoUrl: "/assets/platforms/kafkalogo.png" +- entityUrn: urn:li:dataPlatform:kafka-connect + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: kafka-connect + displayName: Kafka Connect + type: OTHERS + logoUrl: "/assets/platforms/kafkalogo.png" +- entityUrn: urn:li:dataPlatform:kusto + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: kusto + displayName: Kusto + type: OLAP_DATASTORE + logoUrl: "/assets/platforms/kustologo.png" +- entityUrn: urn:li:dataPlatform:mode + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: mode + displayName: Mode + type: KEY_VALUE_STORE + logoUrl: "/assets/platforms/modelogo.png" +- entityUrn: urn:li:dataPlatform:mongodb + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: mongodb + displayName: MongoDB + type: KEY_VALUE_STORE + logoUrl: "/assets/platforms/mongodblogo.png" +- entityUrn: urn:li:dataPlatform:mysql + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: mysql + displayName: MySQL + type: RELATIONAL_DB + logoUrl: "/assets/platforms/mysqllogo.png" +- entityUrn: urn:li:dataPlatform:db2 + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: db2 + displayName: DB2 + type: RELATIONAL_DB + logoUrl: "/assets/platforms/db2logo.png" +- entityUrn: urn:li:dataPlatform:mariadb + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: mariadb + displayName: MariaDB + type: RELATIONAL_DB + logoUrl: "/assets/platforms/mariadblogo.png" +- entityUrn: urn:li:dataPlatform:OpenApi + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: openapi + displayName: OpenAPI + type: OTHERS + logoUrl: "/assets/platforms/openapilogo.png" +- entityUrn: urn:li:dataPlatform:oracle + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: oracle + displayName: Oracle + type: RELATIONAL_DB + logoUrl: "/assets/platforms/oraclelogo.png" +- entityUrn: urn:li:dataPlatform:pinot + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: pinot + displayName: Pinot + type: OLAP_DATASTORE + logoUrl: "/assets/platforms/pinotlogo.png" +- entityUrn: urn:li:dataPlatform:postgres + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: postgres + displayName: PostgreSQL + type: RELATIONAL_DB + logoUrl: "/assets/platforms/postgreslogo.png" +- entityUrn: urn:li:dataPlatform:prefect + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: prefect + displayName: Prefect + type: OTHERS + logoUrl: "/assets/platforms/prefectlogo.png" +- entityUrn: urn:li:dataPlatform:presto + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: prefect + displayName: Prefect + type: OTHERS + logoUrl: "/assets/platforms/prefectlogo.png" +- entityUrn: urn:li:dataPlatform:presto + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: presto + displayName: Presto + type: QUERY_ENGINE + logoUrl: "/assets/platforms/prestologo.png" +- entityUrn: urn:li:dataPlatform:tableau + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: tableau + displayName: Tableau + type: OTHERS + logoUrl: "/assets/platforms/tableaulogo.svg" +- entityUrn: urn:li:dataPlatform:teradata + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: teradata + displayName: Teradata + type: RELATIONAL_DB + logoUrl: "/assets/platforms/teradatalogo.png" +- entityUrn: urn:li:dataPlatform:voldemort + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: voldemort + displayName: Voldemort + type: KEY_VALUE_STORE +- entityUrn: urn:li:dataPlatform:snowflake + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: snowflake + displayName: Snowflake + type: RELATIONAL_DB + logoUrl: "/assets/platforms/snowflakelogo.png" +- entityUrn: urn:li:dataPlatform:redshift + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: redshift + displayName: Redshift + type: RELATIONAL_DB + logoUrl: "/assets/platforms/redshiftlogo.png" +- entityUrn: urn:li:dataPlatform:mssql + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: mssql + displayName: SQL Server + type: RELATIONAL_DB + logoUrl: "/assets/platforms/mssqllogo.png" +- entityUrn: urn:li:dataPlatform:bigquery + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: bigquery + displayName: BigQuery + type: RELATIONAL_DB + logoUrl: "/assets/platforms/bigquerylogo.png" +- entityUrn: urn:li:dataPlatform:druid + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: druid + displayName: Druid + type: OLAP_DATASTORE + logoUrl: "/assets/platforms/druidlogo.png" +- entityUrn: urn:li:dataPlatform:looker + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: looker + displayName: Looker + type: OTHERS + logoUrl: "/assets/platforms/lookerlogo.svg" +- entityUrn: urn:li:dataPlatform:feast + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: feast + displayName: Feast + type: OTHERS + logoUrl: "/assets/platforms/feastlogo.png" +- entityUrn: urn:li:dataPlatform:sagemaker + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: sagemaker + displayName: SageMaker + type: OTHERS + logoUrl: "/assets/platforms/sagemakerlogo.png" +- entityUrn: urn:li:dataPlatform:mlflow + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: mlflow + displayName: MLflow + type: OTHERS + logoUrl: "/assets/platforms/mlflowlogo.png" +- entityUrn: urn:li:dataPlatform:glue + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: glue + displayName: Glue + type: OTHERS + logoUrl: "/assets/platforms/gluelogo.png" +- entityUrn: urn:li:dataPlatform:redash + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: redash + displayName: Redash + type: OTHERS + logoUrl: "/assets/platforms/redashlogo.png" +- entityUrn: urn:li:dataPlatform:athena + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: athena + displayName: AWS Athena + type: RELATIONAL_DB + logoUrl: "/assets/platforms/awsathenalogo.png" +- entityUrn: urn:li:dataPlatform:spark + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: spark + displayName: Spark + type: OTHERS + logoUrl: "/assets/platforms/sparklogo.png" +- entityUrn: urn:li:dataPlatform:dbt + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: dbt + displayName: dbt + type: OTHERS + logoUrl: "/assets/platforms/dbtlogo.png" +- entityUrn: urn:li:dataPlatform:elasticsearch + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: elasticsearch + displayName: Elasticsearch + type: OTHERS + logoUrl: "/assets/platforms/elasticsearchlogo.png" +- entityUrn: urn:li:dataPlatform:great-expectations + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + name: Great Expectations + displayName: Great Expectations + type: OTHERS + logoUrl: "/assets/platforms/greatexpectationslogo.png" + datasetNameDelimiter: "." +- entityUrn: urn:li:dataPlatform:powerbi + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: powerbi + displayName: Power BI + type: OTHERS + logoUrl: "/assets/platforms/powerbilogo.png" +- entityUrn: urn:li:dataPlatform:presto-on-hive + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: presto-on-hive + displayName: Presto on Hive + type: FILE_SYSTEM + logoUrl: "/assets/platforms/prestoonhivelogo.png" +- entityUrn: urn:li:dataPlatform:metabase + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: metabase + displayName: Metabase + type: OTHERS + logoUrl: "/assets/platforms/metabaselogo.svg" +- entityUrn: urn:li:dataPlatform:nifi + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: nifi + displayName: NiFi + type: OTHERS + logoUrl: "/assets/platforms/nifilogo.svg" +- entityUrn: urn:li:dataPlatform:superset + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: superset + displayName: Superset + type: OTHERS + logoUrl: "/assets/platforms/supersetlogo.png" +- entityUrn: urn:li:dataPlatform:trino + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: trino + displayName: Trino + type: QUERY_ENGINE + logoUrl: "/assets/platforms/trinologo.png" +- entityUrn: urn:li:dataPlatform:pulsar + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: pulsar + displayName: Pulsar + type: MESSAGE_BROKER + logoUrl: "/assets/platforms/pulsarlogo.png" +- entityUrn: urn:li:dataPlatform:salesforce + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: salesforce + displayName: Salesforce + type: OTHERS + logoUrl: "/assets/platforms/logo-salesforce.svg" +- entityUrn: urn:li:dataPlatform:unknown + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: Unknown Platform + displayName: N/A + type: OTHERS +- entityUrn: urn:li:dataPlatform:delta-lake + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: delta-lake + displayName: Delta Lake + type: OTHERS + logoUrl: "/assets/platforms/deltalakelogo.png" +- entityUrn: urn:li:dataPlatform:databricks + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: databricks + displayName: Databricks + type: OTHERS + logoUrl: "/assets/platforms/databrickslogo.png" +- entityUrn: urn:li:dataPlatform:vertica + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: vertica + displayName: Vertica + type: OLAP_DATASTORE + logoUrl: "/assets/platforms/verticalogo.png" +- entityUrn: urn:li:dataPlatform:gcs + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "/" + name: gcs + displayName: Google Cloud Storage + type: FILE_SYSTEM + logoUrl: "/assets/platforms/gcslogo.svg" +- entityUrn: urn:li:dataPlatform:slack + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: Slack + displayName: Slack + type: OTHERS + logoUrl: "/assets/platforms/slacklogo.png" +- entityUrn: urn:li:dataPlatform:microsoft-teams + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: Microsoft Teams + displayName: Microsoft Teams + type: OTHERS + logoUrl: "/assets/platforms/teamslogo.png" +- entityUrn: urn:li:dataPlatform:dynamodb + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: dynamodb + displayName: DynamoDB + type: KEY_VALUE_STORE + logoUrl: "/assets/platforms/dynamodblogo.png" +- entityUrn: urn:li:dataPlatform:fivetran + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: fivetran + displayName: Fivetran + type: OTHERS + logoUrl: "/assets/platforms/fivetranlogo.png" +- entityUrn: urn:li:dataPlatform:csv + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: csv + displayName: CSV + type: OTHERS + logoUrl: "/assets/platforms/csv-logo.png" +- entityUrn: urn:li:dataPlatform:qlik-sense + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: qlik-sense + displayName: Qlik Sense + type: OTHERS + logoUrl: "/assets/platforms/qliklogo.png" +- entityUrn: urn:li:dataPlatform:file + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: file + displayName: File + type: OTHERS + logoUrl: "/assets/platforms/file-logo.svg" +- entityUrn: urn:li:dataPlatform:excel + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + name: excel + displayName: Excel + type: OTHERS + datasetNameDelimiter: "/" + logoUrl: "/assets/platforms/excel-logo.svg" +- entityUrn: urn:li:dataPlatform:sigma + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: sigma + displayName: Sigma + type: OTHERS + logoUrl: "/assets/platforms/sigmalogo.png" +- entityUrn: urn:li:dataPlatform:sac + entityType: dataPlatform + aspectName: dataPlatformInfo + changeType: UPSERT + aspect: + datasetNameDelimiter: "." + name: sac + displayName: SAP Analytics Cloud + type: OTHERS + logoUrl: "/assets/platforms/saclogo.svg" diff --git a/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-types.yaml b/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-types.yaml new file mode 100644 index 00000000000000..e73288f8171438 --- /dev/null +++ b/metadata-service/configuration/src/main/resources/bootstrap_mcps/data-types.yaml @@ -0,0 +1,43 @@ +# Instructions to add additional entry +# 1. Add new entry to this list +# 2. Increment version in bootstrap_mcps.yaml for the entry referring to this file +- entityUrn: urn:li:dataType:datahub.string + entityType: dataType + aspectName: dataTypeInfo + changeType: UPSERT + aspect: + qualifiedName: datahub.string + displayName: String + description: A string of characters. +- entityUrn: urn:li:dataType:datahub.number + entityType: dataType + aspectName: dataTypeInfo + changeType: UPSERT + aspect: + qualifiedName: datahub.number + displayName: Number + description: An integer or decimal number. +- entityUrn: urn:li:dataType:datahub.urn + entityType: dataType + aspectName: dataTypeInfo + changeType: UPSERT + aspect: + qualifiedName: datahub.urn + displayName: Urn + description: An unique identifier for a DataHub entity. +- entityUrn: urn:li:dataType:datahub.rich_text + entityType: dataType + aspectName: dataTypeInfo + changeType: UPSERT + aspect: + qualifiedName: datahub.rich_text + displayName: Rich Text + description: An attributed string of characters. +- entityUrn: urn:li:dataType:datahub.date + entityType: dataType + aspectName: dataTypeInfo + changeType: UPSERT + aspect: + qualifiedName: datahub.date + displayName: Date + description: A specific day, without time. \ No newline at end of file diff --git a/metadata-service/configuration/src/main/resources/bootstrap_mcps/ingestion-datahub-gc.yaml b/metadata-service/configuration/src/main/resources/bootstrap_mcps/ingestion-datahub-gc.yaml new file mode 100644 index 00000000000000..aa3c768dcf1b89 --- /dev/null +++ b/metadata-service/configuration/src/main/resources/bootstrap_mcps/ingestion-datahub-gc.yaml @@ -0,0 +1,33 @@ +# Instructions to add additional entry or update on the target system +# 1. Edit this file +# 2. Increment version in bootstrap_mcps.yaml for the entry referring to this file +- entityType: dataHubIngestionSource + entityUrn: urn:li:dataHubIngestionSource:datahub-gc + aspectName: dataHubIngestionSourceInfo + changeType: UPSERT + aspect: + type: 'datahub-gc' + name: '{{ingestion.name}}{{^ingestion.name}}datahub-gc{{/ingestion.name}}' + schedule: + timezone: '{{schedule.timezone}}{{^schedule.timezone}}UTC{{/schedule.timezone}}' + interval: '{{schedule.interval}}{{^schedule.interval}}0 1 * * *{{/schedule.interval}}' + config: + version: 0.14.1.1rc5 + recipe: + source: + type: 'datahub-gc' + config: + cleanup_expired_tokens: {{cleanup_expired_tokens}}{{^cleanup_expired_tokens}}false{{/cleanup_expired_tokens}} + truncate_indices: {{truncate_indices}}{{^truncate_indices}}true{{/truncate_indices}} + dataprocess_cleanup: + retention_days: {{dataprocess_cleanup.retention_days}}{{^dataprocess_cleanup.retention_days}}10{{/dataprocess_cleanup.retention_days}} + delete_empty_data_jobs: {{dataprocess_cleanup.delete_empty_data_jobs}}{{^dataprocess_cleanup.delete_empty_data_jobs}}true{{/dataprocess_cleanup.delete_empty_data_jobs}} + delete_empty_data_flows: {{dataprocess_cleanup.delete_empty_data_flows}}{{^dataprocess_cleanup.delete_empty_data_flows}}true{{/dataprocess_cleanup.delete_empty_data_flows}} + hard_delete_entities: {{dataprocess_cleanup.hard_delete_entities}}{{^dataprocess_cleanup.hard_delete_entities}}false{{/dataprocess_cleanup.hard_delete_entities}} + keep_last_n: {{dataprocess_cleanup.keep_last_n}}{{^dataprocess_cleanup.keep_last_n}}5{{/dataprocess_cleanup.keep_last_n}} + soft_deleted_entities_cleanup: + retention_days: {{soft_deleted_entities_cleanup.retention_days}}{{^soft_deleted_entities_cleanup.retention_days}}10{{/soft_deleted_entities_cleanup.retention_days}} + extraArgs: {} + debugMode: false + executorId: default + headers: {} \ No newline at end of file diff --git a/metadata-service/configuration/src/main/resources/bootstrap_mcps/ownership-types.yaml b/metadata-service/configuration/src/main/resources/bootstrap_mcps/ownership-types.yaml new file mode 100644 index 00000000000000..23d1f37b76138c --- /dev/null +++ b/metadata-service/configuration/src/main/resources/bootstrap_mcps/ownership-types.yaml @@ -0,0 +1,39 @@ +# Instructions to add additional entry +# 1. Add new entry to this list +# 2. Increment version in bootstrap_mcps.yaml for the entry referring to this file +- entityUrn: urn:li:ownershipType:__system__technical_owner + entityType: ownershipType + aspectName: ownershipTypeInfo + changeType: UPSERT + aspect: + name: Technical Owner + description: Involved in the production, maintenance, or distribution of the asset(s). + created: {{&auditStamp}} + lastModified: {{&auditStamp}} +- entityUrn: urn:li:ownershipType:__system__business_owner + entityType: ownershipType + aspectName: ownershipTypeInfo + changeType: UPSERT + aspect: + name: Business Owner + description: Principle stakeholders or domain experts associated with the asset(s). + created: {{&auditStamp}} + lastModified: {{&auditStamp}} +- entityUrn: urn:li:ownershipType:__system__data_steward + entityType: ownershipType + aspectName: ownershipTypeInfo + changeType: UPSERT + aspect: + name: Data Steward + description: Involved in governance of the asset(s). + created: {{&auditStamp}} + lastModified: {{&auditStamp}} +- entityUrn: urn:li:ownershipType:__system__none + entityType: ownershipType + aspectName: ownershipTypeInfo + changeType: UPSERT + aspect: + name: None + description: No ownership type specified. + created: {{&auditStamp}} + lastModified: {{&auditStamp}} diff --git a/metadata-service/configuration/src/main/resources/bootstrap_mcps/roles.yaml b/metadata-service/configuration/src/main/resources/bootstrap_mcps/roles.yaml new file mode 100644 index 00000000000000..274b4fce1c3bff --- /dev/null +++ b/metadata-service/configuration/src/main/resources/bootstrap_mcps/roles.yaml @@ -0,0 +1,28 @@ +# Instructions to add additional entry +# 1. Add new entry to this list +# 2. Increment version in bootstrap_mcps.yaml for the entry referring to this file +- entityUrn: urn:li:dataHubRole:Admin + entityType: dataHubRole + aspectName: dataHubRoleInfo + changeType: UPSERT + aspect: + name: Admin + description: Can do everything on the platform. + editable: false +- entityUrn: urn:li:dataHubRole:Editor + entityType: dataHubRole + aspectName: dataHubRoleInfo + changeType: UPSERT + aspect: + name: Editor + description: Can read and edit all metadata. Cannot take administrative actions. + editable: false +- entityUrn: urn:li:dataHubRole:Reader + entityType: dataHubRole + aspectName: dataHubRoleInfo + changeType: UPSERT + aspect: + name: Reader + description: Can read all metadata. Cannot edit anything by default, or take administrative + actions. + editable: false \ No newline at end of file diff --git a/metadata-service/configuration/src/main/resources/bootstrap_mcps/root-user.yaml b/metadata-service/configuration/src/main/resources/bootstrap_mcps/root-user.yaml new file mode 100644 index 00000000000000..40d33468f0168a --- /dev/null +++ b/metadata-service/configuration/src/main/resources/bootstrap_mcps/root-user.yaml @@ -0,0 +1,8 @@ +- entityUrn: urn:li:corpuser:datahub + entityType: corpuser + aspectName: corpUserInfo + changeType: UPSERT + aspect: + active: true + displayName: DataHub + title: DataHub Root User diff --git a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/factories/BootstrapManagerFactory.java b/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/factories/BootstrapManagerFactory.java index 9e29883f439a74..ffc739e905cd68 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/factories/BootstrapManagerFactory.java +++ b/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/factories/BootstrapManagerFactory.java @@ -11,15 +11,10 @@ import com.linkedin.metadata.boot.dependencies.BootstrapDependency; import com.linkedin.metadata.boot.steps.IndexDataPlatformsStep; import com.linkedin.metadata.boot.steps.IngestDataPlatformInstancesStep; -import com.linkedin.metadata.boot.steps.IngestDataPlatformsStep; -import com.linkedin.metadata.boot.steps.IngestDataTypesStep; import com.linkedin.metadata.boot.steps.IngestDefaultGlobalSettingsStep; import com.linkedin.metadata.boot.steps.IngestEntityTypesStep; -import com.linkedin.metadata.boot.steps.IngestOwnershipTypesStep; import com.linkedin.metadata.boot.steps.IngestPoliciesStep; import com.linkedin.metadata.boot.steps.IngestRetentionPoliciesStep; -import com.linkedin.metadata.boot.steps.IngestRolesStep; -import com.linkedin.metadata.boot.steps.IngestRootUserStep; import com.linkedin.metadata.boot.steps.RemoveClientIdAspectStep; import com.linkedin.metadata.boot.steps.RestoreColumnLineageIndices; import com.linkedin.metadata.boot.steps.RestoreDbtSiblingsIndices; @@ -90,21 +85,14 @@ public class BootstrapManagerFactory { @Value("${bootstrap.policies.file}") private Resource _policiesResource; - @Value("${bootstrap.ownershipTypes.file}") - private Resource _ownershipTypesResource; - @Bean(name = "bootstrapManager") @Scope("singleton") @Nonnull protected BootstrapManager createInstance( @Qualifier("systemOperationContext") final OperationContext systemOpContext) { - final IngestRootUserStep ingestRootUserStep = new IngestRootUserStep(_entityService); final IngestPoliciesStep ingestPoliciesStep = new IngestPoliciesStep( _entityService, _entitySearchService, _searchDocumentTransformer, _policiesResource); - final IngestRolesStep ingestRolesStep = new IngestRolesStep(_entityService, _entityRegistry); - final IngestDataPlatformsStep ingestDataPlatformsStep = - new IngestDataPlatformsStep(_entityService); final IngestDataPlatformInstancesStep ingestDataPlatformInstancesStep = new IngestDataPlatformInstancesStep(_entityService, _migrationsDao); final RestoreGlossaryIndices restoreGlossaryIndicesStep = @@ -121,29 +109,21 @@ protected BootstrapManager createInstance( new IngestDefaultGlobalSettingsStep(_entityService); final WaitForSystemUpdateStep waitForSystemUpdateStep = new WaitForSystemUpdateStep(_dataHubUpgradeKafkaListener, _configurationProvider); - final IngestOwnershipTypesStep ingestOwnershipTypesStep = - new IngestOwnershipTypesStep(_entityService, _ownershipTypesResource); - final IngestDataTypesStep ingestDataTypesStep = new IngestDataTypesStep(_entityService); final IngestEntityTypesStep ingestEntityTypesStep = new IngestEntityTypesStep(_entityService); final List finalSteps = new ArrayList<>( ImmutableList.of( waitForSystemUpdateStep, - ingestRootUserStep, ingestPoliciesStep, - ingestRolesStep, - ingestDataPlatformsStep, ingestDataPlatformInstancesStep, _ingestRetentionPoliciesStep, - ingestOwnershipTypesStep, ingestSettingsStep, restoreGlossaryIndicesStep, removeClientIdAspectStep, restoreDbtSiblingsIndices, indexDataPlatformsStep, restoreColumnLineageIndices, - ingestDataTypesStep, ingestEntityTypesStep)); return new BootstrapManager(finalSteps); diff --git a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestDataPlatformsStep.java b/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestDataPlatformsStep.java deleted file mode 100644 index f88343b6db322b..00000000000000 --- a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestDataPlatformsStep.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.linkedin.metadata.boot.steps; - -import static com.linkedin.metadata.Constants.*; - -import com.datahub.util.RecordUtils; -import com.fasterxml.jackson.core.StreamReadConstraints; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.linkedin.common.AuditStamp; -import com.linkedin.common.urn.Urn; -import com.linkedin.dataplatform.DataPlatformInfo; -import com.linkedin.metadata.Constants; -import com.linkedin.metadata.boot.BootstrapStep; -import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl; -import com.linkedin.metadata.entity.ebean.batch.ChangeItemImpl; -import io.datahubproject.metadata.context.OperationContext; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.List; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; -import javax.annotation.Nonnull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.ClassPathResource; - -@Slf4j -@RequiredArgsConstructor -public class IngestDataPlatformsStep implements BootstrapStep { - - private static final String PLATFORM_ASPECT_NAME = "dataPlatformInfo"; - - private final EntityService _entityService; - - @Override - public String name() { - return "IngestDataPlatformsStep"; - } - - @Override - public void execute(@Nonnull OperationContext systemOperationContext) - throws IOException, URISyntaxException { - - final ObjectMapper mapper = new ObjectMapper(); - int maxSize = - Integer.parseInt( - System.getenv() - .getOrDefault(INGESTION_MAX_SERIALIZED_STRING_LENGTH, MAX_JACKSON_STRING_SIZE)); - mapper - .getFactory() - .setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(maxSize).build()); - - // 1. Read from the file into JSON. - final JsonNode dataPlatforms = - mapper.readTree(new ClassPathResource("./boot/data_platforms.json").getFile()); - - if (!dataPlatforms.isArray()) { - throw new RuntimeException( - String.format( - "Found malformed data platforms file, expected an Array but found %s", - dataPlatforms.getNodeType())); - } - - // 2. For each JSON object, cast into a DataPlatformSnapshot object. - List dataPlatformAspects = - StreamSupport.stream( - Spliterators.spliteratorUnknownSize(dataPlatforms.iterator(), Spliterator.ORDERED), - false) - .map( - dataPlatform -> { - final String urnString; - final Urn urn; - try { - urnString = dataPlatform.get("urn").asText(); - urn = Urn.createFromString(urnString); - } catch (URISyntaxException e) { - log.error("Malformed urn: {}", dataPlatform.get("urn").asText()); - throw new RuntimeException("Malformed urn", e); - } - - final DataPlatformInfo info = - RecordUtils.toRecordTemplate( - DataPlatformInfo.class, dataPlatform.get("aspect").toString()); - - try { - return ChangeItemImpl.builder() - .urn(urn) - .aspectName(PLATFORM_ASPECT_NAME) - .recordTemplate(info) - .auditStamp( - new AuditStamp() - .setActor(Urn.createFromString(Constants.SYSTEM_ACTOR)) - .setTime(System.currentTimeMillis())) - .build( - systemOperationContext - .getRetrieverContext() - .get() - .getAspectRetriever()); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toList()); - - _entityService.ingestAspects( - systemOperationContext, - AspectsBatchImpl.builder() - .retrieverContext(systemOperationContext.getRetrieverContext().get()) - .items(dataPlatformAspects) - .build(), - true, - false); - } -} diff --git a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestDataTypesStep.java b/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestDataTypesStep.java deleted file mode 100644 index 1ac3aeb2daed0e..00000000000000 --- a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestDataTypesStep.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.linkedin.metadata.boot.steps; - -import static com.linkedin.metadata.Constants.*; - -import com.datahub.util.RecordUtils; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.linkedin.common.AuditStamp; -import com.linkedin.common.urn.Urn; -import com.linkedin.datatype.DataTypeInfo; -import com.linkedin.events.metadata.ChangeType; -import com.linkedin.metadata.boot.BootstrapStep; -import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.utils.GenericRecordUtils; -import com.linkedin.mxe.MetadataChangeProposal; -import io.datahubproject.metadata.context.OperationContext; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import javax.annotation.Nonnull; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.ClassPathResource; - -/** This bootstrap step is responsible for ingesting default data types. */ -@Slf4j -public class IngestDataTypesStep implements BootstrapStep { - - private static final String DEFAULT_FILE_PATH = "./boot/data_types.json"; - private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); - private final EntityService _entityService; - private final String _resourcePath; - - public IngestDataTypesStep(@Nonnull final EntityService entityService) { - this(entityService, DEFAULT_FILE_PATH); - } - - public IngestDataTypesStep( - @Nonnull final EntityService entityService, @Nonnull final String filePath) { - _entityService = Objects.requireNonNull(entityService, "entityService must not be null"); - _resourcePath = filePath; - } - - @Override - public String name() { - return "IngestDataTypesStep"; - } - - @Override - public void execute(@Nonnull OperationContext systemOperationContext) throws Exception { - log.info("Ingesting default data types..."); - - // 1. Read from the file into JSON. - final JsonNode dataTypesObj = - JSON_MAPPER.readTree(new ClassPathResource(_resourcePath).getFile()); - - if (!dataTypesObj.isArray()) { - throw new RuntimeException( - String.format( - "Found malformed data types file, expected an Array but found %s", - dataTypesObj.getNodeType())); - } - - log.info("Ingesting {} data types types", dataTypesObj.size()); - int numIngested = 0; - - Map urnDataTypesMap = new HashMap<>(); - for (final JsonNode roleObj : dataTypesObj) { - final Urn urn = Urn.createFromString(roleObj.get("urn").asText()); - urnDataTypesMap.put(urn, roleObj); - } - - Set existingUrns = _entityService.exists(systemOperationContext, urnDataTypesMap.keySet()); - - for (final Map.Entry entry : urnDataTypesMap.entrySet()) { - if (!existingUrns.contains(entry.getKey())) { - final DataTypeInfo info = - RecordUtils.toRecordTemplate( - DataTypeInfo.class, entry.getValue().get("info").toString()); - log.info(String.format("Ingesting default data type with urn %s", entry.getKey())); - ingestDataType(systemOperationContext, entry.getKey(), info); - numIngested++; - } - } - log.info("Ingested {} new data types", numIngested); - } - - private void ingestDataType( - @Nonnull OperationContext systemOperationContext, - final Urn dataTypeUrn, - final DataTypeInfo info) - throws Exception { - final MetadataChangeProposal proposal = new MetadataChangeProposal(); - proposal.setEntityUrn(dataTypeUrn); - proposal.setEntityType(DATA_TYPE_ENTITY_NAME); - proposal.setAspectName(DATA_TYPE_INFO_ASPECT_NAME); - proposal.setAspect(GenericRecordUtils.serializeAspect(info)); - proposal.setChangeType(ChangeType.UPSERT); - - _entityService.ingestProposal( - systemOperationContext, - proposal, - new AuditStamp() - .setActor(Urn.createFromString(SYSTEM_ACTOR)) - .setTime(System.currentTimeMillis()), - false); - } -} diff --git a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestOwnershipTypesStep.java b/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestOwnershipTypesStep.java deleted file mode 100644 index 4488849f34ca91..00000000000000 --- a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestOwnershipTypesStep.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.linkedin.metadata.boot.steps; - -import static com.linkedin.metadata.Constants.*; - -import com.datahub.util.RecordUtils; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.linkedin.common.AuditStamp; -import com.linkedin.common.urn.Urn; -import com.linkedin.events.metadata.ChangeType; -import com.linkedin.metadata.Constants; -import com.linkedin.metadata.boot.BootstrapStep; -import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl; -import com.linkedin.metadata.models.AspectSpec; -import com.linkedin.metadata.utils.EntityKeyUtils; -import com.linkedin.metadata.utils.GenericRecordUtils; -import com.linkedin.mxe.GenericAspect; -import com.linkedin.mxe.MetadataChangeProposal; -import com.linkedin.ownership.OwnershipTypeInfo; -import io.datahubproject.metadata.context.OperationContext; -import java.util.List; -import javax.annotation.Nonnull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.Resource; - -/** - * This bootstrap step is responsible for ingesting default ownership types. - * - *

If system has never bootstrapped this step will: For each ownership type defined in the yaml - * file, it checks whether the urn exists. If not, it ingests the ownership type into DataHub. - */ -@Slf4j -@RequiredArgsConstructor -public class IngestOwnershipTypesStep implements BootstrapStep { - - private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); - private final EntityService _entityService; - private final Resource _ownershipTypesResource; - - @Override - public String name() { - return "IngestOwnershipTypesStep"; - } - - @Override - public void execute(@Nonnull OperationContext systemOperationContext) throws Exception { - log.info("Ingesting default ownership types from {}...", _ownershipTypesResource); - - // 1. Read from the file into JSON. - final JsonNode ownershipTypesObj = JSON_MAPPER.readTree(_ownershipTypesResource.getFile()); - - if (!ownershipTypesObj.isArray()) { - throw new RuntimeException( - String.format( - "Found malformed ownership file, expected an Array but found %s", - ownershipTypesObj.getNodeType())); - } - - final AuditStamp auditStamp = - new AuditStamp() - .setActor(Urn.createFromString(Constants.SYSTEM_ACTOR)) - .setTime(System.currentTimeMillis()); - - log.info("Ingesting {} ownership types", ownershipTypesObj.size()); - int numIngested = 0; - for (final JsonNode roleObj : ownershipTypesObj) { - final Urn urn = Urn.createFromString(roleObj.get("urn").asText()); - final OwnershipTypeInfo info = - RecordUtils.toRecordTemplate(OwnershipTypeInfo.class, roleObj.get("info").toString()); - log.info(String.format("Ingesting default ownership type with urn %s", urn)); - ingestOwnershipType(systemOperationContext, urn, info, auditStamp); - numIngested++; - } - log.info("Ingested {} new ownership types", numIngested); - } - - private void ingestOwnershipType( - @Nonnull OperationContext systemOperationContext, - final Urn ownershipTypeUrn, - final OwnershipTypeInfo info, - final AuditStamp auditStamp) { - - // 3. Write key & aspect MCPs. - final MetadataChangeProposal keyAspectProposal = new MetadataChangeProposal(); - final AspectSpec keyAspectSpec = - systemOperationContext.getEntityRegistryContext().getKeyAspectSpec(ownershipTypeUrn); - GenericAspect aspect = - GenericRecordUtils.serializeAspect( - EntityKeyUtils.convertUrnToEntityKey(ownershipTypeUrn, keyAspectSpec)); - keyAspectProposal.setAspect(aspect); - keyAspectProposal.setAspectName(keyAspectSpec.getName()); - keyAspectProposal.setEntityType(OWNERSHIP_TYPE_ENTITY_NAME); - keyAspectProposal.setChangeType(ChangeType.UPSERT); - keyAspectProposal.setEntityUrn(ownershipTypeUrn); - - final MetadataChangeProposal proposal = new MetadataChangeProposal(); - proposal.setEntityUrn(ownershipTypeUrn); - proposal.setEntityType(OWNERSHIP_TYPE_ENTITY_NAME); - proposal.setAspectName(OWNERSHIP_TYPE_INFO_ASPECT_NAME); - info.setCreated(auditStamp); - info.setLastModified(auditStamp); - proposal.setAspect(GenericRecordUtils.serializeAspect(info)); - proposal.setChangeType(ChangeType.UPSERT); - - _entityService.ingestProposal( - systemOperationContext, - AspectsBatchImpl.builder() - .mcps( - List.of(keyAspectProposal, proposal), - auditStamp, - systemOperationContext.getRetrieverContext().get()) - .build(), - false); - } -} diff --git a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestRolesStep.java b/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestRolesStep.java deleted file mode 100644 index 449336268e34f2..00000000000000 --- a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestRolesStep.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.linkedin.metadata.boot.steps; - -import static com.linkedin.metadata.Constants.*; - -import com.datahub.util.RecordUtils; -import com.fasterxml.jackson.core.StreamReadConstraints; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.linkedin.common.AuditStamp; -import com.linkedin.common.urn.Urn; -import com.linkedin.events.metadata.ChangeType; -import com.linkedin.metadata.Constants; -import com.linkedin.metadata.boot.BootstrapStep; -import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl; -import com.linkedin.metadata.models.AspectSpec; -import com.linkedin.metadata.models.registry.EntityRegistry; -import com.linkedin.metadata.utils.EntityKeyUtils; -import com.linkedin.metadata.utils.GenericRecordUtils; -import com.linkedin.mxe.GenericAspect; -import com.linkedin.mxe.MetadataChangeProposal; -import com.linkedin.policy.DataHubRoleInfo; -import io.datahubproject.metadata.context.OperationContext; -import jakarta.annotation.Nonnull; -import java.net.URISyntaxException; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.ClassPathResource; - -@Slf4j -@RequiredArgsConstructor -public class IngestRolesStep implements BootstrapStep { - private static final int SLEEP_SECONDS = 60; - private final EntityService _entityService; - private final EntityRegistry _entityRegistry; - - @Override - public String name() { - return this.getClass().getSimpleName(); - } - - @Nonnull - @Override - public ExecutionMode getExecutionMode() { - return ExecutionMode.ASYNC; - } - - @Override - public void execute(@Nonnull OperationContext systemOperationContext) throws Exception { - final ObjectMapper mapper = new ObjectMapper(); - int maxSize = - Integer.parseInt( - System.getenv() - .getOrDefault(INGESTION_MAX_SERIALIZED_STRING_LENGTH, MAX_JACKSON_STRING_SIZE)); - mapper - .getFactory() - .setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(maxSize).build()); - - // Sleep to ensure deployment process finishes. - Thread.sleep(SLEEP_SECONDS * 1000); - - // 0. Execute preflight check to see whether we need to ingest Roles - log.info("Ingesting default Roles..."); - - // 1. Read from the file into JSON. - final JsonNode rolesObj = mapper.readTree(new ClassPathResource("./boot/roles.json").getFile()); - - if (!rolesObj.isArray()) { - throw new RuntimeException( - String.format( - "Found malformed roles file, expected an Array but found %s", - rolesObj.getNodeType())); - } - - final AspectSpec roleInfoAspectSpec = - _entityRegistry - .getEntitySpec(DATAHUB_ROLE_ENTITY_NAME) - .getAspectSpec(DATAHUB_ROLE_INFO_ASPECT_NAME); - final AuditStamp auditStamp = - new AuditStamp() - .setActor(Urn.createFromString(Constants.SYSTEM_ACTOR)) - .setTime(System.currentTimeMillis()); - - for (final JsonNode roleObj : rolesObj) { - final Urn urn = Urn.createFromString(roleObj.get("urn").asText()); - - // If the info is not there, it means that the role was there before, but must now be removed - if (!roleObj.has("info")) { - _entityService.deleteUrn(systemOperationContext, urn); - continue; - } - - final DataHubRoleInfo info = - RecordUtils.toRecordTemplate(DataHubRoleInfo.class, roleObj.get("info").toString()); - ingestRole(systemOperationContext, urn, info, auditStamp, roleInfoAspectSpec); - } - - log.info("Successfully ingested default Roles."); - } - - private void ingestRole( - @Nonnull OperationContext systemOperationContext, - final Urn roleUrn, - final DataHubRoleInfo dataHubRoleInfo, - final AuditStamp auditStamp, - final AspectSpec roleInfoAspectSpec) - throws URISyntaxException { - // 3. Write key & aspect - final MetadataChangeProposal keyAspectProposal = new MetadataChangeProposal(); - final AspectSpec keyAspectSpec = - systemOperationContext.getEntityRegistryContext().getKeyAspectSpec(roleUrn); - GenericAspect aspect = - GenericRecordUtils.serializeAspect( - EntityKeyUtils.convertUrnToEntityKey(roleUrn, keyAspectSpec)); - keyAspectProposal.setAspect(aspect); - keyAspectProposal.setAspectName(keyAspectSpec.getName()); - keyAspectProposal.setEntityType(DATAHUB_ROLE_ENTITY_NAME); - keyAspectProposal.setChangeType(ChangeType.UPSERT); - keyAspectProposal.setEntityUrn(roleUrn); - - final MetadataChangeProposal proposal = new MetadataChangeProposal(); - proposal.setEntityUrn(roleUrn); - proposal.setEntityType(DATAHUB_ROLE_ENTITY_NAME); - proposal.setAspectName(DATAHUB_ROLE_INFO_ASPECT_NAME); - proposal.setAspect(GenericRecordUtils.serializeAspect(dataHubRoleInfo)); - proposal.setChangeType(ChangeType.UPSERT); - - _entityService.ingestProposal( - systemOperationContext, - AspectsBatchImpl.builder() - .mcps( - List.of(keyAspectProposal, proposal), - new AuditStamp() - .setActor(Urn.createFromString(SYSTEM_ACTOR)) - .setTime(System.currentTimeMillis()), - systemOperationContext.getRetrieverContext().get()) - .build(), - false); - - _entityService.alwaysProduceMCLAsync( - systemOperationContext, - roleUrn, - DATAHUB_ROLE_ENTITY_NAME, - DATAHUB_ROLE_INFO_ASPECT_NAME, - roleInfoAspectSpec, - null, - dataHubRoleInfo, - null, - null, - auditStamp, - ChangeType.RESTATE); - } -} diff --git a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestRootUserStep.java b/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestRootUserStep.java deleted file mode 100644 index f4862275d310be..00000000000000 --- a/metadata-service/factories/src/main/java/com/linkedin/metadata/boot/steps/IngestRootUserStep.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.linkedin.metadata.boot.steps; - -import static com.linkedin.metadata.Constants.*; - -import com.datahub.util.RecordUtils; -import com.fasterxml.jackson.core.StreamReadConstraints; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.linkedin.common.AuditStamp; -import com.linkedin.common.urn.Urn; -import com.linkedin.identity.CorpUserInfo; -import com.linkedin.metadata.boot.BootstrapStep; -import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.key.CorpUserKey; -import com.linkedin.metadata.models.AspectSpec; -import com.linkedin.metadata.models.EntitySpec; -import com.linkedin.metadata.utils.EntityKeyUtils; -import com.linkedin.util.Pair; -import io.datahubproject.metadata.context.OperationContext; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.List; -import javax.annotation.Nonnull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.ClassPathResource; - -@Slf4j -@RequiredArgsConstructor -public class IngestRootUserStep implements BootstrapStep { - - private static final String USER_INFO_ASPECT_NAME = "corpUserInfo"; - - private final EntityService _entityService; - - @Override - public String name() { - return getClass().getSimpleName(); - } - - @Override - public void execute(@Nonnull OperationContext systemOperationContext) - throws IOException, URISyntaxException { - - final ObjectMapper mapper = new ObjectMapper(); - int maxSize = - Integer.parseInt( - System.getenv() - .getOrDefault(INGESTION_MAX_SERIALIZED_STRING_LENGTH, MAX_JACKSON_STRING_SIZE)); - mapper - .getFactory() - .setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(maxSize).build()); - - // 1. Read from the file into JSON. - final JsonNode userObj = - mapper.readTree(new ClassPathResource("./boot/root_user.json").getFile()); - - if (!userObj.isObject()) { - throw new RuntimeException( - String.format( - "Found malformed root user file, expected an Object but found %s", - userObj.getNodeType())); - } - - // 2. Ingest the user info - final Urn urn; - try { - urn = Urn.createFromString(userObj.get("urn").asText()); - } catch (URISyntaxException e) { - log.error("Malformed urn: {}", userObj.get("urn").asText()); - throw new RuntimeException("Malformed urn", e); - } - - final CorpUserInfo info = - RecordUtils.toRecordTemplate(CorpUserInfo.class, userObj.get("info").toString()); - final CorpUserKey key = - (CorpUserKey) - EntityKeyUtils.convertUrnToEntityKey(urn, getUserKeyAspectSpec(systemOperationContext)); - final AuditStamp aspectAuditStamp = - new AuditStamp() - .setActor(Urn.createFromString(SYSTEM_ACTOR)) - .setTime(System.currentTimeMillis()); - - _entityService.ingestAspects( - systemOperationContext, - urn, - List.of(Pair.of(CORP_USER_KEY_ASPECT_NAME, key), Pair.of(USER_INFO_ASPECT_NAME, info)), - aspectAuditStamp, - null); - } - - private AspectSpec getUserKeyAspectSpec(@Nonnull OperationContext opContext) { - final EntitySpec spec = opContext.getEntityRegistry().getEntitySpec(CORP_USER_ENTITY_NAME); - return spec.getKeyAspectSpec(); - } -} diff --git a/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataTypesStepTest.java b/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataTypesStepTest.java deleted file mode 100644 index 65cffc6b86a5bb..00000000000000 --- a/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataTypesStepTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.linkedin.metadata.boot.steps; - -import static com.linkedin.metadata.Constants.*; -import static org.mockito.Mockito.*; - -import com.linkedin.common.AuditStamp; -import com.linkedin.common.urn.Urn; -import com.linkedin.common.urn.UrnUtils; -import com.linkedin.datatype.DataTypeInfo; -import com.linkedin.events.metadata.ChangeType; -import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.models.registry.ConfigEntityRegistry; -import com.linkedin.metadata.models.registry.EntityRegistry; -import com.linkedin.metadata.utils.GenericRecordUtils; -import com.linkedin.mxe.MetadataChangeProposal; -import io.datahubproject.metadata.context.EntityRegistryContext; -import io.datahubproject.metadata.context.OperationContext; -import java.util.Collection; -import java.util.Set; -import org.jetbrains.annotations.NotNull; -import org.mockito.Mockito; -import org.testng.Assert; -import org.testng.annotations.Test; - -public class IngestDataTypesStepTest { - - private static final Urn TEST_DATA_TYPE_URN = UrnUtils.getUrn("urn:li:dataType:datahub.test"); - - @Test - public void testExecuteValidDataTypesNoExistingDataTypes() throws Exception { - EntityRegistry testEntityRegistry = getTestEntityRegistry(); - final EntityService entityService = mock(EntityService.class); - - final OperationContext mockContext = mock(OperationContext.class); - final EntityRegistryContext entityRegistryContext = mock(EntityRegistryContext.class); - when(mockContext.getEntityRegistryContext()).thenReturn(entityRegistryContext); - when(mockContext.getEntityRegistry()).thenReturn(testEntityRegistry); - when(entityRegistryContext.getKeyAspectSpec(anyString())) - .thenAnswer( - args -> testEntityRegistry.getEntitySpec(args.getArgument(0)).getKeyAspectSpec()); - - final IngestDataTypesStep step = - new IngestDataTypesStep(entityService, "./boot/test_data_types_valid.json"); - - step.execute(mockContext); - - DataTypeInfo expectedResult = new DataTypeInfo(); - expectedResult.setDescription("Test Description"); - expectedResult.setDisplayName("Test Name"); - expectedResult.setQualifiedName("datahub.test"); - - Mockito.verify(entityService, times(1)) - .ingestProposal( - any(OperationContext.class), - Mockito.eq(buildUpdateDataTypeProposal(expectedResult)), - Mockito.any(AuditStamp.class), - Mockito.eq(false)); - } - - @Test - public void testExecuteInvalidJson() throws Exception { - final EntityService entityService = mock(EntityService.class); - final OperationContext mockContext = mock(OperationContext.class); - when(mockContext.getEntityRegistry()).thenReturn(mock(EntityRegistry.class)); - - when(entityService.exists(any(OperationContext.class), any(Collection.class))) - .thenAnswer(args -> Set.of()); - - final IngestDataTypesStep step = - new IngestDataTypesStep(entityService, "./boot/test_data_types_invalid.json"); - - Assert.assertThrows(RuntimeException.class, () -> step.execute(mockContext)); - - verify(entityService, times(1)).exists(any(OperationContext.class), any(Collection.class)); - - // Verify no additional interactions - verifyNoMoreInteractions(entityService); - } - - private static MetadataChangeProposal buildUpdateDataTypeProposal(final DataTypeInfo info) { - final MetadataChangeProposal mcp = new MetadataChangeProposal(); - mcp.setEntityUrn(TEST_DATA_TYPE_URN); - mcp.setEntityType(DATA_TYPE_ENTITY_NAME); - mcp.setAspectName(DATA_TYPE_INFO_ASPECT_NAME); - mcp.setChangeType(ChangeType.UPSERT); - mcp.setAspect(GenericRecordUtils.serializeAspect(info)); - return mcp; - } - - @NotNull - private ConfigEntityRegistry getTestEntityRegistry() { - return new ConfigEntityRegistry( - IngestDataPlatformInstancesStepTest.class - .getClassLoader() - .getResourceAsStream("test-entity-registry.yaml")); - } -} diff --git a/metadata-service/factories/src/test/resources/boot/test_data_types_invalid.json b/metadata-service/factories/src/test/resources/boot/test_data_types_invalid.json deleted file mode 100644 index ed1d8a7b45abe0..00000000000000 --- a/metadata-service/factories/src/test/resources/boot/test_data_types_invalid.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "urn": "urn:li:dataType:datahub.test", - "badField": { - "qualifiedName":"datahub.test", - "description": "Test Description" - } - } -] \ No newline at end of file diff --git a/metadata-service/factories/src/test/resources/boot/test_data_types_valid.json b/metadata-service/factories/src/test/resources/boot/test_data_types_valid.json deleted file mode 100644 index 3694c92947aa18..00000000000000 --- a/metadata-service/factories/src/test/resources/boot/test_data_types_valid.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "urn": "urn:li:dataType:datahub.test", - "info": { - "qualifiedName":"datahub.test", - "displayName": "Test Name", - "description": "Test Description" - } - } -] \ No newline at end of file diff --git a/metadata-service/war/src/main/resources/boot/data_platforms.json b/metadata-service/war/src/main/resources/boot/data_platforms.json deleted file mode 100644 index 03f1cf8e6c934e..00000000000000 --- a/metadata-service/war/src/main/resources/boot/data_platforms.json +++ /dev/null @@ -1,708 +0,0 @@ -[ - { - "urn": "urn:li:dataPlatform:adlsGen1", - "aspect": { - "datasetNameDelimiter": "/", - "name": "adlsGen1", - "displayName": "Azure Data Lake (Gen 1)", - "type": "FILE_SYSTEM", - "logoUrl": "/assets/platforms/adlslogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:adlsGen2", - "aspect": { - "datasetNameDelimiter": "/", - "name": "adlsGen2", - "displayName": "Azure Data Lake (Gen 2)", - "type": "FILE_SYSTEM", - "logoUrl": "/assets/platforms/adlslogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:airflow", - "aspect": { - "datasetNameDelimiter": ".", - "name": "airflow", - "displayName": "Airflow", - "type": "OTHERS", - "logoUrl": "/assets/platforms/airflowlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:ambry", - "aspect": { - "datasetNameDelimiter": ".", - "name": "ambry", - "displayName": "Ambry", - "type": "OBJECT_STORE" - } - }, - { - "urn": "urn:li:dataPlatform:clickhouse", - "aspect": { - "datasetNameDelimiter": ".", - "name": "clickhouse", - "displayName": "ClickHouse", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/clickhouselogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:cockroachdb", - "aspect": { - "datasetNameDelimiter": ".", - "name": "cockroachdb", - "displayName": "CockroachDb", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/cockroachdblogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:couchbase", - "aspect": { - "datasetNameDelimiter": ".", - "name": "couchbase", - "displayName": "Couchbase", - "type": "KEY_VALUE_STORE", - "logoUrl": "/assets/platforms/couchbaselogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:dagster", - "aspect": { - "datasetNameDelimiter": "/", - "name": "dagster", - "displayName": "Dagster", - "type": "OTHERS", - "logoUrl": "/assets/platforms/dagsterlogo.svg" - } - }, - { - "urn": "urn:li:dataPlatform:external", - "aspect": { - "datasetNameDelimiter": ".", - "name": "external", - "displayName": "External Source", - "type": "OTHERS" - } - }, - { - "urn": "urn:li:dataPlatform:hdfs", - "aspect": { - "datasetNameDelimiter": "/", - "name": "hdfs", - "displayName": "HDFS", - "type": "FILE_SYSTEM", - "logoUrl": "/assets/platforms/hadooplogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:hana", - "aspect": { - "datasetNameDelimiter": ".", - "name": "hana", - "displayName": "SAP HANA", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/hanalogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:hive", - "aspect": { - "datasetNameDelimiter": ".", - "name": "hive", - "displayName": "Hive", - "type": "FILE_SYSTEM", - "logoUrl": "/assets/platforms/hivelogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:iceberg", - "aspect": { - "datasetNameDelimiter": ".", - "name": "iceberg", - "displayName": "Iceberg", - "type": "FILE_SYSTEM", - "logoUrl": "/assets/platforms/iceberglogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:s3", - "aspect": { - "datasetNameDelimiter": "/", - "name": "s3", - "displayName": "AWS S3", - "type": "FILE_SYSTEM", - "logoUrl": "/assets/platforms/s3.png" - } - }, - { - "urn": "urn:li:dataPlatform:kafka", - "aspect": { - "datasetNameDelimiter": ".", - "name": "kafka", - "displayName": "Kafka", - "type": "MESSAGE_BROKER", - "logoUrl": "/assets/platforms/kafkalogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:kafka-connect", - "aspect": { - "datasetNameDelimiter": ".", - "name": "kafka-connect", - "displayName": "Kafka Connect", - "type": "OTHERS", - "logoUrl": "/assets/platforms/kafkalogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:kusto", - "aspect": { - "datasetNameDelimiter": ".", - "name": "kusto", - "displayName": "Kusto", - "type": "OLAP_DATASTORE", - "logoUrl": "/assets/platforms/kustologo.png" - } - }, - { - "urn": "urn:li:dataPlatform:mode", - "aspect": { - "datasetNameDelimiter": ".", - "name": "mode", - "displayName": "Mode", - "type": "KEY_VALUE_STORE", - "logoUrl": "/assets/platforms/modelogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:mongodb", - "aspect": { - "datasetNameDelimiter": ".", - "name": "mongodb", - "displayName": "MongoDB", - "type": "KEY_VALUE_STORE", - "logoUrl": "/assets/platforms/mongodblogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:mysql", - "aspect": { - "datasetNameDelimiter": ".", - "name": "mysql", - "displayName": "MySQL", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/mysqllogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:db2", - "aspect": { - "datasetNameDelimiter": ".", - "name": "db2", - "displayName": "DB2", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/db2logo.png" - } - }, - { - "urn": "urn:li:dataPlatform:mariadb", - "aspect": { - "datasetNameDelimiter": ".", - "name": "mariadb", - "displayName": "MariaDB", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/mariadblogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:OpenApi", - "aspect": { - "datasetNameDelimiter": ".", - "name": "openapi", - "displayName": "OpenAPI", - "type": "OTHERS", - "logoUrl": "/assets/platforms/openapilogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:oracle", - "aspect": { - "datasetNameDelimiter": ".", - "name": "oracle", - "displayName": "Oracle", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/oraclelogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:pinot", - "aspect": { - "datasetNameDelimiter": ".", - "name": "pinot", - "displayName": "Pinot", - "type": "OLAP_DATASTORE", - "logoUrl": "/assets/platforms/pinotlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:postgres", - "aspect": { - "datasetNameDelimiter": ".", - "name": "postgres", - "displayName": "PostgreSQL", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/postgreslogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:prefect", - "aspect": { - "datasetNameDelimiter": ".", - "name": "prefect", - "displayName": "Prefect", - "type": "OTHERS", - "logoUrl": "/assets/platforms/prefectlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:presto", - "aspect": { - "datasetNameDelimiter": ".", - "name": "prefect", - "displayName": "Prefect", - "type": "OTHERS", - "logoUrl": "/assets/platforms/prefectlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:presto", - "aspect": { - "datasetNameDelimiter": ".", - "name": "presto", - "displayName": "Presto", - "type": "QUERY_ENGINE", - "logoUrl": "/assets/platforms/prestologo.png" - } - }, - { - "urn": "urn:li:dataPlatform:tableau", - "aspect": { - "datasetNameDelimiter": ".", - "name": "tableau", - "displayName": "Tableau", - "type": "OTHERS", - "logoUrl": "/assets/platforms/tableaulogo.svg" - } - }, - { - "urn": "urn:li:dataPlatform:teradata", - "aspect": { - "datasetNameDelimiter": ".", - "name": "teradata", - "displayName": "Teradata", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/teradatalogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:voldemort", - "aspect": { - "datasetNameDelimiter": ".", - "name": "voldemort", - "displayName": "Voldemort", - "type": "KEY_VALUE_STORE" - } - }, - { - "urn": "urn:li:dataPlatform:snowflake", - "aspect": { - "datasetNameDelimiter": ".", - "name": "snowflake", - "displayName": "Snowflake", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/snowflakelogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:redshift", - "aspect": { - "datasetNameDelimiter": ".", - "name": "redshift", - "displayName": "Redshift", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/redshiftlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:mssql", - "aspect": { - "datasetNameDelimiter": ".", - "name": "mssql", - "displayName": "SQL Server", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/mssqllogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:bigquery", - "aspect": { - "datasetNameDelimiter": ".", - "name": "bigquery", - "displayName": "BigQuery", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/bigquerylogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:druid", - "aspect": { - "datasetNameDelimiter": ".", - "name": "druid", - "displayName": "Druid", - "type": "OLAP_DATASTORE", - "logoUrl": "/assets/platforms/druidlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:looker", - "aspect": { - "datasetNameDelimiter": ".", - "name": "looker", - "displayName": "Looker", - "type": "OTHERS", - "logoUrl": "/assets/platforms/lookerlogo.svg" - } - }, - { - "urn": "urn:li:dataPlatform:feast", - "aspect": { - "datasetNameDelimiter": ".", - "name": "feast", - "displayName": "Feast", - "type": "OTHERS", - "logoUrl": "/assets/platforms/feastlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:sagemaker", - "aspect": { - "datasetNameDelimiter": ".", - "name": "sagemaker", - "displayName": "SageMaker", - "type": "OTHERS", - "logoUrl": "/assets/platforms/sagemakerlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:mlflow", - "aspect": { - "datasetNameDelimiter": ".", - "name": "mlflow", - "displayName": "MLflow", - "type": "OTHERS", - "logoUrl": "/assets/platforms/mlflowlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:glue", - "aspect": { - "datasetNameDelimiter": ".", - "name": "glue", - "displayName": "Glue", - "type": "OTHERS", - "logoUrl": "/assets/platforms/gluelogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:redash", - "aspect": { - "datasetNameDelimiter": ".", - "name": "redash", - "displayName": "Redash", - "type": "OTHERS", - "logoUrl": "/assets/platforms/redashlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:athena", - "aspect": { - "datasetNameDelimiter": ".", - "name": "athena", - "displayName": "AWS Athena", - "type": "RELATIONAL_DB", - "logoUrl": "/assets/platforms/awsathenalogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:spark", - "aspect": { - "datasetNameDelimiter": ".", - "name": "spark", - "displayName": "Spark", - "type": "OTHERS", - "logoUrl": "/assets/platforms/sparklogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:dbt", - "aspect": { - "datasetNameDelimiter": ".", - "name": "dbt", - "displayName": "dbt", - "type": "OTHERS", - "logoUrl": "/assets/platforms/dbtlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:elasticsearch", - "aspect": { - "datasetNameDelimiter": ".", - "name": "elasticsearch", - "displayName": "Elasticsearch", - "type": "OTHERS", - "logoUrl": "/assets/platforms/elasticsearchlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:great-expectations", - "aspect": { - "name": "Great Expectations", - "displayName": "Great Expectations", - "type": "OTHERS", - "logoUrl": "/assets/platforms/greatexpectationslogo.png", - "datasetNameDelimiter": "." - } - }, - { - "urn": "urn:li:dataPlatform:powerbi", - "aspect": { - "datasetNameDelimiter": ".", - "name": "powerbi", - "displayName": "Power BI", - "type": "OTHERS", - "logoUrl": "/assets/platforms/powerbilogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:presto-on-hive", - "aspect": { - "datasetNameDelimiter": ".", - "name": "presto-on-hive", - "displayName": "Presto on Hive", - "type": "FILE_SYSTEM", - "logoUrl": "/assets/platforms/prestoonhivelogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:metabase", - "aspect": { - "datasetNameDelimiter": ".", - "name": "metabase", - "displayName": "Metabase", - "type": "OTHERS", - "logoUrl": "/assets/platforms/metabaselogo.svg" - } - }, - { - "urn": "urn:li:dataPlatform:nifi", - "aspect": { - "datasetNameDelimiter": ".", - "name": "nifi", - "displayName": "NiFi", - "type": "OTHERS", - "logoUrl": "/assets/platforms/nifilogo.svg" - } - }, - { - "urn": "urn:li:dataPlatform:superset", - "aspect": { - "datasetNameDelimiter": ".", - "name": "superset", - "displayName": "Superset", - "type": "OTHERS", - "logoUrl": "/assets/platforms/supersetlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:trino", - "aspect": { - "datasetNameDelimiter": ".", - "name": "trino", - "displayName": "Trino", - "type": "QUERY_ENGINE", - "logoUrl": "/assets/platforms/trinologo.png" - } - }, - { - "urn": "urn:li:dataPlatform:pulsar", - "aspect": { - "datasetNameDelimiter": ".", - "name": "pulsar", - "displayName": "Pulsar", - "type": "MESSAGE_BROKER", - "logoUrl": "/assets/platforms/pulsarlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:salesforce", - "aspect": { - "datasetNameDelimiter": ".", - "name": "salesforce", - "displayName": "Salesforce", - "type": "OTHERS", - "logoUrl": "/assets/platforms/logo-salesforce.svg" - } - }, - { - "urn": "urn:li:dataPlatform:unknown", - "aspect": { - "datasetNameDelimiter": ".", - "name": "Unknown Platform", - "displayName": "N/A", - "type": "OTHERS" - } - }, - { - "urn": "urn:li:dataPlatform:delta-lake", - "aspect": { - "datasetNameDelimiter": ".", - "name": "delta-lake", - "displayName": "Delta Lake", - "type": "OTHERS", - "logoUrl": "/assets/platforms/deltalakelogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:databricks", - "aspect": { - "datasetNameDelimiter": ".", - "name": "databricks", - "displayName": "Databricks", - "type": "OTHERS", - "logoUrl": "/assets/platforms/databrickslogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:vertica", - "aspect": { - "datasetNameDelimiter": ".", - "name": "vertica", - "displayName": "Vertica", - "type": "OLAP_DATASTORE", - "logoUrl": "/assets/platforms/verticalogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:gcs", - "aspect": { - "datasetNameDelimiter": "/", - "name": "gcs", - "displayName": "Google Cloud Storage", - "type": "FILE_SYSTEM", - "logoUrl": "/assets/platforms/gcslogo.svg" - } - }, - { - "urn": "urn:li:dataPlatform:slack", - "aspect": { - "datasetNameDelimiter": ".", - "name": "Slack", - "displayName": "Slack", - "type": "OTHERS", - "logoUrl": "/assets/platforms/slacklogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:microsoft-teams", - "aspect": { - "datasetNameDelimiter": ".", - "name": "Microsoft Teams", - "displayName": "Microsoft Teams", - "type": "OTHERS", - "logoUrl": "/assets/platforms/teamslogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:dynamodb", - "aspect": { - "datasetNameDelimiter": ".", - "name": "dynamodb", - "displayName": "DynamoDB", - "type": "KEY_VALUE_STORE", - "logoUrl": "/assets/platforms/dynamodblogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:fivetran", - "aspect": { - "datasetNameDelimiter": ".", - "name": "fivetran", - "displayName": "Fivetran", - "type": "OTHERS", - "logoUrl": "/assets/platforms/fivetranlogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:csv", - "aspect": { - "datasetNameDelimiter": ".", - "name": "csv", - "displayName": "CSV", - "type": "OTHERS", - "logoUrl": "/assets/platforms/csv-logo.png" - } - }, - { - "urn": "urn:li:dataPlatform:qlik-sense", - "aspect": { - "datasetNameDelimiter": ".", - "name": "qlik-sense", - "displayName": "Qlik Sense", - "type": "OTHERS", - "logoUrl": "/assets/platforms/qliklogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:file", - "aspect": { - "datasetNameDelimiter": ".", - "name": "file", - "displayName": "File", - "type": "OTHERS", - "logoUrl": "/assets/platforms/file-logo.svg" - } - }, - { - "urn": "urn:li:dataPlatform:excel", - "aspect": { - "name": "excel", - "displayName": "Excel", - "type": "OTHERS", - "datasetNameDelimiter": "/", - "logoUrl": "/assets/platforms/excel-logo.svg" - } - }, - { - "urn": "urn:li:dataPlatform:sigma", - "aspect": { - "datasetNameDelimiter": ".", - "name": "sigma", - "displayName": "Sigma", - "type": "OTHERS", - "logoUrl": "/assets/platforms/sigmalogo.png" - } - }, - { - "urn": "urn:li:dataPlatform:sac", - "aspect": { - "datasetNameDelimiter": ".", - "name": "sac", - "displayName": "SAP Analytics Cloud", - "type": "OTHERS", - "logoUrl": "/assets/platforms/saclogo.svg" - } - } -] diff --git a/metadata-service/war/src/main/resources/boot/data_types.json b/metadata-service/war/src/main/resources/boot/data_types.json deleted file mode 100644 index 2d7294e45bd7a5..00000000000000 --- a/metadata-service/war/src/main/resources/boot/data_types.json +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "urn": "urn:li:dataType:datahub.string", - "info": { - "qualifiedName":"datahub.string", - "displayName": "String", - "description": "A string of characters." - } - }, - { - "urn": "urn:li:dataType:datahub.number", - "info": { - "qualifiedName":"datahub.number", - "displayName": "Number", - "description": "An integer or decimal number." - } - }, - { - "urn": "urn:li:dataType:datahub.urn", - "info": { - "qualifiedName":"datahub.urn", - "displayName": "Urn", - "description": "An unique identifier for a DataHub entity." - } - }, - { - "urn": "urn:li:dataType:datahub.rich_text", - "info": { - "qualifiedName":"datahub.rich_text", - "displayName": "Rich Text", - "description": "An attributed string of characters." - } - }, - { - "urn": "urn:li:dataType:datahub.date", - "info": { - "qualifiedName":"datahub.date", - "displayName": "Date", - "description": "A specific day, without time." - } - } -] diff --git a/metadata-service/war/src/main/resources/boot/ownership_types.json b/metadata-service/war/src/main/resources/boot/ownership_types.json deleted file mode 100644 index 79fe5d600a9ce0..00000000000000 --- a/metadata-service/war/src/main/resources/boot/ownership_types.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "urn": "urn:li:ownershipType:__system__technical_owner", - "info": { - "name":"Technical Owner", - "description":"Involved in the production, maintenance, or distribution of the asset(s)." - } - }, - { - "urn": "urn:li:ownershipType:__system__business_owner", - "info": { - "name":"Business Owner", - "description":"Principle stakeholders or domain experts associated with the asset(s)." - } - }, - { - "urn": "urn:li:ownershipType:__system__data_steward", - "info": { - "name":"Data Steward", - "description":"Involved in governance of the asset(s)." - } - }, - { - "urn": "urn:li:ownershipType:__system__none", - "info": { - "name":"None", - "description":"No ownership type specified." - } - } -] \ No newline at end of file diff --git a/metadata-service/war/src/main/resources/boot/roles.json b/metadata-service/war/src/main/resources/boot/roles.json deleted file mode 100644 index 629226ef136e26..00000000000000 --- a/metadata-service/war/src/main/resources/boot/roles.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "urn": "urn:li:dataHubRole:Admin", - "info": { - "name":"Admin", - "description":"Can do everything on the platform.", - "editable":false - } - }, - { - "urn": "urn:li:dataHubRole:Editor", - "info": { - "name":"Editor", - "description":"Can read and edit all metadata. Cannot take administrative actions.", - "editable":false - } - }, - { - "urn": "urn:li:dataHubRole:Reader", - "info": { - "name":"Reader", - "description":"Can read all metadata. Cannot edit anything by default, or take administrative actions.", - "editable":false - } - } -] diff --git a/metadata-service/war/src/main/resources/boot/root_user.json b/metadata-service/war/src/main/resources/boot/root_user.json deleted file mode 100644 index 7922a6c7fa5afb..00000000000000 --- a/metadata-service/war/src/main/resources/boot/root_user.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "urn": "urn:li:corpuser:datahub", - "info": { - "active": true, - "displayName": "DataHub", - "title": "DataHub Root User" - } -} \ No newline at end of file diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/utils/GenericRecordUtils.java b/metadata-utils/src/main/java/com/linkedin/metadata/utils/GenericRecordUtils.java index 7974d239a25bc4..6638481c1d2794 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/utils/GenericRecordUtils.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/utils/GenericRecordUtils.java @@ -59,9 +59,13 @@ public static T deserializePayload( @Nonnull public static GenericAspect serializeAspect(@Nonnull RecordTemplate aspect) { + return serializeAspect(RecordUtils.toJsonString(aspect)); + } + + @Nonnull + public static GenericAspect serializeAspect(@Nonnull String str) { GenericAspect genericAspect = new GenericAspect(); - genericAspect.setValue( - ByteString.unsafeWrap(RecordUtils.toJsonString(aspect).getBytes(StandardCharsets.UTF_8))); + genericAspect.setValue(ByteString.unsafeWrap(str.getBytes(StandardCharsets.UTF_8))); genericAspect.setContentType(GenericRecordUtils.JSON); return genericAspect; } From 18562000f82397d32c7d5f6cd3c20a424e0eccf9 Mon Sep 17 00:00:00 2001 From: Hendrik Richert Date: Fri, 4 Oct 2024 19:54:41 +0200 Subject: [PATCH 09/25] add new CREATE and UPDATE privileges for USERS_AND_GROUPS (#11364) Co-authored-by: Hendrik Richert --- .../authorization/PoliciesConfig.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index 7a5a34d0f36301..5964bab9465284 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -59,6 +59,18 @@ public class PoliciesConfig { "Manage Users & Groups", "Create, remove, and update users and groups on DataHub."); + static final Privilege CREATE_USERS_AND_GROUPS_PRIVILEGE = + Privilege.of( + "CREATE_USERS_AND_GROUPS", + "Create Users & Groups", + "Create users and groups on DataHub."); + + static final Privilege UPDATE_USERS_AND_GROUPS_PRIVILEGE = + Privilege.of( + "UPDATE_USERS_AND_GROUPS", + "Update Users & Groups", + "Update users and groups on DataHub."); + private static final Privilege VIEW_ANALYTICS_PRIVILEGE = Privilege.of("VIEW_ANALYTICS", "View Analytics", "View the DataHub analytics dashboard."); @@ -177,6 +189,8 @@ public class PoliciesConfig { ImmutableList.of( MANAGE_POLICIES_PRIVILEGE, MANAGE_USERS_AND_GROUPS_PRIVILEGE, + CREATE_USERS_AND_GROUPS_PRIVILEGE, + UPDATE_USERS_AND_GROUPS_PRIVILEGE, VIEW_ANALYTICS_PRIVILEGE, GET_ANALYTICS_PRIVILEGE, MANAGE_DOMAINS_PRIVILEGE, @@ -926,13 +940,15 @@ public class PoliciesConfig { ImmutableMap.>>builder() .put( ApiOperation.CREATE, - Disjunctive.disjoint(MANAGE_USERS_AND_GROUPS_PRIVILEGE)) + Disjunctive.disjoint( + CREATE_USERS_AND_GROUPS_PRIVILEGE, MANAGE_USERS_AND_GROUPS_PRIVILEGE)) .put( ApiOperation.READ, API_PRIVILEGE_MAP.get(ApiGroup.ENTITY).get(ApiOperation.READ)) .put( ApiOperation.UPDATE, - Disjunctive.disjoint(MANAGE_USERS_AND_GROUPS_PRIVILEGE)) + Disjunctive.disjoint( + UPDATE_USERS_AND_GROUPS_PRIVILEGE, MANAGE_USERS_AND_GROUPS_PRIVILEGE)) .put( ApiOperation.DELETE, Disjunctive.disjoint(MANAGE_USERS_AND_GROUPS_PRIVILEGE)) @@ -945,13 +961,15 @@ public class PoliciesConfig { ImmutableMap.>>builder() .put( ApiOperation.CREATE, - Disjunctive.disjoint(MANAGE_USERS_AND_GROUPS_PRIVILEGE)) + Disjunctive.disjoint( + CREATE_USERS_AND_GROUPS_PRIVILEGE, MANAGE_USERS_AND_GROUPS_PRIVILEGE)) .put( ApiOperation.READ, API_PRIVILEGE_MAP.get(ApiGroup.ENTITY).get(ApiOperation.READ)) .put( ApiOperation.UPDATE, - Disjunctive.disjoint(MANAGE_USERS_AND_GROUPS_PRIVILEGE)) + Disjunctive.disjoint( + UPDATE_USERS_AND_GROUPS_PRIVILEGE, MANAGE_USERS_AND_GROUPS_PRIVILEGE)) .put( ApiOperation.DELETE, Disjunctive.disjoint(MANAGE_USERS_AND_GROUPS_PRIVILEGE)) From 81151db04a02e59713b48d800b5cc1f103b68cbe Mon Sep 17 00:00:00 2001 From: Andrew Sikowitz Date: Fri, 4 Oct 2024 11:23:01 -0700 Subject: [PATCH 10/25] feat(graphql/lineage): Support including ghost entities (#11510) Co-authored-by: david-leifker <114954101+david-leifker@users.noreply.github.com> --- .../load/EntityLineageResultResolver.java | 18 +- .../src/main/resources/entity.graphql | 5 + .../entity/validation/ValidationUtils.java | 9 +- .../metadata/graph/SiblingGraphService.java | 52 ++- .../sibling/SiblingGraphServiceTest.java | 313 +++++++----------- 5 files changed, 193 insertions(+), 204 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/load/EntityLineageResultResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/load/EntityLineageResultResolver.java index d872ffad2783db..204e591b1da3ee 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/load/EntityLineageResultResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/load/EntityLineageResultResolver.java @@ -25,6 +25,7 @@ import graphql.schema.DataFetchingEnvironment; import io.datahubproject.metadata.services.RestrictedService; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -60,14 +61,16 @@ public CompletableFuture get(DataFetchingEnvironment enviro final LineageInput input = bindArgument(environment.getArgument("input"), LineageInput.class); final LineageDirection lineageDirection = input.getDirection(); - @Nullable final Integer start = input.getStart(); // Optional! - @Nullable final Integer count = input.getCount(); // Optional! - @Nullable final Boolean separateSiblings = input.getSeparateSiblings(); // Optional! - @Nullable final Long startTimeMillis = input.getStartTimeMillis(); // Optional! + // All inputs are optional + @Nullable final Integer start = input.getStart(); + @Nullable final Integer count = input.getCount(); + @Nullable final Boolean separateSiblings = input.getSeparateSiblings(); + @Nullable final Long startTimeMillis = input.getStartTimeMillis(); @Nullable final Long endTimeMillis = - ResolverUtils.getLineageEndTimeMillis( - input.getStartTimeMillis(), input.getEndTimeMillis()); // Optional! + ResolverUtils.getLineageEndTimeMillis(input.getStartTimeMillis(), input.getEndTimeMillis()); + final Boolean includeGhostEntities = + Optional.ofNullable(input.getIncludeGhostEntities()).orElse(false); com.linkedin.metadata.graph.LineageDirection resolvedDirection = com.linkedin.metadata.graph.LineageDirection.valueOf(lineageDirection.toString()); @@ -80,6 +83,8 @@ public CompletableFuture get(DataFetchingEnvironment enviro _siblingGraphService.getLineage( context .getOperationContext() + .withSearchFlags( + searchFlags -> searchFlags.setIncludeSoftDeleted(includeGhostEntities)) .withLineageFlags( flags -> flags @@ -91,6 +96,7 @@ public CompletableFuture get(DataFetchingEnvironment enviro count != null ? count : 100, 1, separateSiblings != null ? input.getSeparateSiblings() : false, + input.getIncludeGhostEntities(), new HashSet<>()); Set restrictedUrns = new HashSet<>(); diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index fd112c9524ac9a..07dcdf8cc95845 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -1258,6 +1258,11 @@ input LineageInput { An optional ending time to filter on """ endTimeMillis: Long + + """ + If enabled, include entities that do not exist or are soft deleted. + """ + includeGhostEntities: Boolean = false } """ diff --git a/metadata-io/src/main/java/com/linkedin/metadata/entity/validation/ValidationUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/entity/validation/ValidationUtils.java index ddcc6b65992319..6ecac70e13c7e5 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/entity/validation/ValidationUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/entity/validation/ValidationUtils.java @@ -205,7 +205,8 @@ public static LineageSearchResult validateLineageSearchResult( public static EntityLineageResult validateEntityLineageResult( @Nonnull OperationContext opContext, @Nullable final EntityLineageResult entityLineageResult, - @Nonnull final EntityService entityService) { + @Nonnull final EntityService entityService, + boolean includeGhostEntities) { if (entityLineageResult == null) { return null; } @@ -223,8 +224,8 @@ public static EntityLineageResult validateEntityLineageResult( entityLineageResult.getRelationships(), LineageRelationship::getEntity, entityService, - true, - false) + !includeGhostEntities, + includeGhostEntities) .collect(Collectors.toCollection(LineageRelationshipArray::new)); validatedEntityLineageResult.setFiltered( @@ -280,6 +281,8 @@ private static Stream validateSearchUrns( boolean includeSoftDeleted) { if (enforceSQLExistence) { + // TODO: Always set includeSoftDeleted to true once 0.3.7 OSS merge occurs, as soft deleted + // results will be filtered by graph service Set existingUrns = entityService.exists( opContext, diff --git a/metadata-io/src/main/java/com/linkedin/metadata/graph/SiblingGraphService.java b/metadata-io/src/main/java/com/linkedin/metadata/graph/SiblingGraphService.java index f9287ab34cf192..993b5a457206d1 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/graph/SiblingGraphService.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/graph/SiblingGraphService.java @@ -38,10 +38,29 @@ public EntityLineageResult getLineage( int offset, int count, int maxHops) { - return ValidationUtils.validateEntityLineageResult( + return getLineage(opContext, entityUrn, direction, offset, count, maxHops, false, false); + } + + @Nonnull + public EntityLineageResult getLineage( + @Nonnull OperationContext opContext, + @Nonnull Urn entityUrn, + @Nonnull LineageDirection direction, + int offset, + int count, + int maxHops, + boolean separateSiblings, + boolean includeGhostEntities) { + return getLineage( opContext, - getLineage(opContext, entityUrn, direction, offset, count, maxHops, false, new HashSet<>()), - _entityService); + entityUrn, + direction, + offset, + count, + maxHops, + separateSiblings, + includeGhostEntities, + new HashSet<>()); } /** @@ -60,12 +79,14 @@ public EntityLineageResult getLineage( int count, int maxHops, boolean separateSiblings, + boolean includeGhostEntities, @Nonnull Set visitedUrns) { if (separateSiblings) { return ValidationUtils.validateEntityLineageResult( opContext, _graphService.getLineage(opContext, entityUrn, direction, offset, count, maxHops), - _entityService); + _entityService, + includeGhostEntities); } if (maxHops > 1) { @@ -89,7 +110,7 @@ public EntityLineageResult getLineage( // remove your siblings from your lineage entityLineage = filterLineageResultFromSiblings( - opContext, entityUrn, allSiblingsInGroup, entityLineage, null); + opContext, entityUrn, allSiblingsInGroup, entityLineage, null, includeGhostEntities); // Update offset and count to fetch the correct number of edges from the next sibling node offset = Math.max(0, offset - entityLineage.getTotal()); @@ -109,8 +130,17 @@ public EntityLineageResult getLineage( siblingUrn, allSiblingsInGroup, getLineage( - opContext, siblingUrn, direction, offset, count, maxHops, false, visitedUrns), - entityLineage); + opContext, + siblingUrn, + direction, + offset, + count, + maxHops, + false, + includeGhostEntities, + visitedUrns), + entityLineage, + includeGhostEntities); // Update offset and count to fetch the correct number of edges from the next sibling node offset = Math.max(0, offset - nextEntityLineage.getTotal()); @@ -122,7 +152,8 @@ public EntityLineageResult getLineage( ; } - return ValidationUtils.validateEntityLineageResult(opContext, entityLineage, _entityService); + return ValidationUtils.validateEntityLineageResult( + opContext, entityLineage, _entityService, includeGhostEntities); } private int getFiltered(@Nullable EntityLineageResult entityLineageResult) { @@ -138,7 +169,8 @@ private EntityLineageResult filterLineageResultFromSiblings( @Nonnull final Urn urn, @Nonnull final Set allSiblingsInGroup, @Nonnull final EntityLineageResult entityLineageResult, - @Nullable final EntityLineageResult existingResult) { + @Nullable final EntityLineageResult existingResult, + boolean includeGhostEntities) { int numFiltered = 0; // 1) remove the source entities siblings from this entity's downstreams @@ -231,6 +263,6 @@ private EntityLineageResult filterLineageResultFromSiblings( combinedLineageResult.setFiltered( numFiltered + getFiltered(existingResult) + getFiltered(entityLineageResult)); return ValidationUtils.validateEntityLineageResult( - opContext, combinedLineageResult, _entityService); + opContext, combinedLineageResult, _entityService, includeGhostEntities); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/graph/sibling/SiblingGraphServiceTest.java b/metadata-io/src/test/java/com/linkedin/metadata/graph/sibling/SiblingGraphServiceTest.java index 15165f59deb160..a61ea1f2562b0c 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/graph/sibling/SiblingGraphServiceTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/graph/sibling/SiblingGraphServiceTest.java @@ -33,6 +33,7 @@ import javax.annotation.Nonnull; import org.mockito.Mockito; import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; public class SiblingGraphServiceTest { @@ -73,9 +74,6 @@ public class SiblingGraphServiceTest { @BeforeClass public void setup() { _mockEntityService = Mockito.mock(EntityService.class); - when(_mockEntityService.exists( - any(OperationContext.class), any(Collection.class), any(Boolean.class))) - .thenAnswer(args -> new HashSet<>(args.getArgument(1))); EntityRegistry entityRegistry = new ConfigEntityRegistry( Snapshot.class.getClassLoader().getResourceAsStream("entity-registry.yml")); @@ -84,34 +82,16 @@ public void setup() { _client = new SiblingGraphService(_mockEntityService, _graphService); } + @BeforeMethod + public void init() { + when(_mockEntityService.exists( + any(OperationContext.class), any(Collection.class), any(Boolean.class))) + .thenAnswer(args -> new HashSet<>(args.getArgument(1))); + } + @Test public void testNoSiblingMetadata() { - EntityLineageResult mockResult = new EntityLineageResult(); - LineageRelationshipArray relationships = new LineageRelationshipArray(); - LineageRelationship relationship1 = new LineageRelationship(); - relationship1.setDegree(0); - relationship1.setType(downstreamOf); - relationship1.setEntity(datasetOneUrn); - - LineageRelationship relationship2 = new LineageRelationship(); - relationship2.setDegree(0); - relationship2.setType(downstreamOf); - relationship2.setEntity(datasetTwoUrn); - - LineageRelationship relationship3 = new LineageRelationship(); - relationship3.setDegree(0); - relationship3.setType(downstreamOf); - relationship3.setEntity(datasetThreeUrn); - - relationships.add(relationship1); - relationships.add(relationship2); - relationships.add(relationship3); - - mockResult.setStart(0); - mockResult.setTotal(200); - mockResult.setCount(3); - mockResult.setFiltered(0); - mockResult.setRelationships(relationships); + EntityLineageResult mockResult = makeBasicMockResult(); when(_graphService.getLineage( any(OperationContext.class), @@ -137,35 +117,9 @@ public void testNoSiblingMetadata() { @Test public void testNoSiblingInResults() { - EntityLineageResult mockResult = new EntityLineageResult(); + EntityLineageResult mockResult = makeBasicMockResult(); EntityLineageResult siblingMockResult = new EntityLineageResult(); - LineageRelationshipArray relationships = new LineageRelationshipArray(); - LineageRelationship relationship1 = new LineageRelationship(); - relationship1.setDegree(0); - relationship1.setType(downstreamOf); - relationship1.setEntity(datasetOneUrn); - - LineageRelationship relationship2 = new LineageRelationship(); - relationship2.setDegree(0); - relationship2.setType(downstreamOf); - relationship2.setEntity(datasetTwoUrn); - - LineageRelationship relationship3 = new LineageRelationship(); - relationship3.setDegree(0); - relationship3.setType(downstreamOf); - relationship3.setEntity(datasetThreeUrn); - - relationships.add(relationship1); - relationships.add(relationship2); - relationships.add(relationship3); - - mockResult.setStart(0); - mockResult.setTotal(200); - mockResult.setCount(3); - mockResult.setFiltered(0); - mockResult.setRelationships(relationships); - when(_graphService.getLineage( any(OperationContext.class), eq(datasetFourUrn), @@ -229,34 +183,9 @@ public void testNoSiblingInResults() { @Test public void testSiblingInResult() throws Exception { - EntityLineageResult mockResult = new EntityLineageResult(); + EntityLineageResult mockResult = makeBasicMockResult(); EntityLineageResult siblingMockResult = new EntityLineageResult(); - LineageRelationshipArray relationships = new LineageRelationshipArray(); - LineageRelationship relationship1 = new LineageRelationship(); - relationship1.setDegree(0); - relationship1.setType(downstreamOf); - relationship1.setEntity(datasetOneUrn); - - LineageRelationship relationship2 = new LineageRelationship(); - relationship2.setDegree(0); - relationship2.setType(downstreamOf); - relationship2.setEntity(datasetTwoUrn); - - LineageRelationship relationship3 = new LineageRelationship(); - relationship3.setDegree(0); - relationship3.setType(downstreamOf); - relationship3.setEntity(datasetThreeUrn); - - relationships.add(relationship1); - relationships.add(relationship2); - relationships.add(relationship3); - - mockResult.setStart(0); - mockResult.setTotal(3); - mockResult.setCount(3); - mockResult.setRelationships(relationships); - siblingMockResult.setStart(0); siblingMockResult.setTotal(0); siblingMockResult.setCount(0); @@ -315,7 +244,9 @@ public void testSiblingInResult() throws Exception { expectedResult.setTotal(3); expectedResult.setCount(2); expectedResult.setFiltered(1); - expectedResult.setRelationships(new LineageRelationshipArray(relationship1, relationship2)); + expectedResult.setRelationships( + new LineageRelationshipArray( + makeBasicRelationship(datasetOneUrn), makeBasicRelationship(datasetTwoUrn))); EntityLineageResult upstreamLineage = service.getLineage(opContext, datasetFourUrn, LineageDirection.UPSTREAM, 0, 100, 1); @@ -335,25 +266,9 @@ public void testCombineSiblingResult() { LineageRelationshipArray siblingRelationships = new LineageRelationshipArray(); LineageRelationshipArray expectedRelationships = new LineageRelationshipArray(); - LineageRelationship relationship1 = new LineageRelationship(); - relationship1.setDegree(0); - relationship1.setType(downstreamOf); - relationship1.setEntity(datasetOneUrn); - - LineageRelationship relationship2 = new LineageRelationship(); - relationship2.setDegree(0); - relationship2.setType(downstreamOf); - relationship2.setEntity(datasetTwoUrn); - - LineageRelationship relationship3 = new LineageRelationship(); - relationship3.setDegree(0); - relationship3.setType(downstreamOf); - relationship3.setEntity(datasetThreeUrn); - - LineageRelationship relationship4 = new LineageRelationship(); - relationship4.setDegree(0); - relationship4.setType(downstreamOf); - relationship4.setEntity(datasetFiveUrn); + LineageRelationship relationship1 = makeBasicRelationship(datasetOneUrn); + LineageRelationship relationship2 = makeBasicRelationship(datasetTwoUrn); + LineageRelationship relationship4 = makeBasicRelationship(datasetFiveUrn); relationships.add(relationship1); @@ -449,25 +364,9 @@ public void testUpstreamOfSiblings() { LineageRelationshipArray siblingRelationships = new LineageRelationshipArray(); LineageRelationshipArray expectedRelationships = new LineageRelationshipArray(); - LineageRelationship relationship1 = new LineageRelationship(); - relationship1.setDegree(0); - relationship1.setType(downstreamOf); - relationship1.setEntity(datasetOneUrn); - - LineageRelationship relationship2 = new LineageRelationship(); - relationship2.setDegree(0); - relationship2.setType(downstreamOf); - relationship2.setEntity(datasetTwoUrn); - - LineageRelationship relationship3 = new LineageRelationship(); - relationship3.setDegree(0); - relationship3.setType(downstreamOf); - relationship3.setEntity(datasetThreeUrn); - - LineageRelationship relationship5 = new LineageRelationship(); - relationship5.setDegree(0); - relationship5.setType(downstreamOf); - relationship5.setEntity(datasetFiveUrn); + LineageRelationship relationship1 = makeBasicRelationship(datasetOneUrn); + LineageRelationship relationship2 = makeBasicRelationship(datasetTwoUrn); + LineageRelationship relationship5 = makeBasicRelationship(datasetFiveUrn); relationships.add(relationship1); @@ -607,11 +506,7 @@ public void testUpstreamOfSiblingSiblings() { LineageRelationshipArray relationships = new LineageRelationshipArray(); LineageRelationshipArray expectedRelationships = new LineageRelationshipArray(); - LineageRelationship relationship = new LineageRelationship(); - relationship.setDegree(0); - relationship.setType(downstreamOf); - relationship.setEntity(datasetFourUrn); - + LineageRelationship relationship = makeBasicRelationship(datasetFourUrn); relationships.add(relationship); expectedRelationships.add(relationship); @@ -722,25 +617,10 @@ public void testRelationshipWithSibling() throws CloneNotSupportedException { LineageRelationshipArray siblingRelationships = new LineageRelationshipArray(); LineageRelationshipArray expectedRelationships = new LineageRelationshipArray(); - LineageRelationship relationship1 = new LineageRelationship(); - relationship1.setDegree(0); - relationship1.setType(downstreamOf); - relationship1.setEntity(datasetOneUrn); - - LineageRelationship relationship2 = new LineageRelationship(); - relationship2.setDegree(0); - relationship2.setType(downstreamOf); - relationship2.setEntity(datasetTwoUrn); - - LineageRelationship relationship3 = new LineageRelationship(); - relationship3.setDegree(0); - relationship3.setType(downstreamOf); - relationship3.setEntity(datasetThreeUrn); - - LineageRelationship relationship5 = new LineageRelationship(); - relationship5.setDegree(0); - relationship5.setType(downstreamOf); - relationship5.setEntity(datasetFiveUrn); + LineageRelationship relationship1 = makeBasicRelationship(datasetOneUrn); + LineageRelationship relationship2 = makeBasicRelationship(datasetTwoUrn); + LineageRelationship relationship3 = makeBasicRelationship(datasetThreeUrn); + LineageRelationship relationship5 = makeBasicRelationship(datasetFiveUrn); relationships.add(relationship1); // relationship between entity and its sibling @@ -1006,7 +886,7 @@ public void testSiblingCombinations() throws URISyntaxException { // Tests for separateSiblings = true: primary sibling EntityLineageResult primaryDownstreamSeparated = service.getLineage( - opContext, primarySiblingUrn, LineageDirection.DOWNSTREAM, 0, 100, 1, true, Set.of()); + opContext, primarySiblingUrn, LineageDirection.DOWNSTREAM, 0, 100, 1, true, false); LineageRelationshipArray expectedRelationships = new LineageRelationshipArray(); expectedRelationships.add(relationship); @@ -1022,7 +902,7 @@ public void testSiblingCombinations() throws URISyntaxException { EntityLineageResult primaryUpstreamSeparated = service.getLineage( - opContext, primarySiblingUrn, LineageDirection.UPSTREAM, 0, 100, 1, true, Set.of()); + opContext, primarySiblingUrn, LineageDirection.UPSTREAM, 0, 100, 1, true, false); EntityLineageResult expectedResultPrimaryUpstreamSeparated = new EntityLineageResult(); expectedResultPrimaryUpstreamSeparated.setCount(2); expectedResultPrimaryUpstreamSeparated.setStart(0); @@ -1035,7 +915,7 @@ public void testSiblingCombinations() throws URISyntaxException { // Test for separateSiblings = true, secondary sibling EntityLineageResult secondarySiblingSeparated = service.getLineage( - opContext, alternateSiblingUrn, LineageDirection.DOWNSTREAM, 0, 100, 1, true, Set.of()); + opContext, alternateSiblingUrn, LineageDirection.DOWNSTREAM, 0, 100, 1, true, false); EntityLineageResult expectedResultSecondarySeparated = new EntityLineageResult(); expectedResultSecondarySeparated.setCount(numDownstreams); @@ -1048,7 +928,7 @@ public void testSiblingCombinations() throws URISyntaxException { EntityLineageResult secondaryUpstreamSeparated = service.getLineage( - opContext, alternateSiblingUrn, LineageDirection.UPSTREAM, 0, 100, 1, true, Set.of()); + opContext, alternateSiblingUrn, LineageDirection.UPSTREAM, 0, 100, 1, true, false); EntityLineageResult expectedResultSecondaryUpstreamSeparated = new EntityLineageResult(); expectedResultSecondaryUpstreamSeparated.setCount(3); expectedResultSecondaryUpstreamSeparated.setStart(0); @@ -1060,15 +940,7 @@ public void testSiblingCombinations() throws URISyntaxException { // Test for separateSiblings = false, primary sibling EntityLineageResult primarySiblingNonSeparated = - service.getLineage( - opContext, - primarySiblingUrn, - LineageDirection.DOWNSTREAM, - 0, - 100, - 1, - false, - new HashSet<>()); + service.getLineage(opContext, primarySiblingUrn, LineageDirection.DOWNSTREAM, 0, 100, 1); EntityLineageResult expectedResultPrimaryNonSeparated = new EntityLineageResult(); expectedResultPrimaryNonSeparated.setCount(numDownstreams); expectedResultPrimaryNonSeparated.setStart(0); @@ -1078,15 +950,7 @@ public void testSiblingCombinations() throws URISyntaxException { assertEquals(primarySiblingNonSeparated, expectedResultPrimaryNonSeparated); EntityLineageResult primarySiblingNonSeparatedUpstream = - service.getLineage( - opContext, - primarySiblingUrn, - LineageDirection.UPSTREAM, - 0, - 100, - 1, - false, - new HashSet<>()); + service.getLineage(opContext, primarySiblingUrn, LineageDirection.UPSTREAM, 0, 100, 1); EntityLineageResult expectedResultPrimaryUpstreamNonSeparated = new EntityLineageResult(); expectedResultPrimaryUpstreamNonSeparated.setCount(2); expectedResultPrimaryUpstreamNonSeparated.setStart(0); @@ -1097,30 +961,84 @@ public void testSiblingCombinations() throws URISyntaxException { // Test for separateSiblings = false, secondary sibling EntityLineageResult secondarySiblingNonSeparated = - service.getLineage( - opContext, - alternateSiblingUrn, - LineageDirection.DOWNSTREAM, - 0, - 100, - 1, - false, - new HashSet<>()); + service.getLineage(opContext, alternateSiblingUrn, LineageDirection.DOWNSTREAM, 0, 100, 1); assertEquals(secondarySiblingNonSeparated, expectedResultPrimaryNonSeparated); EntityLineageResult secondarySiblingNonSeparatedUpstream = - service.getLineage( - opContext, - alternateSiblingUrn, - LineageDirection.UPSTREAM, - 0, - 100, - 1, - false, - new HashSet<>()); + service.getLineage(opContext, alternateSiblingUrn, LineageDirection.UPSTREAM, 0, 100, 1); assertEquals(secondarySiblingNonSeparatedUpstream, expectedResultPrimaryUpstreamNonSeparated); } + @Test + public void testExcludeGhostEntities() { + when(_mockEntityService.exists(any(OperationContext.class), any(Collection.class), eq(false))) + .thenAnswer(args -> Set.of(datasetOneUrn)); + + EntityLineageResult mockGraphResult = makeBasicMockResult(); + + when(_graphService.getLineage( + any(OperationContext.class), + eq(datasetFourUrn), + eq(LineageDirection.UPSTREAM), + eq(0), + eq(100), + eq(1))) + .thenReturn(mockGraphResult); + + when(_mockEntityService.getLatestAspect( + any(OperationContext.class), eq(datasetFourUrn), eq(SIBLINGS_ASPECT_NAME))) + .thenReturn(null); + + SiblingGraphService service = _client; + + EntityLineageResult upstreamLineage = + service.getLineage(opContext, datasetFourUrn, LineageDirection.UPSTREAM, 0, 100, 1); + + EntityLineageResult mockResult = new EntityLineageResult(); + mockResult.setStart(0); + mockResult.setTotal(3); + mockResult.setCount(3); + mockResult.setFiltered(2); + LineageRelationshipArray relationshipsResult = new LineageRelationshipArray(); + relationshipsResult.add(makeBasicRelationship(datasetOneUrn)); + mockResult.setRelationships(relationshipsResult); + + // assert sibling graph service filters out entities that do not exist + assertEquals(upstreamLineage, mockResult); + } + + @Test + public void testIncludeGhostEntities() { + when(_mockEntityService.exists( + any(OperationContext.class), any(Collection.class), any(Boolean.class))) + .thenAnswer(args -> Set.of(datasetOneUrn)); + + EntityLineageResult mockResult = makeBasicMockResult(); + + when(_graphService.getLineage( + any(OperationContext.class), + eq(datasetFourUrn), + eq(LineageDirection.UPSTREAM), + eq(0), + eq(100), + eq(1))) + .thenReturn(mockResult); + + when(_mockEntityService.getLatestAspect( + any(OperationContext.class), eq(datasetFourUrn), eq(SIBLINGS_ASPECT_NAME))) + .thenReturn(null); + + SiblingGraphService service = _client; + + EntityLineageResult upstreamLineage = + service.getLineage( + opContext, datasetFourUrn, LineageDirection.UPSTREAM, 0, 100, 1, false, true); + + // assert sibling graph service is a pass through when there are no siblings and + // includeGhostEntities + assertEquals(upstreamLineage, mockResult); + } + static Urn createFromString(@Nonnull String rawUrn) { try { return Urn.createFromString(rawUrn); @@ -1128,4 +1046,29 @@ static Urn createFromString(@Nonnull String rawUrn) { return null; } } + + static LineageRelationship makeBasicRelationship(Urn urn) { + LineageRelationship relationship = new LineageRelationship(); + relationship.setDegree(0); + relationship.setType(downstreamOf); + relationship.setEntity(urn); + return relationship; + } + + static EntityLineageResult makeBasicMockResult() { + LineageRelationshipArray relationships = new LineageRelationshipArray(); + LineageRelationship relationship1 = makeBasicRelationship(datasetOneUrn); + LineageRelationship relationship2 = makeBasicRelationship(datasetTwoUrn); + LineageRelationship relationship3 = makeBasicRelationship(datasetThreeUrn); + relationships.addAll(List.of(relationship1, relationship2, relationship3)); + + EntityLineageResult mockResult = new EntityLineageResult(); + mockResult.setStart(0); + mockResult.setTotal(3); + mockResult.setCount(3); + mockResult.setFiltered(0); + mockResult.setRelationships(relationships); + + return mockResult; + } } From 364496c3d708ba6770d2b719946f97ee3484111e Mon Sep 17 00:00:00 2001 From: Mayuri Nehate <33225191+mayurinehate@users.noreply.github.com> Date: Sat, 5 Oct 2024 00:02:57 +0530 Subject: [PATCH 11/25] fix(ingest/sigma): handle members api paginated response (#11535) --- .../ingestion/source/sigma/sigma_api.py | 19 ++++++--- .../tests/integration/sigma/test_sigma.py | 42 ++++++++++--------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/metadata-ingestion/src/datahub/ingestion/source/sigma/sigma_api.py b/metadata-ingestion/src/datahub/ingestion/source/sigma/sigma_api.py index 66d4678e521328..3e88f43142ede6 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/sigma/sigma_api.py +++ b/metadata-ingestion/src/datahub/ingestion/source/sigma/sigma_api.py @@ -141,12 +141,19 @@ def _get_users(self) -> Dict[str, str]: logger.debug("Fetching all accessible users metadata.") try: users: Dict[str, str] = {} - response = self._get_api_call(f"{self.config.api_url}/members") - response.raise_for_status() - for user_dict in response.json(): - users[ - user_dict[Constant.MEMBERID] - ] = f"{user_dict[Constant.FIRSTNAME]}_{user_dict[Constant.LASTNAME]}" + members_url = url = f"{self.config.api_url}/members?limit=50" + while True: + response = self._get_api_call(url) + response.raise_for_status() + response_dict = response.json() + for user_dict in response_dict[Constant.ENTRIES]: + users[ + user_dict[Constant.MEMBERID] + ] = f"{user_dict[Constant.FIRSTNAME]}_{user_dict[Constant.LASTNAME]}" + if response_dict[Constant.NEXTPAGE]: + url = f"{members_url}&page={response_dict[Constant.NEXTPAGE]}" + else: + break return users except Exception as e: self._log_http_error( diff --git a/metadata-ingestion/tests/integration/sigma/test_sigma.py b/metadata-ingestion/tests/integration/sigma/test_sigma.py index b6e9db99eed39a..6c01bf6dc80fe7 100644 --- a/metadata-ingestion/tests/integration/sigma/test_sigma.py +++ b/metadata-ingestion/tests/integration/sigma/test_sigma.py @@ -381,25 +381,29 @@ def register_mock_api(request_mock: Any, override_data: dict = {}) -> None: "https://aws-api.sigmacomputing.com/v2/members": { "method": "GET", "status_code": 200, - "json": [ - { - "organizationId": "b94da709-176c-4242-bea6-6760f34c9228", - "memberId": "CPbEdA26GNQ2cM2Ra2BeO0fa5Awz1", - "memberType": "admin", - "firstName": "Shubham", - "lastName": "Jagtap", - "email": "john.doe@example.com", - "profileImgUrl": None, - "createdBy": "CPbEdA26GNQ2cM2Ra2BeO0fa5Awz1", - "updatedBy": "CPbEdA26GNQ2cM2Ra2BeO0fa5Awz1", - "createdAt": "2023-11-28T10:59:20.957Z", - "updatedAt": "2024-03-12T21:21:17.996Z", - "homeFolderId": "9bb94df1-e8af-49eb-9c37-2bd40b0efb2e", - "userKind": "internal", - "isArchived": False, - "isInactive": False, - }, - ], + "json": { + "entries": [ + { + "organizationId": "b94da709-176c-4242-bea6-6760f34c9228", + "memberId": "CPbEdA26GNQ2cM2Ra2BeO0fa5Awz1", + "memberType": "admin", + "firstName": "Shubham", + "lastName": "Jagtap", + "email": "john.doe@example.com", + "profileImgUrl": None, + "createdBy": "CPbEdA26GNQ2cM2Ra2BeO0fa5Awz1", + "updatedBy": "CPbEdA26GNQ2cM2Ra2BeO0fa5Awz1", + "createdAt": "2023-11-28T10:59:20.957Z", + "updatedAt": "2024-03-12T21:21:17.996Z", + "homeFolderId": "9bb94df1-e8af-49eb-9c37-2bd40b0efb2e", + "userKind": "internal", + "isArchived": False, + "isInactive": False, + }, + ], + "total": 1, + "nextPage": None, + }, }, } From 7c6d31ca01af923bc45ac3ee340ea86d79c2b425 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 4 Oct 2024 21:41:01 +0300 Subject: [PATCH 12/25] fix(metadata-service): Pass editableDatasetNameEnabled feature flag to the config. (#11391) Co-authored-by: Kristina Kurshakova --- .../configuration/src/main/resources/application.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml index 3177a971251946..24764b512c232b 100644 --- a/metadata-service/configuration/src/main/resources/application.yaml +++ b/metadata-service/configuration/src/main/resources/application.yaml @@ -436,6 +436,7 @@ featureFlags: businessAttributeEntityEnabled: ${BUSINESS_ATTRIBUTE_ENTITY_ENABLED:false} # Enables business attribute entity which can be associated with field of dataset dataContractsEnabled: ${DATA_CONTRACTS_ENABLED:true} # Enables the Data Contracts feature (Tab) in the UI showSeparateSiblings: ${SHOW_SEPARATE_SIBLINGS:false} # If turned on, all siblings will be separated with no way to get to a "combined" sibling view + editableDatasetNameEnabled: ${EDITABLE_DATASET_NAME_ENABLED:false} # Enables the ability to edit the dataset name in the UI entityChangeEvents: enabled: ${ENABLE_ENTITY_CHANGE_EVENTS_HOOK:true} From cc63f53c6a17f331c9ea52e09d923d0206f12b3c Mon Sep 17 00:00:00 2001 From: Nbagga14 <114913591+Nbagga14@users.noreply.github.com> Date: Sat, 5 Oct 2024 00:35:29 +0530 Subject: [PATCH 13/25] Added IEQUAL operator to support case insensitive searches (#11501) Co-authored-by: david-leifker <114954101+david-leifker@users.noreply.github.com> --- .../src/main/resources/search.graphql | 6 ++ docs/api/restli/restli-overview.md | 1 + .../metadata/search/utils/ESUtils.java | 53 ++++++++++-- .../metadata/search/utils/ESUtilsTest.java | 80 +++++++++++++++++++ .../metadata/query/filter/Condition.pdl | 5 ++ metadata-service/README.md | 1 + ...linkedin.analytics.analytics.snapshot.json | 3 +- .../com.linkedin.entity.aspects.snapshot.json | 3 +- ...com.linkedin.entity.entities.snapshot.json | 3 +- 9 files changed, 145 insertions(+), 10 deletions(-) diff --git a/datahub-graphql-core/src/main/resources/search.graphql b/datahub-graphql-core/src/main/resources/search.graphql index 32d73845e1ae40..d0f669f05f9598 100644 --- a/datahub-graphql-core/src/main/resources/search.graphql +++ b/datahub-graphql-core/src/main/resources/search.graphql @@ -521,6 +521,11 @@ enum FilterOperator { """ EQUAL + """ + Represent the relation: field = value (case-insensitive), e.g. platform = HDFS + """ + IEQUAL + """ * Represent the relation: String field is one of the array values to, e.g. name in ["Profile", "Event"] """ @@ -575,6 +580,7 @@ enum FilterOperator { Represent the relation: URN field matches any nested child or parent in addition to the given URN """ RELATED_INCL + } """ diff --git a/docs/api/restli/restli-overview.md b/docs/api/restli/restli-overview.md index d8a81075263747..22b913d9a25df4 100644 --- a/docs/api/restli/restli-overview.md +++ b/docs/api/restli/restli-overview.md @@ -1203,6 +1203,7 @@ where valid conditions include - CONTAIN - END_WITH - EQUAL + - IEQUAL (Supports case insensitive equals) - GREATER_THAN - GREATER_THAN_OR_EQUAL_TO - LESS_THAN diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java index ace7fa2bc197c6..9f48727aeca780 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java @@ -552,6 +552,10 @@ private static QueryBuilder getQueryBuilderFromCriterionForFieldToExpand( return orQueryBuilder; } + private static boolean isCaseInsensitiveSearchEnabled(Condition condition) { + return condition == Condition.IEQUAL; + } + @Nonnull private static QueryBuilder getQueryBuilderFromCriterionForSingleField( @Nonnull Criterion criterion, @@ -564,6 +568,8 @@ private static QueryBuilder getQueryBuilderFromCriterionForSingleField( final AspectRetriever aspectRetriever = opContext.getAspectRetriever(); final String fieldName = toParentField(criterion.getField(), aspectRetriever); + boolean enableCaseInsensitiveSearch; + if (condition == Condition.IS_NULL) { return QueryBuilders.boolQuery() .mustNot(QueryBuilders.existsQuery(fieldName)) @@ -573,9 +579,15 @@ private static QueryBuilder getQueryBuilderFromCriterionForSingleField( .must(QueryBuilders.existsQuery(fieldName)) .queryName(queryName != null ? queryName : fieldName); } else if (criterion.hasValues()) { - if (condition == Condition.EQUAL) { + if (condition == Condition.EQUAL || condition == Condition.IEQUAL) { + enableCaseInsensitiveSearch = isCaseInsensitiveSearchEnabled(condition); return buildEqualsConditionFromCriterion( - fieldName, criterion, isTimeseries, searchableFieldTypes, aspectRetriever) + fieldName, + criterion, + isTimeseries, + searchableFieldTypes, + aspectRetriever, + enableCaseInsensitiveSearch) .queryName(queryName != null ? queryName : fieldName); } else if (RANGE_QUERY_CONDITIONS.contains(condition)) { return buildRangeQueryFromCriterion( @@ -596,7 +608,7 @@ private static QueryBuilder getQueryBuilderFromCriterionForSingleField( return buildEndsWithConditionFromCriterion( fieldName, criterion, queryName, isTimeseries, aspectRetriever); } else if (Set.of(ANCESTORS_INCL, DESCENDANTS_INCL, RELATED_INCL).contains(condition)) { - + enableCaseInsensitiveSearch = isCaseInsensitiveSearchEnabled(condition); return QueryFilterRewriterContext.builder() .queryFilterRewriteChain(queryFilterRewriteChain) .condition(condition) @@ -605,7 +617,12 @@ private static QueryBuilder getQueryBuilderFromCriterionForSingleField( .rewrite( opContext, buildEqualsConditionFromCriterion( - fieldName, criterion, isTimeseries, searchableFieldTypes, aspectRetriever)) + fieldName, + criterion, + isTimeseries, + searchableFieldTypes, + aspectRetriever, + enableCaseInsensitiveSearch)) .queryName(queryName != null ? queryName : fieldName); } } @@ -670,9 +687,15 @@ private static QueryBuilder buildEqualsConditionFromCriterion( @Nonnull final Criterion criterion, final boolean isTimeseries, final Map> searchableFieldTypes, - @Nonnull AspectRetriever aspectRetriever) { + @Nonnull AspectRetriever aspectRetriever, + boolean enableCaseInsensitiveSearch) { return buildEqualsConditionFromCriterionWithValues( - fieldName, criterion, isTimeseries, searchableFieldTypes, aspectRetriever); + fieldName, + criterion, + isTimeseries, + searchableFieldTypes, + aspectRetriever, + enableCaseInsensitiveSearch); } /** @@ -684,7 +707,8 @@ private static QueryBuilder buildEqualsConditionFromCriterionWithValues( @Nonnull final Criterion criterion, final boolean isTimeseries, final Map> searchableFieldTypes, - @Nonnull AspectRetriever aspectRetriever) { + @Nonnull AspectRetriever aspectRetriever, + boolean enableCaseInsensitiveSearch) { Set fieldTypes = getFieldTypes(searchableFieldTypes, fieldName, aspectRetriever); if (fieldTypes.size() > 1) { log.warn( @@ -704,6 +728,21 @@ private static QueryBuilder buildEqualsConditionFromCriterionWithValues( criterion.getValues().stream().map(Double::parseDouble).collect(Collectors.toList()); return QueryBuilders.termsQuery(fieldName, doubleValues).queryName(fieldName); } + + if (enableCaseInsensitiveSearch) { + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); + criterion + .getValues() + .forEach( + value -> + boolQuery.should( + QueryBuilders.termQuery( + toKeywordField(criterion.getField(), isTimeseries, aspectRetriever), + value.trim()) + .caseInsensitive(true))); + return boolQuery; + } + return QueryBuilders.termsQuery( toKeywordField(criterion.getField(), isTimeseries, aspectRetriever), criterion.getValues()) diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java index 03d104b9e7bfb3..928818f8c15eb7 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java @@ -101,6 +101,7 @@ public void testGetQueryBuilderFromCriterionEqualsValues() { + " \"_name\" : \"myTestField\"\n" + " }\n" + "}"; + Assert.assertEquals(result.toString(), expected); final Criterion multiValueCriterion = @@ -150,6 +151,85 @@ public void testGetQueryBuilderFromCriterionEqualsValues() { Assert.assertEquals(result.toString(), expected); } + @Test + public void testGetQueryBuilderFromCriterionIEqualValues() { // Test case insensitive searches + + final Criterion singleValueCriterion = + buildCriterion("myTestField", Condition.IEQUAL, "value1"); + + QueryBuilder result = + ESUtils.getQueryBuilderFromCriterion( + singleValueCriterion, + false, + new HashMap<>(), + mock(OperationContext.class), + QueryFilterRewriteChain.EMPTY); + + String expected = + "{\n" + + " \"bool\" : {\n" + + " \"should\" : [\n" + + " {\n" + + " \"term\" : {\n" + + " \"myTestField.keyword\" : {\n" + + " \"value\" : \"value1\",\n" + + " \"case_insensitive\" : true,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"adjust_pure_negative\" : true,\n" + + " \"boost\" : 1.0,\n" + + " \"_name\" : \"myTestField\"\n" + + " }\n" + + "}"; + + Assert.assertEquals(result.toString(), expected); + + final Criterion multiValueCriterion = + buildCriterion("myTestField", Condition.IEQUAL, "value1", "value2"); + + result = + ESUtils.getQueryBuilderFromCriterion( + multiValueCriterion, + false, + new HashMap<>(), + mock(OperationContext.class), + QueryFilterRewriteChain.EMPTY); + + expected = + "{\n" + + " \"bool\" : {\n" + + " \"should\" : [\n" + + " {\n" + + " \"term\" : {\n" + + " \"myTestField.keyword\" : {\n" + + " \"value\" : \"value1\",\n" + + " \"case_insensitive\" : true,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\" : {\n" + + " \"myTestField.keyword\" : {\n" + + " \"value\" : \"value2\",\n" + + " \"case_insensitive\" : true,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"adjust_pure_negative\" : true,\n" + + " \"boost\" : 1.0,\n" + + " \"_name\" : \"myTestField\"\n" + + " }\n" + + "}"; + + Assert.assertEquals(result.toString(), expected); + } + @Test public void testGetQueryBuilderFromCriterionContain() { final Criterion singleValueCriterion = diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/query/filter/Condition.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/query/filter/Condition.pdl index a79055ea3db547..193e7628546294 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/metadata/query/filter/Condition.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/query/filter/Condition.pdl @@ -20,6 +20,11 @@ enum Condition { */ EQUAL + /** + * Represent the relation: field = value and support case insensitive values, e.g. platform = hdfs + */ + IEQUAL + /** * Represent the relation: field is null, e.g. platform is null */ diff --git a/metadata-service/README.md b/metadata-service/README.md index 8aec1ecc3ab92a..0c7085b10da713 100644 --- a/metadata-service/README.md +++ b/metadata-service/README.md @@ -1291,6 +1291,7 @@ where valid conditions include - CONTAIN - END_WITH - EQUAL + - IEQUAL (support case insensitive values) - GREATER_THAN - GREATER_THAN_OR_EQUAL_TO - LESS_THAN diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.analytics.analytics.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.analytics.analytics.snapshot.json index e8cc193f3458d0..061feafac1b9b9 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.analytics.analytics.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.analytics.analytics.snapshot.json @@ -56,13 +56,14 @@ "type" : "enum", "name" : "Condition", "doc" : "The matching condition in a filter criterion", - "symbols" : [ "CONTAIN", "END_WITH", "EQUAL", "IS_NULL", "EXISTS", "GREATER_THAN", "GREATER_THAN_OR_EQUAL_TO", "IN", "LESS_THAN", "LESS_THAN_OR_EQUAL_TO", "START_WITH", "DESCENDANTS_INCL", "ANCESTORS_INCL", "RELATED_INCL" ], + "symbols" : [ "CONTAIN", "END_WITH", "EQUAL", "IEQUAL", "IS_NULL", "EXISTS", "GREATER_THAN", "GREATER_THAN_OR_EQUAL_TO", "IN", "LESS_THAN", "LESS_THAN_OR_EQUAL_TO", "START_WITH", "DESCENDANTS_INCL", "ANCESTORS_INCL", "RELATED_INCL" ], "symbolDocs" : { "ANCESTORS_INCL" : "Represent the relation: URN field matches any nested parent in addition to the given URN", "CONTAIN" : "Represent the relation: String field contains value, e.g. name contains Profile", "DESCENDANTS_INCL" : "Represent the relation: URN field any nested children in addition to the given URN", "END_WITH" : "Represent the relation: String field ends with value, e.g. name ends with Event", "EQUAL" : "Represent the relation: field = value, e.g. platform = hdfs", + "IEQUAL" : "Represent the relation: field = value and support case insensitive values, e.g. platform = hdfs", "EXISTS" : "Represents the relation: field exists and is non-empty, e.g. owners is not null and != [] (empty)", "GREATER_THAN" : "Represent the relation greater than, e.g. ownerCount > 5", "GREATER_THAN_OR_EQUAL_TO" : "Represent the relation greater than or equal to, e.g. ownerCount >= 5", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json index bc4d222e316b0e..fa58edb41cdfb4 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json @@ -162,13 +162,14 @@ "type" : "enum", "name" : "Condition", "doc" : "The matching condition in a filter criterion", - "symbols" : [ "CONTAIN", "END_WITH", "EQUAL", "IS_NULL", "EXISTS", "GREATER_THAN", "GREATER_THAN_OR_EQUAL_TO", "IN", "LESS_THAN", "LESS_THAN_OR_EQUAL_TO", "START_WITH", "DESCENDANTS_INCL", "ANCESTORS_INCL", "RELATED_INCL" ], + "symbols" : [ "CONTAIN", "END_WITH", "EQUAL", "IEQUAL", "IS_NULL", "EXISTS", "GREATER_THAN", "GREATER_THAN_OR_EQUAL_TO", "IN", "LESS_THAN", "LESS_THAN_OR_EQUAL_TO", "START_WITH", "DESCENDANTS_INCL", "ANCESTORS_INCL", "RELATED_INCL" ], "symbolDocs" : { "ANCESTORS_INCL" : "Represent the relation: URN field matches any nested parent in addition to the given URN", "CONTAIN" : "Represent the relation: String field contains value, e.g. name contains Profile", "DESCENDANTS_INCL" : "Represent the relation: URN field any nested children in addition to the given URN", "END_WITH" : "Represent the relation: String field ends with value, e.g. name ends with Event", "EQUAL" : "Represent the relation: field = value, e.g. platform = hdfs", + "IEQUAL" : "Represent the relation: field = value and support case insensitive values, e.g. platform = hdfs", "EXISTS" : "Represents the relation: field exists and is non-empty, e.g. owners is not null and != [] (empty)", "GREATER_THAN" : "Represent the relation greater than, e.g. ownerCount > 5", "GREATER_THAN_OR_EQUAL_TO" : "Represent the relation greater than or equal to, e.g. ownerCount >= 5", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json index 8ff0aa930770cd..086f21cfe7f497 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json @@ -6057,13 +6057,14 @@ "name" : "Condition", "namespace" : "com.linkedin.metadata.query.filter", "doc" : "The matching condition in a filter criterion", - "symbols" : [ "CONTAIN", "END_WITH", "EQUAL", "IS_NULL", "EXISTS", "GREATER_THAN", "GREATER_THAN_OR_EQUAL_TO", "IN", "LESS_THAN", "LESS_THAN_OR_EQUAL_TO", "START_WITH", "DESCENDANTS_INCL", "ANCESTORS_INCL", "RELATED_INCL" ], + "symbols" : [ "CONTAIN", "END_WITH", "EQUAL","IEQUAL", "IS_NULL", "EXISTS", "GREATER_THAN", "GREATER_THAN_OR_EQUAL_TO", "IN", "LESS_THAN", "LESS_THAN_OR_EQUAL_TO", "START_WITH", "DESCENDANTS_INCL", "ANCESTORS_INCL", "RELATED_INCL" ], "symbolDocs" : { "ANCESTORS_INCL" : "Represent the relation: URN field matches any nested parent in addition to the given URN", "CONTAIN" : "Represent the relation: String field contains value, e.g. name contains Profile", "DESCENDANTS_INCL" : "Represent the relation: URN field any nested children in addition to the given URN", "END_WITH" : "Represent the relation: String field ends with value, e.g. name ends with Event", "EQUAL" : "Represent the relation: field = value, e.g. platform = hdfs", + "IEQUAL" : "Represent the relation: field = value and support case insensitive values, e.g. platform = hdfs", "EXISTS" : "Represents the relation: field exists and is non-empty, e.g. owners is not null and != [] (empty)", "GREATER_THAN" : "Represent the relation greater than, e.g. ownerCount > 5", "GREATER_THAN_OR_EQUAL_TO" : "Represent the relation greater than or equal to, e.g. ownerCount >= 5", From d1220a6636882ff8dad9933b2a88951530489dda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:14:46 -0500 Subject: [PATCH 14/25] build(deps): bump rollup from 3.29.4 to 3.29.5 in /datahub-web-react (#11459) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- datahub-web-react/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/datahub-web-react/yarn.lock b/datahub-web-react/yarn.lock index 8d5899d9891f18..76b5314ea80e07 100644 --- a/datahub-web-react/yarn.lock +++ b/datahub-web-react/yarn.lock @@ -9504,9 +9504,9 @@ rimraf@~2.6.2: glob "^7.1.3" rollup@^3.27.1: - version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" - integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + version "3.29.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" + integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w== optionalDependencies: fsevents "~2.3.2" From 36ba8ef205c497558446bfca5100e70e52be3d8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:15:44 -0500 Subject: [PATCH 15/25] build(deps): bump express from 4.19.2 to 4.20.0 in /docs-website (#11357) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs-website/yarn.lock | 85 ++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/docs-website/yarn.lock b/docs-website/yarn.lock index ee300b5cb7672e..039dac5d5556b2 100644 --- a/docs-website/yarn.lock +++ b/docs-website/yarn.lock @@ -4085,10 +4085,10 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -4098,7 +4098,7 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -5307,6 +5307,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -5501,36 +5506,36 @@ expand-template@^2.0.3: integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== express@^4.17.3: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + version "4.20.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.20.0.tgz#f1d08e591fcec770c07be4767af8eb9bcfd67c48" + integrity sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" finalhandler "1.2.0" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" qs "6.11.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.0" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -7565,10 +7570,10 @@ memfs@^3.1.2, memfs@^3.4.3: dependencies: fs-monkey "^1.0.4" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -8684,10 +8689,10 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-to-regexp@2.2.1: version "2.2.1" @@ -9186,6 +9191,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -10345,6 +10357,25 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -10379,10 +10410,10 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.0.tgz#2bf4ed49f8af311b519c46f272bf6ac3baf38a92" + integrity sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" @@ -10468,7 +10499,7 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -side-channel@^1.0.4: +side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== From 759f69e2928a383a4eeee7a52c65c89fe21b7e06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:18:46 -0500 Subject: [PATCH 16/25] build(deps): bump fast-loops from 1.1.3 to 1.1.4 in /datahub-web-react (#10885) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- datahub-web-react/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/datahub-web-react/yarn.lock b/datahub-web-react/yarn.lock index 76b5314ea80e07..9dc563c958dd19 100644 --- a/datahub-web-react/yarn.lock +++ b/datahub-web-react/yarn.lock @@ -5964,9 +5964,9 @@ fast-levenshtein@^2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fast-loops@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.3.tgz#ce96adb86d07e7bf9b4822ab9c6fac9964981f75" - integrity sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g== + version "1.1.4" + resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.4.tgz#61bc77d518c0af5073a638c6d9d5c7683f069ce2" + integrity sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg== fast-querystring@^1.1.1: version "1.1.2" From 5cfcf406c94bbb42b0614ff07ef240bd4555136a Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 4 Oct 2024 21:44:26 +0100 Subject: [PATCH 17/25] feat(fabric): Add sandbox as a possible environment variable (#11491) Co-authored-by: david-leifker <114954101+david-leifker@users.noreply.github.com> --- datahub-graphql-core/src/main/resources/entity.graphql | 5 +++++ docs/how/updating-datahub.md | 1 + .../src/main/pegasus/com/linkedin/common/FabricType.pdl | 5 +++++ .../snapshot/com.linkedin.entity.aspects.snapshot.json | 3 ++- .../snapshot/com.linkedin.entity.entities.snapshot.json | 9 +++------ .../main/snapshot/com.linkedin.entity.runs.snapshot.json | 3 ++- .../com.linkedin.operations.operations.snapshot.json | 3 ++- .../com.linkedin.platform.platform.snapshot.json | 9 +++------ 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 07dcdf8cc95845..16ef59114f86c8 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -2726,6 +2726,11 @@ enum FabricType { Designates review fabrics """ RVW + + """ + Designates sandbox fabrics + """ + SANDBOX } """ diff --git a/docs/how/updating-datahub.md b/docs/how/updating-datahub.md index f878a864e20fab..89ea8ce8c543ab 100644 --- a/docs/how/updating-datahub.md +++ b/docs/how/updating-datahub.md @@ -23,6 +23,7 @@ This file documents any backwards-incompatible changes in DataHub and assists pe - #11486 - Deprecated Criterion filters using `value`. Use `values` instead. This also deprecates the ability to use comma delimited string to represent multiple values using `value`. - #11484 - Metadata service authentication enabled by default - #11484 - Rest API authorization enabled by default +- #10472 - `SANDBOX` added as a FabricType. No rollbacks allowed once metadata with this fabric type is added without manual cleanups in databases. ### Potential Downtime diff --git a/li-utils/src/main/pegasus/com/linkedin/common/FabricType.pdl b/li-utils/src/main/pegasus/com/linkedin/common/FabricType.pdl index 366843e460cb35..fd7bdfc88614ff 100644 --- a/li-utils/src/main/pegasus/com/linkedin/common/FabricType.pdl +++ b/li-utils/src/main/pegasus/com/linkedin/common/FabricType.pdl @@ -59,4 +59,9 @@ enum FabricType { * Designates review fabrics */ RVW + + /** + * Designates sandbox fabrics + */ + SANDBOX } diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json index fa58edb41cdfb4..30d0e9a09cdf41 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json @@ -843,7 +843,7 @@ "name" : "FabricType", "namespace" : "com.linkedin.common", "doc" : "Fabric group type", - "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW" ], + "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW", "SANDBOX" ], "symbolDocs" : { "CORP" : "Designates corporation fabrics", "DEV" : "Designates development fabrics", @@ -853,6 +853,7 @@ "PROD" : "Designates production fabrics", "QA" : "Designates quality assurance fabrics", "RVW" : "Designates review fabrics", + "SANDBOX" : "Designates sandbox fabrics", "STG" : "Designates staging fabrics", "TEST" : "Designates testing fabrics", "UAT" : "Designates user acceptance testing fabrics" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json index 086f21cfe7f497..8cf02a768ecae2 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json @@ -834,7 +834,7 @@ "name" : "FabricType", "namespace" : "com.linkedin.common", "doc" : "Fabric group type", - "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW" ], + "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW", "SANDBOX" ], "symbolDocs" : { "CORP" : "Designates corporation fabrics", "DEV" : "Designates development fabrics", @@ -844,6 +844,7 @@ "PROD" : "Designates production fabrics", "QA" : "Designates quality assurance fabrics", "RVW" : "Designates review fabrics", + "SANDBOX" : "Designates sandbox fabrics", "STG" : "Designates staging fabrics", "TEST" : "Designates testing fabrics", "UAT" : "Designates user acceptance testing fabrics" @@ -4542,11 +4543,7 @@ "name" : "description", "type" : "string", "doc" : "Documentation of the MLPrimaryKey", - "optional" : true, - "Searchable" : { - "fieldType" : "TEXT", - "hasValuesFieldName" : "hasDescription" - } + "optional" : true }, { "name" : "dataType", "type" : "com.linkedin.common.MLFeatureDataType", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json index 16549757a961fa..d06f3b737a3e17 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json @@ -576,7 +576,7 @@ "name" : "FabricType", "namespace" : "com.linkedin.common", "doc" : "Fabric group type", - "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW" ], + "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW", "SANDBOX" ], "symbolDocs" : { "CORP" : "Designates corporation fabrics", "DEV" : "Designates development fabrics", @@ -586,6 +586,7 @@ "PROD" : "Designates production fabrics", "QA" : "Designates quality assurance fabrics", "RVW" : "Designates review fabrics", + "SANDBOX" : "Designates sandbox fabrics", "STG" : "Designates staging fabrics", "TEST" : "Designates testing fabrics", "UAT" : "Designates user acceptance testing fabrics" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json index 95df1d2ce21d93..56562ff49ff8d9 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json @@ -576,7 +576,7 @@ "name" : "FabricType", "namespace" : "com.linkedin.common", "doc" : "Fabric group type", - "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW" ], + "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW", "SANDBOX" ], "symbolDocs" : { "CORP" : "Designates corporation fabrics", "DEV" : "Designates development fabrics", @@ -586,6 +586,7 @@ "PROD" : "Designates production fabrics", "QA" : "Designates quality assurance fabrics", "RVW" : "Designates review fabrics", + "SANDBOX" : "Designates sandbox fabrics", "STG" : "Designates staging fabrics", "TEST" : "Designates testing fabrics", "UAT" : "Designates user acceptance testing fabrics" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json index 226279e1762297..b90543745c65f4 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json @@ -834,7 +834,7 @@ "name" : "FabricType", "namespace" : "com.linkedin.common", "doc" : "Fabric group type", - "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW" ], + "symbols" : [ "DEV", "TEST", "QA", "UAT", "EI", "PRE", "STG", "NON_PROD", "PROD", "CORP", "RVW", "SANDBOX" ], "symbolDocs" : { "CORP" : "Designates corporation fabrics", "DEV" : "Designates development fabrics", @@ -844,6 +844,7 @@ "PROD" : "Designates production fabrics", "QA" : "Designates quality assurance fabrics", "RVW" : "Designates review fabrics", + "SANDBOX" : "Designates sandbox fabrics", "STG" : "Designates staging fabrics", "TEST" : "Designates testing fabrics", "UAT" : "Designates user acceptance testing fabrics" @@ -4536,11 +4537,7 @@ "name" : "description", "type" : "string", "doc" : "Documentation of the MLPrimaryKey", - "optional" : true, - "Searchable" : { - "fieldType" : "TEXT", - "hasValuesFieldName" : "hasDescription" - } + "optional" : true }, { "name" : "dataType", "type" : "com.linkedin.common.MLFeatureDataType", From e71918c56af02f2f8273ceb489a52bea4bde690d Mon Sep 17 00:00:00 2001 From: david-leifker <114954101+david-leifker@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:31:56 -0500 Subject: [PATCH 18/25] chore(bump): bump commons, misc versions (#11538) --- build.gradle | 10 +++++----- buildSrc/build.gradle | 2 +- metadata-service/factories/build.gradle | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 2166c76dbd6d0d..d7fbbb6682e041 100644 --- a/build.gradle +++ b/build.gradle @@ -117,9 +117,9 @@ project.ext.externalDependency = [ 'awsRds':'software.amazon.awssdk:rds:2.18.24', 'cacheApi': 'javax.cache:cache-api:1.1.0', 'commonsCli': 'commons-cli:commons-cli:1.5.0', - 'commonsIo': 'commons-io:commons-io:2.4', + 'commonsIo': 'commons-io:commons-io:2.17.0', 'commonsLang': 'commons-lang:commons-lang:2.6', - 'commonsText': 'org.apache.commons:commons-text:1.10.0', + 'commonsText': 'org.apache.commons:commons-text:1.12.0', 'commonsCollections': 'commons-collections:commons-collections:3.2.2', 'caffeine': 'com.github.ben-manes.caffeine:caffeine:3.1.8', 'datastaxOssNativeProtocol': 'com.datastax.oss:native-protocol:1.5.1', @@ -270,7 +270,6 @@ project.ext.externalDependency = [ 'zookeeper': 'org.apache.zookeeper:zookeeper:3.8.4', 'wire': 'com.squareup.wire:wire-compiler:3.7.1', 'charle': 'com.charleskorn.kaml:kaml:0.53.0', - 'common': 'commons-io:commons-io:2.7', 'jline':'jline:jline:1.4.1', 'jetbrains':' org.jetbrains.kotlin:kotlin-stdlib:1.6.0', 'annotationApi': 'javax.annotation:javax.annotation-api:1.3.2', @@ -393,11 +392,12 @@ subprojects { constraints { implementation("com.google.googlejavaformat:google-java-format:$googleJavaFormatVersion") implementation('io.netty:netty-all:4.1.114.Final') - implementation('org.apache.commons:commons-compress:1.26.0') - implementation('org.apache.velocity:velocity-engine-core:2.3') + implementation('org.apache.commons:commons-compress:1.27.1') + implementation('org.apache.velocity:velocity-engine-core:2.4') implementation('org.hibernate:hibernate-validator:6.0.20.Final') implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-dataformat-cbor:$jacksonVersion") + implementation(externalDependency.commonsIo) } } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 0101d1b717205a..9c0e44c41fb0fd 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -22,7 +22,7 @@ dependencies { implementation 'com.google.guava:guava:32.1.2-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.5' - implementation 'commons-io:commons-io:2.11.0' + implementation 'commons-io:commons-io:2.17.0' compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' diff --git a/metadata-service/factories/build.gradle b/metadata-service/factories/build.gradle index 76e4de70071f56..5e52efd245b7fa 100644 --- a/metadata-service/factories/build.gradle +++ b/metadata-service/factories/build.gradle @@ -48,7 +48,7 @@ dependencies { implementation spec.product.pegasus.restliSpringBridge implementation spec.product.pegasus.restliDocgen implementation externalDependency.jline - implementation externalDependency.common + implementation externalDependency.commonsIo testImplementation externalDependency.springBootTest testImplementation externalDependency.mockito From ba2f1d3147a59d3736c8dbb79278b1163a63bca3 Mon Sep 17 00:00:00 2001 From: RyanHolstien Date: Fri, 4 Oct 2024 16:32:08 -0500 Subject: [PATCH 19/25] feat(dataProduct): add data product unset side effect (#11512) Co-authored-by: david-leifker <114954101+david-leifker@users.noreply.github.com> --- .../DataProductPropertiesTemplate.java | 4 +- .../DataProductUnsetSideEffect.java | 134 +++++++++ .../DataProductUnsetSideEffectTest.java | 263 ++++++++++++++++++ .../src/main/resources/application.yaml | 2 + .../SpringStandardPluginConfiguration.java | 24 ++ .../metadata/service/DataProductService.java | 8 +- 6 files changed, 426 insertions(+), 9 deletions(-) create mode 100644 metadata-io/src/main/java/com/linkedin/metadata/dataproducts/sideeffects/DataProductUnsetSideEffect.java create mode 100644 metadata-io/src/test/java/com/linkedin/metadata/dataproducts/sideeffects/DataProductUnsetSideEffectTest.java diff --git a/entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/template/dataproduct/DataProductPropertiesTemplate.java b/entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/template/dataproduct/DataProductPropertiesTemplate.java index 9b117114395b12..f1b83732f7a9f0 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/template/dataproduct/DataProductPropertiesTemplate.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/template/dataproduct/DataProductPropertiesTemplate.java @@ -10,8 +10,8 @@ public class DataProductPropertiesTemplate implements ArrayMergingTemplate { - private static final String ASSETS_FIELD_NAME = "assets"; - private static final String KEY_FIELD_NAME = "destinationUrn"; + public static final String ASSETS_FIELD_NAME = "assets"; + public static final String KEY_FIELD_NAME = "destinationUrn"; @Override public DataProductProperties getSubtype(RecordTemplate recordTemplate) throws ClassCastException { diff --git a/metadata-io/src/main/java/com/linkedin/metadata/dataproducts/sideeffects/DataProductUnsetSideEffect.java b/metadata-io/src/main/java/com/linkedin/metadata/dataproducts/sideeffects/DataProductUnsetSideEffect.java new file mode 100644 index 00000000000000..544040d14f8b7c --- /dev/null +++ b/metadata-io/src/main/java/com/linkedin/metadata/dataproducts/sideeffects/DataProductUnsetSideEffect.java @@ -0,0 +1,134 @@ +package com.linkedin.metadata.dataproducts.sideeffects; + +import static com.linkedin.metadata.Constants.DATA_PRODUCT_ENTITY_NAME; +import static com.linkedin.metadata.Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME; +import static com.linkedin.metadata.search.utils.QueryUtils.EMPTY_FILTER; + +import com.google.common.collect.ImmutableList; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.dataproduct.DataProductAssociation; +import com.linkedin.dataproduct.DataProductAssociationArray; +import com.linkedin.dataproduct.DataProductProperties; +import com.linkedin.metadata.aspect.RetrieverContext; +import com.linkedin.metadata.aspect.batch.ChangeMCP; +import com.linkedin.metadata.aspect.batch.MCLItem; +import com.linkedin.metadata.aspect.batch.MCPItem; +import com.linkedin.metadata.aspect.models.graph.RelatedEntities; +import com.linkedin.metadata.aspect.models.graph.RelatedEntitiesScrollResult; +import com.linkedin.metadata.aspect.patch.GenericJsonPatch; +import com.linkedin.metadata.aspect.patch.PatchOperationType; +import com.linkedin.metadata.aspect.patch.template.dataproduct.DataProductPropertiesTemplate; +import com.linkedin.metadata.aspect.plugins.config.AspectPluginConfig; +import com.linkedin.metadata.aspect.plugins.hooks.MCPSideEffect; +import com.linkedin.metadata.entity.ebean.batch.PatchItemImpl; +import com.linkedin.metadata.models.EntitySpec; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import com.linkedin.metadata.search.utils.QueryUtils; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +/** + * Side effect that enforces single data product being associated with each entity by removing any + * previous relation when evaluation updates to Data Product Properties aspects. + */ +@Slf4j +@Getter +@Setter +@Accessors(chain = true) +public class DataProductUnsetSideEffect extends MCPSideEffect { + @Nonnull private AspectPluginConfig config; + + @Override + protected Stream applyMCPSideEffect( + Collection changeMCPS, @Nonnull RetrieverContext retrieverContext) { + return Stream.of(); + } + + @Override + protected Stream postMCPSideEffect( + Collection mclItems, @Nonnull RetrieverContext retrieverContext) { + return mclItems.stream().flatMap(item -> generatePatchRemove(item, retrieverContext)); + } + + private static Stream generatePatchRemove( + MCLItem mclItem, @Nonnull RetrieverContext retrieverContext) { + + if (DATA_PRODUCT_PROPERTIES_ASPECT_NAME.equals(mclItem.getAspectName())) { + List mcpItems = new ArrayList<>(); + DataProductProperties dataProductProperties = mclItem.getAspect(DataProductProperties.class); + if (dataProductProperties == null) { + log.error("Unable to process data product properties for urn: {}", mclItem.getUrn()); + return Stream.empty(); + } + for (DataProductAssociation dataProductAssociation : + Optional.ofNullable(dataProductProperties.getAssets()) + .orElse(new DataProductAssociationArray())) { + RelatedEntitiesScrollResult result = + retrieverContext + .getGraphRetriever() + .scrollRelatedEntities( + null, + QueryUtils.newFilter( + "urn", dataProductAssociation.getDestinationUrn().toString()), + null, + EMPTY_FILTER, + ImmutableList.of("DataProductContains"), + QueryUtils.newRelationshipFilter(EMPTY_FILTER, RelationshipDirection.INCOMING), + Collections.emptyList(), + null, + 10, // Should only ever be one, if ever greater than ten will decrease over time + // to become consistent + null, + null); + if (!result.getEntities().isEmpty()) { + for (RelatedEntities entity : result.getEntities()) { + if (!mclItem.getUrn().equals(UrnUtils.getUrn(entity.getSourceUrn()))) { + EntitySpec entitySpec = + retrieverContext + .getAspectRetriever() + .getEntityRegistry() + .getEntitySpec(DATA_PRODUCT_ENTITY_NAME); + GenericJsonPatch.PatchOp patchOp = new GenericJsonPatch.PatchOp(); + patchOp.setOp(PatchOperationType.REMOVE.getValue()); + patchOp.setPath(String.format("/assets/%s", entity.getDestinationUrn())); + mcpItems.add( + PatchItemImpl.builder() + .urn(UrnUtils.getUrn(entity.getSourceUrn())) + .entitySpec( + retrieverContext + .getAspectRetriever() + .getEntityRegistry() + .getEntitySpec(DATA_PRODUCT_ENTITY_NAME)) + .aspectName(DATA_PRODUCT_PROPERTIES_ASPECT_NAME) + .aspectSpec(entitySpec.getAspectSpec(DATA_PRODUCT_PROPERTIES_ASPECT_NAME)) + .patch( + GenericJsonPatch.builder() + .arrayPrimaryKeys( + Map.of( + DataProductPropertiesTemplate.ASSETS_FIELD_NAME, + List.of(DataProductPropertiesTemplate.KEY_FIELD_NAME))) + .patch(List.of(patchOp)) + .build() + .getJsonPatch()) + .auditStamp(mclItem.getAuditStamp()) + .systemMetadata(mclItem.getSystemMetadata()) + .build(retrieverContext.getAspectRetriever().getEntityRegistry())); + } + } + } + } + return mcpItems.stream(); + } + return Stream.empty(); + } +} diff --git a/metadata-io/src/test/java/com/linkedin/metadata/dataproducts/sideeffects/DataProductUnsetSideEffectTest.java b/metadata-io/src/test/java/com/linkedin/metadata/dataproducts/sideeffects/DataProductUnsetSideEffectTest.java new file mode 100644 index 00000000000000..1151014bf1162f --- /dev/null +++ b/metadata-io/src/test/java/com/linkedin/metadata/dataproducts/sideeffects/DataProductUnsetSideEffectTest.java @@ -0,0 +1,263 @@ +package com.linkedin.metadata.dataproducts.sideeffects; + +import static com.linkedin.metadata.Constants.DATA_PRODUCT_ENTITY_NAME; +import static com.linkedin.metadata.Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME; +import static com.linkedin.metadata.search.utils.QueryUtils.EMPTY_FILTER; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.dataproduct.DataProductAssociation; +import com.linkedin.dataproduct.DataProductAssociationArray; +import com.linkedin.dataproduct.DataProductProperties; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.metadata.aspect.AspectRetriever; +import com.linkedin.metadata.aspect.GraphRetriever; +import com.linkedin.metadata.aspect.batch.MCPItem; +import com.linkedin.metadata.aspect.models.graph.RelatedEntities; +import com.linkedin.metadata.aspect.models.graph.RelatedEntitiesScrollResult; +import com.linkedin.metadata.aspect.patch.GenericJsonPatch; +import com.linkedin.metadata.aspect.patch.PatchOperationType; +import com.linkedin.metadata.aspect.patch.template.dataproduct.DataProductPropertiesTemplate; +import com.linkedin.metadata.aspect.plugins.config.AspectPluginConfig; +import com.linkedin.metadata.entity.SearchRetriever; +import com.linkedin.metadata.entity.ebean.batch.ChangeItemImpl; +import com.linkedin.metadata.entity.ebean.batch.MCLItemImpl; +import com.linkedin.metadata.entity.ebean.batch.PatchItemImpl; +import com.linkedin.metadata.models.registry.EntityRegistry; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import com.linkedin.metadata.search.utils.QueryUtils; +import com.linkedin.metadata.utils.AuditStampUtils; +import com.linkedin.test.metadata.aspect.TestEntityRegistry; +import io.datahubproject.metadata.context.RetrieverContext; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class DataProductUnsetSideEffectTest { + private static final EntityRegistry TEST_REGISTRY = new TestEntityRegistry(); + private static final List SUPPORTED_CHANGE_TYPES = + List.of( + ChangeType.CREATE, + ChangeType.PATCH, + ChangeType.CREATE_ENTITY, + ChangeType.UPSERT, + ChangeType.DELETE, + ChangeType.RESTATE); + private static final Urn TEST_PRODUCT_URN = + UrnUtils.getUrn("urn:li:dataProduct:someDataProductId"); + + private static final Urn TEST_PRODUCT_URN_2 = + UrnUtils.getUrn("urn:li:dataProduct:someOtherDataProductId"); + + private static final Urn DATASET_URN_1 = + UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD)"); + private static final Urn DATASET_URN_2 = + UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD)"); + private static final AspectPluginConfig TEST_PLUGIN_CONFIG = + AspectPluginConfig.builder() + .className(DataProductUnsetSideEffect.class.getName()) + .enabled(true) + .supportedOperations( + SUPPORTED_CHANGE_TYPES.stream() + .map(ChangeType::toString) + .collect(Collectors.toList())) + .supportedEntityAspectNames( + List.of( + AspectPluginConfig.EntityAspectName.builder() + .entityName(DATA_PRODUCT_ENTITY_NAME) + .aspectName(DATA_PRODUCT_PROPERTIES_ASPECT_NAME) + .build())) + .build(); + + private AspectRetriever mockAspectRetriever; + private RetrieverContext retrieverContext; + + @BeforeMethod + public void setup() { + mockAspectRetriever = mock(AspectRetriever.class); + when(mockAspectRetriever.getEntityRegistry()).thenReturn(TEST_REGISTRY); + GraphRetriever graphRetriever = mock(GraphRetriever.class); + RelatedEntities relatedEntities = + new RelatedEntities( + "DataProductContains", + TEST_PRODUCT_URN.toString(), + DATASET_URN_1.toString(), + RelationshipDirection.INCOMING, + null); + + List relatedEntitiesList = new ArrayList<>(); + relatedEntitiesList.add(relatedEntities); + RelatedEntitiesScrollResult relatedEntitiesScrollResult = + new RelatedEntitiesScrollResult(1, 10, null, relatedEntitiesList); + when(graphRetriever.scrollRelatedEntities( + eq(null), + eq(QueryUtils.newFilter("urn", DATASET_URN_1.toString())), + eq(null), + eq(EMPTY_FILTER), + eq(ImmutableList.of("DataProductContains")), + eq(QueryUtils.newRelationshipFilter(EMPTY_FILTER, RelationshipDirection.INCOMING)), + eq(Collections.emptyList()), + eq(null), + eq(10), // Should only ever be one, if ever greater than ten will decrease over time to + // become consistent + eq(null), + eq(null))) + .thenReturn(relatedEntitiesScrollResult); + + RelatedEntities relatedEntities2 = + new RelatedEntities( + "DataProductContains", + TEST_PRODUCT_URN_2.toString(), + DATASET_URN_2.toString(), + RelationshipDirection.INCOMING, + null); + + List relatedEntitiesList2 = new ArrayList<>(); + relatedEntitiesList2.add(relatedEntities2); + RelatedEntitiesScrollResult relatedEntitiesScrollResult2 = + new RelatedEntitiesScrollResult(1, 10, null, relatedEntitiesList2); + when(graphRetriever.scrollRelatedEntities( + eq(null), + eq(QueryUtils.newFilter("urn", DATASET_URN_2.toString())), + eq(null), + eq(EMPTY_FILTER), + eq(ImmutableList.of("DataProductContains")), + eq(QueryUtils.newRelationshipFilter(EMPTY_FILTER, RelationshipDirection.INCOMING)), + eq(Collections.emptyList()), + eq(null), + eq(10), // Should only ever be one, if ever greater than ten will decrease over time to + // become consistent + eq(null), + eq(null))) + .thenReturn(relatedEntitiesScrollResult2); + retrieverContext = + RetrieverContext.builder() + .searchRetriever(mock(SearchRetriever.class)) + .aspectRetriever(mockAspectRetriever) + .graphRetriever(graphRetriever) + .build(); + } + + @Test + public void testDPAlreadySetToSame() { + DataProductUnsetSideEffect test = new DataProductUnsetSideEffect(); + test.setConfig(TEST_PLUGIN_CONFIG); + + DataProductProperties dataProductProperties = getTestDataProductProperties(DATASET_URN_1); + + List testOutput; + // Run test + ChangeItemImpl dataProductPropertiesChangeItem = + ChangeItemImpl.builder() + .urn(TEST_PRODUCT_URN) + .aspectName(DATA_PRODUCT_PROPERTIES_ASPECT_NAME) + .changeType(ChangeType.UPSERT) + .entitySpec(TEST_REGISTRY.getEntitySpec(DATA_PRODUCT_ENTITY_NAME)) + .aspectSpec( + TEST_REGISTRY + .getEntitySpec(DATA_PRODUCT_ENTITY_NAME) + .getAspectSpec(DATA_PRODUCT_PROPERTIES_ASPECT_NAME)) + .recordTemplate(dataProductProperties) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .build(mockAspectRetriever); + testOutput = + test.postMCPSideEffect( + List.of( + MCLItemImpl.builder() + .build( + dataProductPropertiesChangeItem, + null, + null, + retrieverContext.getAspectRetriever())), + retrieverContext) + .toList(); + + // Verify test + assertEquals(testOutput.size(), 0, "Expected no additional changes: " + testOutput); + } + + @Test + public void testDPRemoveOld() { + DataProductUnsetSideEffect test = new DataProductUnsetSideEffect(); + test.setConfig(TEST_PLUGIN_CONFIG); + + DataProductProperties dataProductProperties = getTestDataProductProperties(DATASET_URN_2); + + List testOutput; + // Run test + ChangeItemImpl dataProductPropertiesChangeItem = + ChangeItemImpl.builder() + .urn(TEST_PRODUCT_URN) + .aspectName(DATA_PRODUCT_PROPERTIES_ASPECT_NAME) + .changeType(ChangeType.UPSERT) + .entitySpec(TEST_REGISTRY.getEntitySpec(DATA_PRODUCT_ENTITY_NAME)) + .aspectSpec( + TEST_REGISTRY + .getEntitySpec(DATA_PRODUCT_ENTITY_NAME) + .getAspectSpec(DATA_PRODUCT_PROPERTIES_ASPECT_NAME)) + .recordTemplate(dataProductProperties) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .build(mockAspectRetriever); + testOutput = + test.postMCPSideEffect( + List.of( + MCLItemImpl.builder() + .build( + dataProductPropertiesChangeItem, + null, + null, + retrieverContext.getAspectRetriever())), + retrieverContext) + .toList(); + + // Verify test + assertEquals(testOutput.size(), 1, "Expected removal of previous data product: " + testOutput); + + GenericJsonPatch.PatchOp patchOp = new GenericJsonPatch.PatchOp(); + patchOp.setOp(PatchOperationType.REMOVE.getValue()); + patchOp.setPath(String.format("/assets/%s", DATASET_URN_2)); + + assertEquals( + testOutput, + List.of( + PatchItemImpl.builder() + .urn(TEST_PRODUCT_URN_2) + .aspectName(DATA_PRODUCT_PROPERTIES_ASPECT_NAME) + .patch( + GenericJsonPatch.builder() + .arrayPrimaryKeys( + Map.of( + DataProductPropertiesTemplate.ASSETS_FIELD_NAME, + List.of(DataProductPropertiesTemplate.KEY_FIELD_NAME))) + .patch(List.of(patchOp)) + .build() + .getJsonPatch()) + .entitySpec(TEST_REGISTRY.getEntitySpec(DATA_PRODUCT_ENTITY_NAME)) + .aspectSpec( + TEST_REGISTRY + .getEntitySpec(DATA_PRODUCT_ENTITY_NAME) + .getAspectSpec(DATA_PRODUCT_PROPERTIES_ASPECT_NAME)) + .auditStamp(dataProductPropertiesChangeItem.getAuditStamp()) + .systemMetadata(dataProductPropertiesChangeItem.getSystemMetadata()) + .build(mockAspectRetriever.getEntityRegistry()))); + } + + private static DataProductProperties getTestDataProductProperties(Urn destinationUrn) { + DataProductProperties dataProductProperties = new DataProductProperties(); + DataProductAssociationArray dataProductAssociations = new DataProductAssociationArray(); + DataProductAssociation dataProductAssociation1 = new DataProductAssociation(); + dataProductAssociation1.setDestinationUrn(destinationUrn); + dataProductAssociations.add(dataProductAssociation1); + dataProductProperties.setAssets(dataProductAssociations); + return dataProductProperties; + } +} diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml index 24764b512c232b..ef3ae76d81fae3 100644 --- a/metadata-service/configuration/src/main/resources/application.yaml +++ b/metadata-service/configuration/src/main/resources/application.yaml @@ -542,6 +542,8 @@ metadataChangeProposal: sideEffects: schemaField: enabled: ${MCP_SIDE_EFFECTS_SCHEMA_FIELD_ENABLED:false} + dataProductUnset: + enabled: ${MCP_SIDE_EFFECTS_DATA_PRODUCT_UNSET_ENABLED:true} throttle: updateIntervalMs: ${MCP_THROTTLE_UPDATE_INTERVAL_MS:60000} diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/plugins/SpringStandardPluginConfiguration.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/plugins/SpringStandardPluginConfiguration.java index 67fe2dd6d605de..4a2095685abe1f 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/plugins/SpringStandardPluginConfiguration.java +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/plugins/SpringStandardPluginConfiguration.java @@ -7,6 +7,7 @@ import com.linkedin.metadata.aspect.plugins.config.AspectPluginConfig; import com.linkedin.metadata.aspect.plugins.hooks.MCPSideEffect; import com.linkedin.metadata.aspect.plugins.hooks.MutationHook; +import com.linkedin.metadata.dataproducts.sideeffects.DataProductUnsetSideEffect; import com.linkedin.metadata.schemafields.sideeffects.SchemaFieldSideEffect; import com.linkedin.metadata.timeline.eventgenerator.EntityChangeEventGeneratorRegistry; import com.linkedin.metadata.timeline.eventgenerator.SchemaMetadataChangeEventGenerator; @@ -80,4 +81,27 @@ public MCPSideEffect schemaFieldSideEffect() { .setConfig(config) .setEntityChangeEventGeneratorRegistry(entityChangeEventGeneratorRegistry); } + + @Bean + @ConditionalOnProperty( + name = "metadataChangeProposal.sideEffects.dataProductUnset.enabled", + havingValue = "true") + public MCPSideEffect dataProductUnsetSideEffect() { + AspectPluginConfig config = + AspectPluginConfig.builder() + .enabled(true) + .className(DataProductUnsetSideEffect.class.getName()) + .supportedOperations( + List.of("CREATE", "CREATE_ENTITY", "UPSERT", "RESTATE", "DELETE", "PATCH")) + .supportedEntityAspectNames( + List.of( + AspectPluginConfig.EntityAspectName.builder() + .entityName(Constants.DATA_PRODUCT_ENTITY_NAME) + .aspectName(Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME) + .build())) + .build(); + + log.info("Initialized {}", SchemaFieldSideEffect.class.getName()); + return new DataProductUnsetSideEffect().setConfig(config); + } } diff --git a/metadata-service/services/src/main/java/com/linkedin/metadata/service/DataProductService.java b/metadata-service/services/src/main/java/com/linkedin/metadata/service/DataProductService.java index 3abd663832f4b1..f222c31d0876d8 100644 --- a/metadata-service/services/src/main/java/com/linkedin/metadata/service/DataProductService.java +++ b/metadata-service/services/src/main/java/com/linkedin/metadata/service/DataProductService.java @@ -340,12 +340,6 @@ public void batchSetDataProduct( .filter(urn -> !existingResourceUrns.contains(urn)) .collect(Collectors.toList()); - // unset existing data product on resources first as we only allow one data product on an - // entity at a time - for (Urn resourceUrn : resourceUrns) { - unsetDataProduct(opContext, resourceUrn, actorUrn); - } - AuditStamp nowAuditStamp = new AuditStamp().setTime(System.currentTimeMillis()).setActor(actorUrn); for (Urn resourceUrn : newResourceUrns) { @@ -390,7 +384,7 @@ public void unsetDataProduct( 10, // should never be more than 1 as long as we only allow one actorUrn.toString()); - if (relationships.hasRelationships() && relationships.getRelationships().size() > 0) { + if (relationships.hasRelationships() && !relationships.getRelationships().isEmpty()) { relationships .getRelationships() .forEach( From 09e9d83f26022ab74ed69c59629afe44059ad1f5 Mon Sep 17 00:00:00 2001 From: david-leifker <114954101+david-leifker@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:32:19 -0500 Subject: [PATCH 20/25] fix(search): Elasticsearch bool query should (#11536) --- .../graph/elastic/ESGraphQueryDAO.java | 22 +++++++++++++++++-- .../graph/elastic/GraphFilterUtils.java | 11 ++++++---- .../query/filter/BaseQueryFilterRewriter.java | 5 ++++- .../request/AutocompleteRequestHandler.java | 13 ++++++----- .../query/request/SearchQueryBuilder.java | 15 ++++++++++--- .../metadata/search/utils/ESUtils.java | 9 ++++---- .../search/fixtures/GoldenTestBase.java | 1 - .../ContainerExpansionRewriterTest.java | 11 +++++++--- .../filter/DomainExpansionRewriterTest.java | 20 ++++++++++++----- .../metadata/search/utils/ESUtilsTest.java | 8 +++++++ .../test/fixtures/search/LineageExporter.java | 8 ++++--- .../lineage_query_filters_full.json | 6 +++++ ...eage_query_filters_full_empty_filters.json | 2 ++ ...e_query_filters_full_multiple_filters.json | 6 +++++ .../lineage_query_filters_limited.json | 1 + .../lineage_time_query_filters_1.json | 4 ++++ 16 files changed, 109 insertions(+), 33 deletions(-) diff --git a/metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/ESGraphQueryDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/ESGraphQueryDAO.java index 8c7f0e3256cf82..a801cab81c952f 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/ESGraphQueryDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/ESGraphQueryDAO.java @@ -128,6 +128,9 @@ private static void addFilterToQueryBuilder( criterion.getValues()))); orQuery.should(andQuery); } + if (!orQuery.should().isEmpty()) { + orQuery.minimumShouldMatch(1); + } rootQuery.filter(orQuery); } @@ -177,21 +180,26 @@ private SearchResponse executeGroupByLineageSearchQuery( // directions for lineage // set up filters for each relationship type in the correct direction to limit buckets BoolQueryBuilder sourceFilterQuery = QueryBuilders.boolQuery(); - sourceFilterQuery.minimumShouldMatch(1); + validEdges.stream() .filter(pair -> RelationshipDirection.OUTGOING.equals(pair.getValue().getDirection())) .forEach( pair -> sourceFilterQuery.should( getAggregationFilter(pair, RelationshipDirection.OUTGOING))); + if (!sourceFilterQuery.should().isEmpty()) { + sourceFilterQuery.minimumShouldMatch(1); + } BoolQueryBuilder destFilterQuery = QueryBuilders.boolQuery(); - destFilterQuery.minimumShouldMatch(1); validEdges.stream() .filter(pair -> RelationshipDirection.INCOMING.equals(pair.getValue().getDirection())) .forEach( pair -> destFilterQuery.should(getAggregationFilter(pair, RelationshipDirection.INCOMING))); + if (!destFilterQuery.should().isEmpty()) { + destFilterQuery.minimumShouldMatch(1); + } FilterAggregationBuilder sourceRelationshipTypeFilters = AggregationBuilders.filter(FILTER_BY_SOURCE_RELATIONSHIP, sourceFilterQuery); @@ -347,6 +355,9 @@ public static BoolQueryBuilder buildQuery( relationshipType -> relationshipQuery.should( QueryBuilders.termQuery(RELATIONSHIP_TYPE, relationshipType))); + if (!relationshipQuery.should().isEmpty()) { + relationshipQuery.minimumShouldMatch(1); + } finalQuery.filter(relationshipQuery); } @@ -697,6 +708,9 @@ public static QueryBuilder getLineageQuery( urns, edgesPerEntityType.get(entityType), graphFilters)); } }); + if (!entityTypeQueries.should().isEmpty()) { + entityTypeQueries.minimumShouldMatch(1); + } BoolQueryBuilder finalQuery = QueryBuilders.boolQuery(); @@ -741,6 +755,10 @@ static QueryBuilder getLineageQueryForEntityType( query.should(getIncomingEdgeQuery(urns, incomingEdges, graphFilters)); } + if (!query.should().isEmpty()) { + query.minimumShouldMatch(1); + } + return query; } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/GraphFilterUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/GraphFilterUtils.java index 982bcae9b5fd96..b57b5b0b4b5eb2 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/GraphFilterUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/GraphFilterUtils.java @@ -37,11 +37,14 @@ public static QueryBuilder getUrnStatusQuery( if (removed) { finalQuery.filter(QueryBuilders.termQuery(statusField, removed.toString())); } else { - finalQuery.minimumShouldMatch(1); finalQuery.should(QueryBuilders.termQuery(statusField, removed.toString())); finalQuery.should(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(statusField))); } + if (!finalQuery.should().isEmpty()) { + finalQuery.minimumShouldMatch(1); + } + return finalQuery; } @@ -102,7 +105,7 @@ public static QueryBuilder getEdgeTimeFilterQuery( * 2. The createdOn and updatedOn window does not exist on the edge at all (support legacy cases) * 3. Special lineage case: The edge is marked as a "manual" edge, meaning that the time filters should NOT be applied. */ - BoolQueryBuilder timeFilterQuery = QueryBuilders.boolQuery(); + BoolQueryBuilder timeFilterQuery = QueryBuilders.boolQuery().minimumShouldMatch(1); timeFilterQuery.should(buildTimeWindowFilter(startTimeMillis, endTimeMillis)); timeFilterQuery.should(buildTimestampsMissingFilter()); timeFilterQuery.should(buildManualLineageFilter()); @@ -158,7 +161,7 @@ public static QueryBuilder getEdgeTimeFilterQuery( */ private static QueryBuilder buildTimeWindowFilter( final long startTimeMillis, final long endTimeMillis) { - final BoolQueryBuilder timeWindowQuery = QueryBuilders.boolQuery(); + final BoolQueryBuilder timeWindowQuery = QueryBuilders.boolQuery().minimumShouldMatch(1); /* * To perform comparison: @@ -198,7 +201,7 @@ private static QueryBuilder buildTimestampsMissingFilter() { private static QueryBuilder buildNotExistsFilter(String fieldName) { // This filter returns 'true' if the field DOES NOT EXIST or it exists but is equal to 0. - final BoolQueryBuilder notExistsFilter = QueryBuilders.boolQuery(); + final BoolQueryBuilder notExistsFilter = QueryBuilders.boolQuery().minimumShouldMatch(1); notExistsFilter.should(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(fieldName))); notExistsFilter.should(QueryBuilders.boolQuery().must(QueryBuilders.termQuery(fieldName, 0L))); return notExistsFilter; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/filter/BaseQueryFilterRewriter.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/filter/BaseQueryFilterRewriter.java index 367705d369c7ce..d545f60a1ee8fa 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/filter/BaseQueryFilterRewriter.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/filter/BaseQueryFilterRewriter.java @@ -110,9 +110,12 @@ private BoolQueryBuilder handleNestedFilters( mustNotQueryBuilders.forEach(expandedQueryBuilder::mustNot); expandedQueryBuilder.queryName(boolQueryBuilder.queryName()); expandedQueryBuilder.adjustPureNegative(boolQueryBuilder.adjustPureNegative()); - expandedQueryBuilder.minimumShouldMatch(boolQueryBuilder.minimumShouldMatch()); expandedQueryBuilder.boost(boolQueryBuilder.boost()); + if (!expandedQueryBuilder.should().isEmpty()) { + expandedQueryBuilder.minimumShouldMatch(1); + } + return expandedQueryBuilder; } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java index b7a04f2064d9b4..294efb069a9046 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java @@ -115,8 +115,7 @@ public SearchRequest getSearchRequest( QueryConfiguration customQueryConfig = customizedQueryHandler.lookupQueryConfig(input).orElse(null); - BoolQueryBuilder baseQuery = QueryBuilders.boolQuery(); - baseQuery.minimumShouldMatch(1); + BoolQueryBuilder baseQuery = QueryBuilders.boolQuery().minimumShouldMatch(1); // Initial query with input filters BoolQueryBuilder filterQuery = @@ -176,12 +175,15 @@ public BoolQueryBuilder getQuery( BoolQueryBuilder finalQuery = Optional.ofNullable(customAutocompleteConfig) .flatMap(cac -> CustomizedQueryHandler.boolQueryBuilder(objectMapper, cac, query)) - .orElse(QueryBuilders.boolQuery()) - .minimumShouldMatch(1); + .orElse(QueryBuilders.boolQuery()); getAutocompleteQuery(customAutocompleteConfig, autocompleteFields, query) .ifPresent(finalQuery::should); + if (!finalQuery.should().isEmpty()) { + finalQuery.minimumShouldMatch(1); + } + return finalQuery; } @@ -200,8 +202,7 @@ private Optional getAutocompleteQuery( private static BoolQueryBuilder defaultQuery( List autocompleteFields, @Nonnull String query) { - BoolQueryBuilder finalQuery = QueryBuilders.boolQuery(); - finalQuery.minimumShouldMatch(1); + BoolQueryBuilder finalQuery = QueryBuilders.boolQuery().minimumShouldMatch(1); // Search for exact matches with higher boost and ngram matches MultiMatchQueryBuilder autocompleteQueryBuilder = diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java index 3e76d3600d6a68..529c13c7d71ef1 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java @@ -103,8 +103,7 @@ private QueryBuilder buildInternalQuery( cqc -> CustomizedQueryHandler.boolQueryBuilder( opContext.getObjectMapper(), cqc, sanitizedQuery)) - .orElse(QueryBuilders.boolQuery()) - .minimumShouldMatch(1); + .orElse(QueryBuilders.boolQuery()); if (fulltext && !query.startsWith(STRUCTURED_QUERY_PREFIX)) { getSimpleQuery(opContext.getEntityRegistry(), customQueryConfig, entitySpecs, sanitizedQuery) @@ -135,6 +134,10 @@ private QueryBuilder buildInternalQuery( } } + if (!finalQuery.should().isEmpty()) { + finalQuery.minimumShouldMatch(1); + } + return finalQuery; } @@ -368,6 +371,10 @@ private Optional getSimpleQuery( simplePerField.should(simpleBuilder); }); + if (!simplePerField.should().isEmpty()) { + simplePerField.minimumShouldMatch(1); + } + result = Optional.of(simplePerField); } @@ -454,7 +461,9 @@ private Optional getPrefixAndExactMatchQuery( } }); - return finalQuery.should().size() > 0 ? Optional.of(finalQuery) : Optional.empty(); + return finalQuery.should().size() > 0 + ? Optional.of(finalQuery.minimumShouldMatch(1)) + : Optional.empty(); } private Optional getStructuredQuery( diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java index 9f48727aeca780..e135f1941bfec9 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java @@ -166,8 +166,6 @@ public static BoolQueryBuilder buildFilterQuery( searchableFieldTypes, opContext, queryFilterRewriteChain))); - // The default is not always 1 (ensure consistent default) - finalQueryBuilder.minimumShouldMatch(1); } else if (filter.getCriteria() != null) { // Otherwise, build boolean query from the deprecated "criteria" field. log.warn("Received query Filter with a deprecated field 'criteria'. Use 'or' instead."); @@ -187,7 +185,8 @@ public static BoolQueryBuilder buildFilterQuery( } }); finalQueryBuilder.should(andQueryBuilder); - // The default is not always 1 (ensure consistent default) + } + if (!finalQueryBuilder.should().isEmpty()) { finalQueryBuilder.minimumShouldMatch(1); } return finalQueryBuilder; @@ -533,7 +532,7 @@ private static QueryBuilder getQueryBuilderFromCriterionForFieldToExpand( final Map> searchableFieldTypes, @Nonnull OperationContext opContext, @Nonnull QueryFilterRewriteChain queryFilterRewriteChain) { - final BoolQueryBuilder orQueryBuilder = new BoolQueryBuilder(); + final BoolQueryBuilder orQueryBuilder = new BoolQueryBuilder().minimumShouldMatch(1); for (String field : fields) { orQueryBuilder.should( getQueryBuilderFromCriterionForSingleField( @@ -636,7 +635,7 @@ private static QueryBuilder buildWildcardQueryWithMultipleValues( @Nullable String queryName, @Nonnull AspectRetriever aspectRetriever, String wildcardPattern) { - BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().minimumShouldMatch(1); for (String value : criterion.getValues()) { boolQuery.should( diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/GoldenTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/GoldenTestBase.java index 1ebcc03eb690bc..052daeece8cd0b 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/GoldenTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/GoldenTestBase.java @@ -4,7 +4,6 @@ import static com.linkedin.metadata.utils.CriterionUtils.buildCriterion; import static io.datahubproject.test.search.SearchTestUtils.searchAcrossEntities; import static org.testng.Assert.*; -import static org.testng.AssertJUnit.assertNotNull; import com.google.common.collect.ImmutableList; import com.linkedin.common.urn.Urn; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/filter/ContainerExpansionRewriterTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/filter/ContainerExpansionRewriterTest.java index f91e3a28f1bd69..5246e4dbe5bf92 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/filter/ContainerExpansionRewriterTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/filter/ContainerExpansionRewriterTest.java @@ -311,7 +311,7 @@ public void testNestedBoolQueryRewrite() { new RelatedEntities( "IsPartOf", childUrn, parentUrn, RelationshipDirection.OUTGOING, null)))); - BoolQueryBuilder testQuery = QueryBuilders.boolQuery(); + BoolQueryBuilder testQuery = QueryBuilders.boolQuery().minimumShouldMatch(1); testQuery.filter( QueryBuilders.boolQuery() .filter( @@ -319,8 +319,11 @@ public void testNestedBoolQueryRewrite() { testQuery.filter(QueryBuilders.existsQuery("someField")); testQuery.should( QueryBuilders.boolQuery() + .minimumShouldMatch(1) .should( - QueryBuilders.boolQuery().should(QueryBuilders.termsQuery(FIELD_NAME, childUrn)))); + QueryBuilders.boolQuery() + .minimumShouldMatch(1) + .should(QueryBuilders.termsQuery(FIELD_NAME, childUrn)))); testQuery.should(QueryBuilders.existsQuery("someField")); testQuery.must( QueryBuilders.boolQuery() @@ -332,7 +335,7 @@ public void testNestedBoolQueryRewrite() { QueryBuilders.boolQuery().mustNot(QueryBuilders.termsQuery(FIELD_NAME, childUrn)))); testQuery.mustNot(QueryBuilders.existsQuery("someField")); - BoolQueryBuilder expectedRewrite = QueryBuilders.boolQuery(); + BoolQueryBuilder expectedRewrite = QueryBuilders.boolQuery().minimumShouldMatch(1); expectedRewrite.filter( QueryBuilders.boolQuery() .filter( @@ -341,8 +344,10 @@ public void testNestedBoolQueryRewrite() { expectedRewrite.filter(QueryBuilders.existsQuery("someField")); expectedRewrite.should( QueryBuilders.boolQuery() + .minimumShouldMatch(1) .should( QueryBuilders.boolQuery() + .minimumShouldMatch(1) .should(QueryBuilders.termsQuery(FIELD_NAME, childUrn, parentUrn)))); expectedRewrite.should(QueryBuilders.existsQuery("someField")); expectedRewrite.must( diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/filter/DomainExpansionRewriterTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/filter/DomainExpansionRewriterTest.java index 76e650f4054566..edc6449438581f 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/filter/DomainExpansionRewriterTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/filter/DomainExpansionRewriterTest.java @@ -312,7 +312,7 @@ public void testNestedBoolQueryRewrite() { new RelatedEntities( "IsPartOf", childUrn, parentUrn, RelationshipDirection.INCOMING, null)))); - BoolQueryBuilder testQuery = QueryBuilders.boolQuery(); + BoolQueryBuilder testQuery = QueryBuilders.boolQuery().minimumShouldMatch(1); testQuery.filter( QueryBuilders.boolQuery() .filter( @@ -320,9 +320,15 @@ public void testNestedBoolQueryRewrite() { testQuery.filter(QueryBuilders.boolQuery().filter(QueryBuilders.existsQuery("someField"))); testQuery.should( QueryBuilders.boolQuery() + .minimumShouldMatch(1) .should( - QueryBuilders.boolQuery().should(QueryBuilders.termsQuery(FIELD_NAME, parentUrn)))); - testQuery.should(QueryBuilders.boolQuery().should(QueryBuilders.existsQuery("someField"))); + QueryBuilders.boolQuery() + .minimumShouldMatch(1) + .should(QueryBuilders.termsQuery(FIELD_NAME, parentUrn)))); + testQuery.should( + QueryBuilders.boolQuery() + .minimumShouldMatch(1) + .should(QueryBuilders.existsQuery("someField"))); testQuery.must( QueryBuilders.boolQuery() .must(QueryBuilders.boolQuery().must(QueryBuilders.termsQuery(FIELD_NAME, parentUrn)))); @@ -334,7 +340,7 @@ public void testNestedBoolQueryRewrite() { .mustNot(QueryBuilders.termsQuery(FIELD_NAME, parentUrn)))); testQuery.mustNot(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("someField"))); - BoolQueryBuilder expectedRewrite = QueryBuilders.boolQuery(); + BoolQueryBuilder expectedRewrite = QueryBuilders.boolQuery().minimumShouldMatch(1); expectedRewrite.filter( QueryBuilders.boolQuery() .filter( @@ -344,11 +350,15 @@ public void testNestedBoolQueryRewrite() { QueryBuilders.boolQuery().filter(QueryBuilders.existsQuery("someField"))); expectedRewrite.should( QueryBuilders.boolQuery() + .minimumShouldMatch(1) .should( QueryBuilders.boolQuery() + .minimumShouldMatch(1) .should(QueryBuilders.termsQuery(FIELD_NAME, childUrn, parentUrn)))); expectedRewrite.should( - QueryBuilders.boolQuery().should(QueryBuilders.existsQuery("someField"))); + QueryBuilders.boolQuery() + .minimumShouldMatch(1) + .should(QueryBuilders.existsQuery("someField"))); expectedRewrite.must( QueryBuilders.boolQuery() .must( diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java index 928818f8c15eb7..8d06594e415e08 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java @@ -259,6 +259,7 @@ public void testGetQueryBuilderFromCriterionContain() { + " }\n" + " ],\n" + " \"adjust_pure_negative\" : true,\n" + + " \"minimum_should_match\" : \"1\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; @@ -302,6 +303,7 @@ public void testGetQueryBuilderFromCriterionContain() { + " }\n" + " ],\n" + " \"adjust_pure_negative\" : true,\n" + + " \"minimum_should_match\" : \"1\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; @@ -338,6 +340,7 @@ public void testWildcardQueryBuilderFromCriterionWhenStartsWith() { + " }\n" + " ],\n" + " \"adjust_pure_negative\" : true,\n" + + " \"minimum_should_match\" : \"1\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; @@ -381,6 +384,7 @@ public void testWildcardQueryBuilderFromCriterionWhenStartsWith() { + " }\n" + " ],\n" + " \"adjust_pure_negative\" : true,\n" + + " \"minimum_should_match\" : \"1\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; @@ -417,6 +421,7 @@ public void testWildcardQueryBuilderFromCriterionWhenEndsWith() { + " }\n" + " ],\n" + " \"adjust_pure_negative\" : true,\n" + + " \"minimum_should_match\" : \"1\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; @@ -459,6 +464,7 @@ public void testWildcardQueryBuilderFromCriterionWhenEndsWith() { + " }\n" + " ],\n" + " \"adjust_pure_negative\" : true,\n" + + " \"minimum_should_match\" : \"1\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; @@ -620,6 +626,7 @@ public void testGetQueryBuilderFromCriterionFieldToExpand() { + " }\n" + " ],\n" + " \"adjust_pure_negative\" : true,\n" + + " \"minimum_should_match\" : \"1\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; @@ -662,6 +669,7 @@ public void testGetQueryBuilderFromCriterionFieldToExpand() { + " }\n" + " ],\n" + " \"adjust_pure_negative\" : true,\n" + + " \"minimum_should_match\" : \"1\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; diff --git a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/LineageExporter.java b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/LineageExporter.java index 4b7d81aa04416a..4a2411138ed67b 100644 --- a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/LineageExporter.java +++ b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/LineageExporter.java @@ -58,7 +58,7 @@ public void exportGraphIndex( Set urns, Set visitedUrns, Set visitedIds, int hops) { Set nextIds = new HashSet<>(); if (!urns.isEmpty()) { - BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().minimumShouldMatch(1); boolQueryBuilder.must(QueryBuilders.termQuery("relationshipType", "DownstreamOf")); @@ -70,7 +70,6 @@ public void exportGraphIndex( boolQueryBuilder.should( QueryBuilders.termsQuery("destination.urn", batch.toArray(String[]::new))); }); - boolQueryBuilder.minimumShouldMatch(1); // Exclude visited Lists.partition(Arrays.asList(visitedIds.toArray(String[]::new)), queryStatementSize) @@ -144,7 +143,10 @@ public void exportEntityIndex(Set ids, Set visitedIds, int hops) batch -> boolQueryBuilder.should( QueryBuilders.idsQuery().addIds(batch.toArray(String[]::new)))); - boolQueryBuilder.minimumShouldMatch(1); + + if (!boolQueryBuilder.should().isEmpty()) { + boolQueryBuilder.minimumShouldMatch(1); + } // Exclude visited Lists.partition(Arrays.asList(visitedIds.toArray(String[]::new)), queryStatementSize) diff --git a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full.json b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full.json index 0a1cee08414a9d..1aa1b4b5088a1d 100644 --- a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full.json +++ b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full.json @@ -33,11 +33,13 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, @@ -115,6 +117,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, @@ -156,6 +159,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, @@ -194,6 +198,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } @@ -212,6 +217,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } diff --git a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full_empty_filters.json b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full_empty_filters.json index ab2841d6602d82..5ba0e36456889a 100644 --- a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full_empty_filters.json +++ b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full_empty_filters.json @@ -33,11 +33,13 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, diff --git a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full_multiple_filters.json b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full_multiple_filters.json index 39f595e0e8dd2d..938d878a9c8d19 100644 --- a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full_multiple_filters.json +++ b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_full_multiple_filters.json @@ -36,11 +36,13 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, @@ -122,6 +124,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, @@ -163,6 +166,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, @@ -201,6 +205,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } @@ -219,6 +224,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } diff --git a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_limited.json b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_limited.json index 95d468ec3dac8e..24fbb56065ebf4 100644 --- a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_limited.json +++ b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_query_filters_limited.json @@ -27,6 +27,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } \ No newline at end of file diff --git a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_time_query_filters_1.json b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_time_query_filters_1.json index 327f1d4ff93389..13eb02fb61a4e5 100644 --- a/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_time_query_filters_1.json +++ b/metadata-io/src/test/resources/elasticsearch/sample_filters/lineage_time_query_filters_1.json @@ -56,6 +56,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, @@ -97,6 +98,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } }, @@ -135,6 +137,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } @@ -153,6 +156,7 @@ } ], "adjust_pure_negative" : true, + "minimum_should_match" : "1", "boost" : 1.0 } } \ No newline at end of file From 5c2c555a847cd353bccd7e85a8623d6a1c33562d Mon Sep 17 00:00:00 2001 From: RyanHolstien Date: Sat, 5 Oct 2024 06:07:07 -0500 Subject: [PATCH 21/25] fix(changeGenerator): fix logic around descriptions and make execution more efficient (#11539) --- .../SchemaMetadataChangeEventGenerator.java | 81 ++++++------- ...chemaMetadataChangeEventGeneratorTest.java | 110 ++++++++++++++++-- 2 files changed, 144 insertions(+), 47 deletions(-) diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGenerator.java index c6a48ea27cbf3f..53f757d8d6c6b1 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGenerator.java @@ -4,6 +4,7 @@ import com.datahub.util.RecordUtils; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.linkedin.common.AuditStamp; import com.linkedin.common.urn.DatasetUrn; import com.linkedin.common.urn.Urn; @@ -173,7 +174,7 @@ private static List getFieldPropertyChangeEvents( SchemaField baseField, SchemaField targetField, Urn datasetUrn, - ChangeCategory changeCategory, + Set changeCategories, AuditStamp auditStamp) { List propChangeEvents = new ArrayList<>(); String datasetFieldUrn; @@ -184,7 +185,7 @@ private static List getFieldPropertyChangeEvents( } // Description Change. - if (ChangeCategory.DOCUMENTATION.equals(changeCategory)) { + if (changeCategories != null && changeCategories.contains(ChangeCategory.DOCUMENTATION)) { ChangeEvent descriptionChangeEvent = getDescriptionChange(baseField, targetField, datasetFieldUrn, auditStamp); if (descriptionChangeEvent != null) { @@ -193,14 +194,14 @@ private static List getFieldPropertyChangeEvents( } // Global Tags - if (ChangeCategory.TAG.equals(changeCategory)) { + if (changeCategories != null && changeCategories.contains(ChangeCategory.TAG)) { propChangeEvents.addAll( getGlobalTagChangeEvents( baseField, targetField, datasetUrn.toString(), datasetFieldUrn, auditStamp)); } // Glossary terms. - if (ChangeCategory.GLOSSARY_TERM.equals(changeCategory)) { + if (changeCategories != null && changeCategories.contains(ChangeCategory.GLOSSARY_TERM)) { propChangeEvents.addAll( getGlossaryTermsChangeEvents( baseField, targetField, datasetUrn.toString(), datasetFieldUrn, auditStamp)); @@ -213,7 +214,7 @@ private static List computeDiffs( SchemaMetadata baseSchema, SchemaMetadata targetSchema, Urn datasetUrn, - ChangeCategory changeCategory, + Set changeCategories, AuditStamp auditStamp) { // Sort the fields by their field path. This aligns both sets of fields based on field paths for // comparisons. @@ -247,11 +248,11 @@ private static List computeDiffs( // This is the same field. Check for change events from property changes. if (!curBaseField.getNativeDataType().equals(curTargetField.getNativeDataType())) { processNativeTypeChange( - changeCategory, changeEvents, datasetUrn, curBaseField, curTargetField, auditStamp); + changeCategories, changeEvents, datasetUrn, curBaseField, curTargetField, auditStamp); } List propChangeEvents = getFieldPropertyChangeEvents( - curBaseField, curTargetField, datasetUrn, changeCategory, auditStamp); + curBaseField, curTargetField, datasetUrn, changeCategories, auditStamp); changeEvents.addAll(propChangeEvents); ++baseFieldIdx; ++targetFieldIdx; @@ -268,16 +269,17 @@ private static List computeDiffs( targetFields.subList(targetFieldIdx, targetFields.size()), renamedFields); if (renamedField == null) { - processRemoval(changeCategory, changeEvents, datasetUrn, curBaseField, auditStamp); + processRemoval(changeCategories, changeEvents, datasetUrn, curBaseField, auditStamp); ++baseFieldIdx; } else { - if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + if (changeCategories != null + && changeCategories.contains(ChangeCategory.TECHNICAL_SCHEMA)) { changeEvents.add( generateRenameEvent(datasetUrn, curBaseField, renamedField, auditStamp)); } List propChangeEvents = getFieldPropertyChangeEvents( - curBaseField, curTargetField, datasetUrn, changeCategory, auditStamp); + curBaseField, renamedField, datasetUrn, changeCategories, auditStamp); changeEvents.addAll(propChangeEvents); ++baseFieldIdx; renamedFields.add(renamedField); @@ -289,16 +291,17 @@ private static List computeDiffs( findRenamedField( curTargetField, baseFields.subList(baseFieldIdx, baseFields.size()), renamedFields); if (renamedField == null) { - processAdd(changeCategory, changeEvents, datasetUrn, curTargetField, auditStamp); + processAdd(changeCategories, changeEvents, datasetUrn, curTargetField, auditStamp); ++targetFieldIdx; } else { - if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + if (changeCategories != null + && changeCategories.contains(ChangeCategory.TECHNICAL_SCHEMA)) { changeEvents.add( generateRenameEvent(datasetUrn, renamedField, curTargetField, auditStamp)); } List propChangeEvents = getFieldPropertyChangeEvents( - curBaseField, curTargetField, datasetUrn, changeCategory, auditStamp); + renamedField, curTargetField, datasetUrn, changeCategories, auditStamp); changeEvents.addAll(propChangeEvents); ++targetFieldIdx; renamedFields.add(renamedField); @@ -309,7 +312,7 @@ private static List computeDiffs( // Handle removed fields. Non-backward compatible change + major version bump SchemaField baseField = baseFields.get(baseFieldIdx); if (!renamedFields.contains(baseField)) { - processRemoval(changeCategory, changeEvents, datasetUrn, baseField, auditStamp); + processRemoval(changeCategories, changeEvents, datasetUrn, baseField, auditStamp); } ++baseFieldIdx; } @@ -317,14 +320,15 @@ private static List computeDiffs( // Newly added fields. Forwards & backwards compatible change + minor version bump. SchemaField targetField = targetFields.get(targetFieldIdx); if (!renamedFields.contains(targetField)) { - processAdd(changeCategory, changeEvents, datasetUrn, targetField, auditStamp); + processAdd(changeCategories, changeEvents, datasetUrn, targetField, auditStamp); } ++targetFieldIdx; } // Handle primary key constraint change events. List primaryKeyChangeEvents = - getPrimaryKeyChangeEvents(changeCategory, baseSchema, targetSchema, datasetUrn, auditStamp); + getPrimaryKeyChangeEvents( + changeCategories, baseSchema, targetSchema, datasetUrn, auditStamp); changeEvents.addAll(primaryKeyChangeEvents); // Handle foreign key constraint change events, currently no-op due to field not being utilized. @@ -375,12 +379,12 @@ private static boolean descriptionsMatch(SchemaField curField, SchemaField schem } private static void processRemoval( - ChangeCategory changeCategory, + Set changeCategories, List changeEvents, Urn datasetUrn, SchemaField baseField, AuditStamp auditStamp) { - if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + if (changeCategories != null && changeCategories.contains(ChangeCategory.TECHNICAL_SCHEMA)) { changeEvents.add( DatasetSchemaFieldChangeEvent.schemaFieldChangeEventBuilder() .modifier(getSchemaFieldUrn(datasetUrn, baseField).toString()) @@ -401,17 +405,17 @@ private static void processRemoval( .build()); } List propChangeEvents = - getFieldPropertyChangeEvents(baseField, null, datasetUrn, changeCategory, auditStamp); + getFieldPropertyChangeEvents(baseField, null, datasetUrn, changeCategories, auditStamp); changeEvents.addAll(propChangeEvents); } private static void processAdd( - ChangeCategory changeCategory, + Set changeCategories, List changeEvents, Urn datasetUrn, SchemaField targetField, AuditStamp auditStamp) { - if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + if (changeCategories != null && changeCategories.contains(ChangeCategory.TECHNICAL_SCHEMA)) { changeEvents.add( DatasetSchemaFieldChangeEvent.schemaFieldChangeEventBuilder() .modifier(getSchemaFieldUrn(datasetUrn, targetField).toString()) @@ -428,22 +432,23 @@ private static void processAdd( .fieldUrn(getSchemaFieldUrn(datasetUrn, targetField)) .nullable(targetField.isNullable()) .auditStamp(auditStamp) + .modificationCategory(SchemaFieldModificationCategory.OTHER) .build()); } List propChangeEvents = - getFieldPropertyChangeEvents(null, targetField, datasetUrn, changeCategory, auditStamp); + getFieldPropertyChangeEvents(null, targetField, datasetUrn, changeCategories, auditStamp); changeEvents.addAll(propChangeEvents); } private static void processNativeTypeChange( - ChangeCategory changeCategory, + Set changeCategories, List changeEvents, Urn datasetUrn, SchemaField curBaseField, SchemaField curTargetField, AuditStamp auditStamp) { // Non-backward compatible change + Major version bump - if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + if (changeCategories != null && changeCategories.contains(ChangeCategory.TECHNICAL_SCHEMA)) { changeEvents.add( DatasetSchemaFieldChangeEvent.schemaFieldChangeEventBuilder() .category(ChangeCategory.TECHNICAL_SCHEMA) @@ -505,12 +510,12 @@ private static List getForeignKeyChangeEvents() { } private static List getPrimaryKeyChangeEvents( - ChangeCategory changeCategory, + Set changeCategories, SchemaMetadata baseSchema, SchemaMetadata targetSchema, Urn datasetUrn, AuditStamp auditStamp) { - if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + if (changeCategories != null && changeCategories.contains(ChangeCategory.TECHNICAL_SCHEMA)) { List primaryKeyChangeEvents = new ArrayList<>(); Set basePrimaryKeys = (baseSchema != null && baseSchema.getPrimaryKeys() != null) @@ -598,7 +603,7 @@ public ChangeTransaction getSemanticDiff( baseSchema, targetSchema, DatasetUrn.createFromString(currentValue.getUrn()), - changeCategory, + Collections.singleton(changeCategory), null)); } catch (URISyntaxException e) { throw new IllegalArgumentException("Malformed DatasetUrn " + currentValue.getUrn()); @@ -632,18 +637,16 @@ public List getChangeEvents( @Nonnull Aspect from, @Nonnull Aspect to, @Nonnull AuditStamp auditStamp) { - final List changeEvents = new ArrayList<>(); - changeEvents.addAll( - computeDiffs( - from.getValue(), to.getValue(), urn, ChangeCategory.DOCUMENTATION, auditStamp)); - changeEvents.addAll( - computeDiffs(from.getValue(), to.getValue(), urn, ChangeCategory.TAG, auditStamp)); - changeEvents.addAll( + return new ArrayList<>( computeDiffs( - from.getValue(), to.getValue(), urn, ChangeCategory.TECHNICAL_SCHEMA, auditStamp)); - changeEvents.addAll( - computeDiffs( - from.getValue(), to.getValue(), urn, ChangeCategory.GLOSSARY_TERM, auditStamp)); - return changeEvents; + from.getValue(), + to.getValue(), + urn, + ImmutableSet.of( + ChangeCategory.DOCUMENTATION, + ChangeCategory.TAG, + ChangeCategory.TECHNICAL_SCHEMA, + ChangeCategory.GLOSSARY_TERM), + auditStamp)); } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java b/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java index 772ef374af18b0..88dd81d953947c 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java @@ -6,6 +6,7 @@ import com.linkedin.common.urn.Urn; import com.linkedin.data.template.StringArray; import com.linkedin.metadata.timeline.data.ChangeEvent; +import com.linkedin.metadata.timeline.data.dataset.DatasetSchemaFieldChangeEvent; import com.linkedin.metadata.timeline.data.dataset.SchemaFieldModificationCategory; import com.linkedin.mxe.SystemMetadata; import com.linkedin.restli.internal.server.util.DataMapUtils; @@ -18,6 +19,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.annotations.Test; @@ -41,18 +43,17 @@ private static void compareDescriptions( changeEvent -> { actualDescriptions.add(changeEvent.getDescription()); }); - assertEquals(expectedDescriptions, actualDescriptions); + assertEquals(actualDescriptions, expectedDescriptions); } private static void compareModificationCategories( Set expectedCategories, List actual) { - Set actualModificationCategories = new HashSet<>(); - actual.forEach( - changeEvent -> { - actualModificationCategories.add( - changeEvent.getParameters().get("modificationCategory").toString()); - }); - assertEquals(expectedCategories, actualModificationCategories); + Set actualModificationCategories = + actual.stream() + .filter(changeEvent -> changeEvent instanceof DatasetSchemaFieldChangeEvent) + .map(changeEvent -> changeEvent.getParameters().get("modificationCategory").toString()) + .collect(Collectors.toSet()); + assertEquals(actualModificationCategories, expectedCategories); } private static Aspect getSchemaMetadata(List schemaFieldList) { @@ -236,6 +237,99 @@ public void testDelete() throws Exception { assertEquals(14, actual.size()); } + @Test + public void testSchemaFieldPrimaryKeyChangeRenameAdd() throws Exception { + // When a rename cannot be detected, treated as drop -> add + SchemaMetadataChangeEventGenerator test = new SchemaMetadataChangeEventGenerator(); + + Urn urn = getTestUrn(); + String entity = "dataset"; + String aspect = "schemaMetadata"; + AuditStamp auditStamp = getTestAuditStamp(); + + Aspect from = + getSchemaMetadata( + List.of( + new SchemaField() + .setFieldPath("ID") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Description"), + new SchemaField() + .setFieldPath("ID2") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Other Description"))); + from.getValue().setPrimaryKeys(new StringArray(List.of("ID"))); + Aspect to3 = + getSchemaMetadata( + List.of( + new SchemaField() + .setFieldPath("ID") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Description"), + new SchemaField() + .setFieldPath("ID2") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Other Description"))); + to3.getValue().setPrimaryKeys(new StringArray(List.of("ID2"))); + List actual = test.getChangeEvents(urn, entity, aspect, from, to3, auditStamp); + compareDescriptions( + Set.of( + "A backwards incompatible change due to a primary key constraint change. " + + "The following fields were removed: 'ID'. The following fields were added: 'ID2'."), + actual); + assertEquals(1, actual.size()); + compareModificationCategories(Set.of(SchemaFieldModificationCategory.OTHER.toString()), actual); + + Aspect to4 = + getSchemaMetadata( + List.of( + new SchemaField() + .setFieldPath("IDZ") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Description"), + new SchemaField() + .setFieldPath("ID2") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Other Description"))); + to4.getValue().setPrimaryKeys(new StringArray(List.of("ID2"))); + + List actual2 = test.getChangeEvents(urn, entity, aspect, to3, to4, auditStamp); + compareDescriptions( + Set.of( + "A forwards & backwards compatible change due to renaming of the field 'ID to IDZ'."), + actual2); + assertEquals(1, actual2.size()); + compareModificationCategories( + Set.of(SchemaFieldModificationCategory.RENAME.toString()), actual2); + + Aspect to5 = + getSchemaMetadata( + List.of( + new SchemaField() + .setFieldPath("IDZ") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Description"), + new SchemaField() + .setFieldPath("ID1") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Third Description"), + new SchemaField() + .setFieldPath("ID2") + .setNativeDataType("NUMBER(16,1)") + .setDescription("My Other Description"))); + to5.getValue().setPrimaryKeys(new StringArray(List.of("ID2"))); + + List actual3 = test.getChangeEvents(urn, entity, aspect, to4, to5, auditStamp); + compareDescriptions( + Set.of( + "A forwards & backwards compatible change due to the newly added field 'ID1'.", + "The description 'My Third Description' for the field 'ID1' has been added."), + actual3); + assertEquals(actual3.size(), 2); + compareModificationCategories( + Set.of(SchemaFieldModificationCategory.OTHER.toString()), actual3); + } + // CHECKSTYLE:OFF private static final String TEST_OBJECT = "{\"platformSchema\":{\"com.linkedin.schema.KafkaSchema\":{\"documentSchema\":\"{\\\"type\\\":\\\"record\\\",\\\"name\\\":\\\"SampleHdfsSchema\\\",\\\"namespace\\\":\\\"com.linkedin.dataset\\\",\\\"doc\\\":\\\"Sample HDFS dataset\\\",\\\"fields\\\":[{\\\"name\\\":\\\"field_foo\\\",\\\"type\\\":[\\\"string\\\"]},{\\\"name\\\":\\\"field_bar\\\",\\\"type\\\":[\\\"boolean\\\"]}]}\"}},\"created\":{\"actor\":\"urn:li:corpuser:jdoe\",\"time\":1674291843000},\"lastModified\":{\"actor\":\"urn:li:corpuser:jdoe\",\"time\":1674291843000},\"fields\":[{\"nullable\":false,\"fieldPath\":\"shipment_info\",\"description\":\"Shipment info description\",\"isPartOfKey\":false,\"type\":{\"type\":{\"com.linkedin.schema.RecordType\":{}}},\"recursive\":false,\"nativeDataType\":\"varchar(100)\"},{\"nullable\":false,\"fieldPath\":\"shipment_info.date\",\"description\":\"Shipment info date description\",\"isPartOfKey\":false,\"type\":{\"type\":{\"com.linkedin.schema.DateType\":{}}},\"recursive\":false,\"nativeDataType\":\"Date\"},{\"nullable\":false,\"fieldPath\":\"shipment_info.target\",\"description\":\"Shipment info target description\",\"isPartOfKey\":false,\"type\":{\"type\":{\"com.linkedin.schema.StringType\":{}}},\"recursive\":false,\"nativeDataType\":\"text\"},{\"nullable\":false,\"fieldPath\":\"shipment_info.destination\",\"description\":\"Shipment info destination description\",\"isPartOfKey\":false,\"type\":{\"type\":{\"com.linkedin.schema.StringType\":{}}},\"recursive\":false,\"nativeDataType\":\"varchar(100)\"},{\"nullable\":false,\"fieldPath\":\"shipment_info.geo_info\",\"description\":\"Shipment info geo_info description\",\"isPartOfKey\":false,\"type\":{\"type\":{\"com.linkedin.schema.RecordType\":{}}},\"recursive\":false,\"nativeDataType\":\"varchar(100)\"},{\"nullable\":false,\"fieldPath\":\"shipment_info.geo_info.lat\",\"description\":\"Shipment info geo_info lat\",\"isPartOfKey\":false,\"type\":{\"type\":{\"com.linkedin.schema.NumberType\":{}}},\"recursive\":false,\"nativeDataType\":\"float\"},{\"nullable\":false,\"fieldPath\":\"shipment_info.geo_info.lng\",\"description\":\"Shipment info geo_info lng\",\"isPartOfKey\":false,\"type\":{\"type\":{\"com.linkedin.schema.NumberType\":{}}},\"recursive\":false,\"nativeDataType\":\"float\"}],\"schemaName\":\"SampleHdfsSchema\",\"version\":0,\"hash\":\"\",\"platform\":\"urn:li:dataPlatform:hdfs\"}"; From fff67b97e4767f9d30131c236c4c6f7f95259486 Mon Sep 17 00:00:00 2001 From: Shirshanka Das Date: Sat, 5 Oct 2024 15:43:40 -0700 Subject: [PATCH 22/25] feat(models): add support for generic platform resources (#11531) --- .../com/linkedin/common/SerializedValue.pdl | 42 ++++++++++++++ .../platformresource/PlatformResourceInfo.pdl | 57 +++++++++++++++++++ .../platformresource/PlatformResourceKey.pdl | 28 +++++++++ .../platformresource/PlatformResourceType.pdl | 17 ++++++ .../src/main/resources/entity-registry.yml | 11 ++++ 5 files changed, 155 insertions(+) create mode 100644 metadata-models/src/main/pegasus/com/linkedin/common/SerializedValue.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceInfo.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceKey.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceType.pdl diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/SerializedValue.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/SerializedValue.pdl new file mode 100644 index 00000000000000..ae1d00c568325d --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/common/SerializedValue.pdl @@ -0,0 +1,42 @@ +namespace com.linkedin.common + +/** + * Captures the serialized value of a (usually) schema-d blob. + */ +record SerializedValue { + /** + * The serialized blob value. + */ + blob: bytes + + /** + * The content-type of the serialized blob value. + */ + contentType: enum SerializedValueContentType { + JSON, + BINARY + } = "JSON" + + /** + * The schema type for the schema that models the object that was serialized + into the blob. + * Absence of this field indicates that the schema is not known. + * If the schema is known, the value should be set to the appropriate schema + * type. + * Use the NONE value if the existing schema categories do not apply. + */ + schemaType: optional enum SerializedValueSchemaType { + AVRO + PROTOBUF + PEGASUS + THRIFT + JSON + NONE + } + + /** + * An optional reference to the schema that models the object. + * e.g., 'com.linkedin.platformresource.slack.SlackConversation' + */ + schemaRef: optional string +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceInfo.pdl new file mode 100644 index 00000000000000..32dff19b44a53a --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceInfo.pdl @@ -0,0 +1,57 @@ +namespace com.linkedin.platformresource + +import com.linkedin.common.SerializedValue + +/** + * Platform Resource Info. + * These entities are for miscelaneous data that is used in non-core parts of the system. + * For instance, if we want to persist & retrieve data from auxiliary integrations such as Slack or Microsoft Teams. + */ +@Aspect = { + "name": "platformResourceInfo" +} +record PlatformResourceInfo { + /** + * The type of the resource. + * Intended as a loose specifier of the generic type of the resource. + * Producer is not forced to conform to a specific set of symbols for + * resource types. + * The @PlatformResourceType enumeration offers a paved path for agreed upon + * common terms, but is not required to be followed. + * Example values could be: conversation, user, grant, etc. + * Resource types are indexed for ease of access. + * e.g. Get me all platform resources of type user for the platform looker + */ + @Searchable = { + "fieldType": "KEYWORD" + } + resourceType: string + + /** + * The primary key for this platform resource. + * e.g. for a slack member this would be the memberID. + * primary keys specified here don't need to include any additional specificity for the + dataPlatform + * The @PlatformResourceKey is supposed to represent that + */ + @Searchable = { + "fieldType": "KEYWORD" + } + primaryKey: string + + /** + * The secondary keys this platform resource can be located by. + * I.e., for a slack member this would be email or phone. + */ + @Searchable = { + "/*": { + "fieldType": "KEYWORD" + } + } + secondaryKeys: optional array[string] + + /** + * The serialized value of this platform resource item. + */ + value: optional SerializedValue +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceKey.pdl new file mode 100644 index 00000000000000..8514c73bc26eb8 --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceKey.pdl @@ -0,0 +1,28 @@ +namespace com.linkedin.platformresource + +/** + * Key for a Platform Resource. + * Platform Resources are assets that are not part of the core data model. + * They are stored in DataHub primarily to help with application-specific + * use-cases that are not sufficiently generalized to move into the core data model. + * For instance, if we want to persist & retrieve additional user profile data + * from auxiliary integrations such as Slack or Microsoft Teams for resolving details later. + */ +@Aspect = { + "name": "platformResourceKey" +} +record PlatformResourceKey { + /** + * A unique id for this entity. + * There are no constraints on the format of this id, but most implementations + * will choose to use a UUID. + * This id should be globally unique for the entire DataHub instance and + uniquely identify the resource that is being stored, so most + implementations + * will combine logical attributes like platform name, platform instance, + * platform-specific-id and the resource type to create the unique id. + * e.g. slack:slack-instance:slack-user-id:user-info + * or guid(slack, slack-instance, slack-user-id, user-info) etc. + */ + id: string +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceType.pdl b/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceType.pdl new file mode 100644 index 00000000000000..2f36eda9141abb --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/platformresource/PlatformResourceType.pdl @@ -0,0 +1,17 @@ +namespace com.linkedin.platformresource + +/** +* A set of symbols for loose agreements between producers and consumers of + platform resources + See @PlatformResourceInfo.resourceType for where this can be populated into + **/ +enum PlatformResourceType { + /** + * e.g. a Slack member resource, Looker user resource, etc. + */ + USER_INFO, + /** + * e.g. a Slack channel + */ + CONVERSATION +} diff --git a/metadata-models/src/main/resources/entity-registry.yml b/metadata-models/src/main/resources/entity-registry.yml index 7beb08a6b1032f..9b692b51dc2b5f 100644 --- a/metadata-models/src/main/resources/entity-registry.yml +++ b/metadata-models/src/main/resources/entity-registry.yml @@ -615,6 +615,17 @@ entities: aspects: - dataHubConnectionDetails - dataPlatformInstance + - name: platformResource + doc: >- + Platform Resources are assets that are unmodeled and stored outside of + the core data model. They are stored in DataHub primarily to help with + application-specific use-cases that are not sufficiently generalized to move into the core data model. + category: core + keyAspect: platformResourceKey + aspects: + - dataPlatformInstance + - platformResourceInfo + - status events: plugins: aspectPayloadValidators: From 849e2c1db18e5368268c6c2a08e22488d6e7f993 Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Mon, 7 Oct 2024 00:40:03 -0700 Subject: [PATCH 23/25] fix(ingest): fix UnboundLocalError in dataproduct transformer (#11528) --- .../datahub/ingestion/transformer/add_dataset_dataproduct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata-ingestion/src/datahub/ingestion/transformer/add_dataset_dataproduct.py b/metadata-ingestion/src/datahub/ingestion/transformer/add_dataset_dataproduct.py index 4045917eb830e0..ce224bde003fd3 100644 --- a/metadata-ingestion/src/datahub/ingestion/transformer/add_dataset_dataproduct.py +++ b/metadata-ingestion/src/datahub/ingestion/transformer/add_dataset_dataproduct.py @@ -53,9 +53,9 @@ def handle_end_of_stream( data_products: Dict[str, DataProductPatchBuilder] = {} data_products_container: Dict[str, DataProductPatchBuilder] = {} logger.debug("Generating dataproducts") + is_container = self.config.is_container for entity_urn in self.entity_map.keys(): data_product_urn = self.config.get_data_product_to_add(entity_urn) - is_container = self.config.is_container if data_product_urn: if data_product_urn not in data_products: data_products[data_product_urn] = DataProductPatchBuilder( From ad24cf1aacaa3174eed8d29b4c89554938ab525a Mon Sep 17 00:00:00 2001 From: Aseem Bansal Date: Mon, 7 Oct 2024 19:29:21 +0530 Subject: [PATCH 24/25] fix(ingest): add typeUrn in glossary sync source (#11545) --- .../src/datahub/ingestion/source/metadata/business_glossary.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/metadata-ingestion/src/datahub/ingestion/source/metadata/business_glossary.py b/metadata-ingestion/src/datahub/ingestion/source/metadata/business_glossary.py index d3c4e2e3cd80e8..79ec47a7efb2c2 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/metadata/business_glossary.py +++ b/metadata-ingestion/src/datahub/ingestion/source/metadata/business_glossary.py @@ -40,6 +40,7 @@ class Owners(ConfigModel): type: str = models.OwnershipTypeClass.DEVELOPER + typeUrn: Optional[str] = None users: Optional[List[str]] = None groups: Optional[List[str]] = None @@ -154,6 +155,8 @@ def make_glossary_term_urn( def get_owners(owners: Owners) -> models.OwnershipClass: ownership_type, ownership_type_urn = validate_ownership_type(owners.type) + if owners.typeUrn is not None: + ownership_type_urn = owners.typeUrn owners_meta: List[models.OwnerClass] = [] if owners.users is not None: owners_meta = owners_meta + [ From 7884c855a9776524ecb98ffb1d78dc575e629256 Mon Sep 17 00:00:00 2001 From: Jay <159848059+jayacryl@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:29:32 +0530 Subject: [PATCH 25/25] fix(docs-site) cloud form copy spelling error (#11455) --- docs-website/src/pages/cloud/DemoForm/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-website/src/pages/cloud/DemoForm/index.jsx b/docs-website/src/pages/cloud/DemoForm/index.jsx index 28777e722e962d..fcaa1e129d9bed 100644 --- a/docs-website/src/pages/cloud/DemoForm/index.jsx +++ b/docs-website/src/pages/cloud/DemoForm/index.jsx @@ -80,9 +80,9 @@ const DemoForm = ({ formId }) => {

-
Book a free Demo
+
Book a Demo
- Schedule a personalized demo and get a free a trial. + Schedule your personalized demo and get a free trial.
{/* Use unique ID */}