chore: import upstream snapshot with attribution
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:35:45 +08:00
commit bf2343b7e4
16049 changed files with 3531137 additions and 0 deletions
@@ -0,0 +1,5 @@
package org.openmetadata.schema;
public interface AppRuntime {
Boolean getEnabled();
}
@@ -0,0 +1,26 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
import java.util.List;
import org.openmetadata.schema.type.EntityReference;
/** Interface to be implemented by all entities to provide a way to access all the common fields. */
@SuppressWarnings("unused")
public interface BulkAssetsRequestInterface {
// Lower case entity name to canonical entity name map
List<EntityReference> getAssets();
void setAssets(List<EntityReference> assets);
}
@@ -0,0 +1,28 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
import java.util.List;
import org.openmetadata.schema.type.Column;
/**
* Interface to be implemented by entities with a list of Column and FullyQualifiedName. It is used
* when adding lineage between different entities.
*/
public interface ColumnsEntityInterface {
String getFullyQualifiedName();
List<Column> getColumns();
}
@@ -0,0 +1,82 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
import java.util.List;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.LifeCycle;
import org.openmetadata.schema.type.TagLabel;
@SuppressWarnings("unchecked")
public interface CreateEntity {
String getName();
String getDisplayName();
String getDescription();
default List<EntityReference> getOwners() {
return null;
}
default List<EntityReference> getReviewers() {
return null;
}
default List<TagLabel> getTags() {
return null;
}
default Object getExtension() {
return null;
}
default List<String> getDomains() {
return null;
}
default List<String> getDataProducts() {
return null;
}
default LifeCycle getLifeCycle() {
return null;
}
<K extends CreateEntity> K withName(String name);
<K extends CreateEntity> K withDisplayName(String displayName);
<K extends CreateEntity> K withDescription(String description);
default void setOwners(List<EntityReference> owners) {}
default void setDomains(List<String> domains) {}
default void setDataProducts(List<String> dataProducts) {}
default void setTags(List<TagLabel> tags) {
/* no-op implementation to be overridden */
}
default void setReviewers(List<EntityReference> reviewers) {}
default <K extends CreateEntity> K withExtension(Object extension) {
return (K) this;
}
default <K extends CreateEntity> K withLifeCycle(LifeCycle lifeCycle) {
return (K) this;
}
}
@@ -0,0 +1,5 @@
package org.openmetadata.schema;
public interface DataInsightInterface {
Long getTimestamp();
}
@@ -0,0 +1,25 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
/**
* Interface to be implemented by all doc store entities to provide a way to access all the common
* fields.
*/
@SuppressWarnings("unused")
public interface DocStoreEntityInterface {
String getEntityType();
Object getData();
}
@@ -0,0 +1,254 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.entity.type.Style;
import org.openmetadata.schema.type.*;
import org.openmetadata.schema.utils.EntityInterfaceUtil;
/** Interface to be implemented by all entities to provide a way to access all the common fields. */
@SuppressWarnings("unused")
public interface EntityInterface {
// Lower case entity name to canonical entity name map
Map<String, String> CANONICAL_ENTITY_NAME_MAP = new HashMap<>();
Map<String, Class<? extends EntityInterface>> ENTITY_TYPE_TO_CLASS_MAP = new HashMap<>();
UUID getId();
String getDescription();
String getDisplayName();
String getName();
default Boolean getDeleted() {
return null;
}
Double getVersion();
String getUpdatedBy();
Long getUpdatedAt();
default String getImpersonatedBy() {
return null;
}
URI getHref();
ChangeDescription getChangeDescription();
ChangeDescription getIncrementalChangeDescription();
default UsageDetails getUsageSummary() {
return null;
}
default List<EntityReference> getOwners() {
return null;
}
default List<TagLabel> getTags() {
return null;
}
default EntityStatus getEntityStatus() {
return null;
}
default ProviderType getProvider() {
return null;
}
default List<EntityReference> getFollowers() {
return null;
}
default Votes getVotes() {
return null;
}
default EntityReference getService() {
return null;
}
String getFullyQualifiedName();
default Object getExtension() {
return null;
}
default List<EntityReference> getChildren() {
return null;
}
default List<EntityReference> getReviewers() {
return null;
}
default List<EntityReference> getExperts() {
return null;
}
default List<EntityReference> getDomains() {
return null;
}
default List<EntityReference> getDataProducts() {
return null;
}
default EntityReference getDataContract() {
return null;
}
default Style getStyle() {
return null;
}
default LifeCycle getLifeCycle() {
return null;
}
default AssetCertification getCertification() {
return null;
}
/**
* Source hash (fingerprint) of the entity as computed by the ingestion connector. Only entities
* whose JSON schema declares {@code sourceHash} override this; all others inherit {@code null}.
* Used by the bulk update path to skip entities whose source content is unchanged.
*/
default String getSourceHash() {
return null;
}
void setId(UUID id);
void setDescription(String description);
void setDisplayName(String displayName);
void setName(String name);
void setVersion(Double newVersion);
void setChangeDescription(ChangeDescription changeDescription);
void setIncrementalChangeDescription(ChangeDescription incrementalChangeDescription);
default void setUsageSummary(UsageDetails usageSummary) {}
void setFullyQualifiedName(String fullyQualifiedName);
default void setDeleted(Boolean flag) {}
void setUpdatedBy(String admin);
void setUpdatedAt(Long updatedAt);
default void setImpersonatedBy(String botName) {
/* no-op implementation to be overridden */
}
void setHref(URI href);
default void setTags(List<TagLabel> tags) {
/* no-op implementation to be overridden */
}
default void setEntityStatus(EntityStatus approvalStatus) {
/* no-op implementation to be overridden */
}
default void setOwners(List<EntityReference> owners) {
/* no-op implementation to be overridden */
}
default void setExtension(Object extension) {
/* no-op implementation to be overridden */
}
default void setChildren(List<EntityReference> entityReference) {
/* no-op implementation to be overridden */
}
default void setReviewers(List<EntityReference> entityReference) {
/* no-op implementation to be overridden */
}
default void setExperts(List<EntityReference> entityReference) {
/* no-op implementation to be overridden */
}
default void setDomains(List<EntityReference> entityReference) {
/* no-op implementation to be overridden */
}
default void setDataProducts(List<EntityReference> dataProducts) {
/* no-op implementation to be overridden */
}
default void setDataContract(EntityReference dataContract) {
/* no-op implementation to be overridden */
}
default void setFollowers(List<EntityReference> followers) {
/* no-op implementation to be overridden */
}
default void setVotes(Votes vote) {
/* no-op implementation to be overridden */
}
default void setStyle(Style style) {
/* no-op implementation to be overridden */
}
default void setLifeCycle(LifeCycle lifeCycle) {
/* no-op implementation to be overridden */
}
default void setCertification(AssetCertification certification) {
/* no-op implementation to be overridden */
}
<T extends EntityInterface> T withHref(URI href);
@JsonIgnore
default EntityReference getEntityReference() {
return new EntityReference()
.withId(getId())
.withName(getName())
.withFullyQualifiedName(
getFullyQualifiedName() == null
? EntityInterfaceUtil.quoteName(getName())
: getFullyQualifiedName())
.withDescription(getDescription())
.withDisplayName(CommonUtil.nullOrEmpty(getDisplayName()) ? getName() : getDisplayName())
.withType(
CANONICAL_ENTITY_NAME_MAP.get(this.getClass().getSimpleName().toLowerCase(Locale.ROOT)))
.withDeleted(getDeleted())
.withHref(getHref());
}
}
@@ -0,0 +1,51 @@
package org.openmetadata.schema;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import org.openmetadata.schema.type.EntityReference;
public interface EntityTimeSeriesInterface {
Map<String, String> CANONICAL_ENTITY_NAME_MAP = new HashMap<>();
Map<String, Class<? extends EntityTimeSeriesInterface>> ENTITY_TYPE_TO_CLASS_MAP =
new HashMap<>();
UUID getId();
Long getTimestamp();
void setId(UUID id);
@JsonIgnore
default Date getDateParsedTimestamp() {
return new Date(getTimestamp());
}
@JsonIgnore
default String getStrParsedTimestamp() {
Date date = new Date(getTimestamp());
Format formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return formatter.format(date);
}
@JsonIgnore
default String getIso8601StrDate() {
Date date = new Date(getTimestamp());
Format formatter = new SimpleDateFormat("yyyy-MM-dd");
return formatter.format(date);
}
@JsonIgnore
default EntityReference getEntityReference() {
return new EntityReference()
.withId(getId())
.withType(
CANONICAL_ENTITY_NAME_MAP.get(
this.getClass().getSimpleName().toLowerCase(Locale.ROOT)));
}
}
@@ -0,0 +1,19 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
/** Interface which could be implemented by Enums classes */
public interface EnumInterface {
String value();
}
@@ -0,0 +1,24 @@
package org.openmetadata.schema;
import java.util.List;
import org.openmetadata.schema.type.TagLabel;
public interface FieldInterface {
String getName();
String getDisplayName();
String getDescription();
String getDataTypeDisplay();
String getFullyQualifiedName();
List<TagLabel> getTags();
default void setTags(List<TagLabel> tags) {
/* no-op implementation to be overridden */
}
List<? extends FieldInterface> getChildren();
}
@@ -0,0 +1,21 @@
package org.openmetadata.schema;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.openmetadata.schema.type.Function.ParameterType;
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface Function {
String name();
String input();
String description();
String[] examples();
ParameterType paramInputType() default ParameterType.NOT_REQUIRED;
}
@@ -0,0 +1,136 @@
/*
* Copyright 2024 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
import java.util.List;
import org.openmetadata.schema.entity.type.Style;
import org.openmetadata.schema.type.AssetCertification;
import org.openmetadata.schema.type.ChangeDescription;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.LifeCycle;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.type.Votes;
/**
* Lightweight interface for named objects that don't need full entity semantics. Use this for
* objects like Task, Workflow instances, etc. that need identity and versioning but not owners,
* followers, votes, or other entity-specific features.
*
* <p>Extends EntityInterface to reuse existing repository infrastructure, but provides explicit
* null/no-op defaults for features that don't apply to lightweight entities.
*/
@SuppressWarnings("unused")
public interface NamedEntityInterface extends EntityInterface {
@Override
default List<EntityReference> getOwners() {
return null;
}
@Override
default void setOwners(List<EntityReference> owners) {}
@Override
default List<EntityReference> getFollowers() {
return null;
}
@Override
default void setFollowers(List<EntityReference> followers) {}
@Override
default Votes getVotes() {
return null;
}
@Override
default void setVotes(Votes votes) {}
@Override
default List<EntityReference> getDataProducts() {
return null;
}
@Override
default void setDataProducts(List<EntityReference> dataProducts) {}
@Override
default List<TagLabel> getTags() {
return null;
}
@Override
default void setTags(List<TagLabel> tags) {}
@Override
default Object getExtension() {
return null;
}
@Override
default void setExtension(Object extension) {}
@Override
default Style getStyle() {
return null;
}
@Override
default void setStyle(Style style) {}
@Override
default LifeCycle getLifeCycle() {
return null;
}
@Override
default void setLifeCycle(LifeCycle lifeCycle) {}
@Override
default AssetCertification getCertification() {
return null;
}
@Override
default void setCertification(AssetCertification certification) {}
@Override
default List<EntityReference> getExperts() {
return null;
}
@Override
default void setExperts(List<EntityReference> experts) {}
@Override
default List<EntityReference> getChildren() {
return null;
}
@Override
default void setChildren(List<EntityReference> children) {}
@Override
default ChangeDescription getIncrementalChangeDescription() {
return null;
}
@Override
default void setIncrementalChangeDescription(ChangeDescription changeDescription) {}
@Override
default EntityReference getService() {
return null;
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
/**
* Interface to be implemented by all services entities to provide a way to access all the common
* fields.
*/
public interface ServiceConnectionEntityInterface {
Object getConfig();
void setConfig(Object config);
}
@@ -0,0 +1,41 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
import java.util.List;
import org.openmetadata.schema.entity.services.connections.TestConnectionResult;
import org.openmetadata.schema.type.EntityReference;
/**
* Interface to be implemented by all services entities to provide a way to access all the common
* fields.
*/
public interface ServiceEntityInterface extends EntityInterface {
ServiceConnectionEntityInterface getConnection();
ServiceEntityInterface withOwners(List<EntityReference> owners);
void setPipelines(List<EntityReference> pipelines);
void setIngestionRunner(EntityReference ingestionRunner);
void setTestConnectionResult(TestConnectionResult testConnectionResult);
EnumInterface getServiceType();
default EntityReference getIngestionRunner() {
return null;
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema;
import java.util.Collections;
import java.util.Set;
public interface SubscriptionAction {
default Set<String> getReceivers() {
return Collections.emptySet();
}
default Boolean getSendToAdmins() {
return false;
}
default Boolean getSendToOwners() {
return false;
}
default Boolean getSendToFollowers() {
return false;
}
}
@@ -0,0 +1,22 @@
package org.openmetadata.schema;
import java.util.UUID;
import org.openmetadata.schema.auth.TokenType;
public interface TokenInterface {
UUID getToken();
UUID getUserId();
TokenType getTokenType();
Long getExpiryDate();
void setToken(UUID id);
void setUserId(UUID id);
void setTokenType(TokenType type);
void setExpiryDate(Long expiry);
}
@@ -0,0 +1,44 @@
/*
* Copyright 2026 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema.entity.ai;
/**
* Common registration/approval milestones shared by {@code GovernanceMetadata} (AI Application) and
* {@code McpGovernanceMetadata} (MCP Server), which are otherwise distinct generated types with no
* shared supertype. Applied via {@code javaInterfaces} in the schema so the intake/approval logic
* can stamp these fields once instead of duplicating it per governance-metadata type. The
* {@code registrationStatus} enum is intentionally excluded — each type generates its own nested
* enum with identical values.
*/
public interface AIGovernanceRegistration {
String getRegisteredBy();
void setRegisteredBy(String registeredBy);
Long getRegisteredAt();
void setRegisteredAt(Long registeredAt);
String getApprovedBy();
void setApprovedBy(String approvedBy);
Long getApprovedAt();
void setApprovedAt(Long approvedAt);
String getApprovalComments();
void setApprovalComments(String approvalComments);
}
@@ -0,0 +1,38 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema.exception;
import jakarta.ws.rs.core.Response;
import org.openmetadata.sdk.exception.WebServiceException;
public class JsonParsingException extends WebServiceException {
private static final String MESSAGE = "JSON parsing failed with message [%s].";
public static final String JSON_PARSING_ERROR = "JSON_PARSING_EXCEPTION";
public JsonParsingException(String exceptionMessage) {
super(
Response.Status.INTERNAL_SERVER_ERROR,
JSON_PARSING_ERROR,
String.format(MESSAGE, exceptionMessage));
}
public JsonParsingException(String exceptionMessage, Throwable cause) {
super(
Response.Status.INTERNAL_SERVER_ERROR,
JSON_PARSING_ERROR,
String.format(MESSAGE, exceptionMessage),
cause);
}
}
@@ -0,0 +1,141 @@
package org.openmetadata.schema.governance.workflows.elements;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.util.List;
import java.util.Map;
import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.ApplyRecognizerFeedbackTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.CheckChangeDescriptionTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.CheckEntityAttributesTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.CreateAndRunAIAutomationTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.CreateAndRunIngestionPipelineTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.DataCompletenessTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.PolicyAgentTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.RejectRecognizerFeedbackTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.RollbackEntityTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.RunAppTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.SetEntityAttributeTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.SetEntityCertificationTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.SetGlossaryTermStatusTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.automatedTask.SinkTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.endEvent.EndEventDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.gateway.ParallelGatewayDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.startEvent.StartEventDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.userTask.CreateRecognizerFeedbackApprovalTaskDefinition;
import org.openmetadata.schema.governance.workflows.elements.nodes.userTask.UserApprovalTaskDefinition;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "subType")
@JsonSubTypes({
@JsonSubTypes.Type(
value = CheckEntityAttributesTaskDefinition.class,
name = "checkEntityAttributesTask"),
@JsonSubTypes.Type(
value = CheckChangeDescriptionTaskDefinition.class,
name = "checkChangeDescriptionTask"),
@JsonSubTypes.Type(
value = SetEntityCertificationTaskDefinition.class,
name = "setEntityCertificationTask"),
@JsonSubTypes.Type(
value = SetEntityAttributeTaskDefinition.class,
name = "setEntityAttributeTask"),
@JsonSubTypes.Type(value = RollbackEntityTaskDefinition.class, name = "rollbackEntityTask"),
@JsonSubTypes.Type(value = DataCompletenessTaskDefinition.class, name = "dataCompletenessTask"),
@JsonSubTypes.Type(value = StartEventDefinition.class, name = "startEvent"),
@JsonSubTypes.Type(value = EndEventDefinition.class, name = "endEvent"),
@JsonSubTypes.Type(
value = SetGlossaryTermStatusTaskDefinition.class,
name = "setGlossaryTermStatusTask"),
@JsonSubTypes.Type(value = UserApprovalTaskDefinition.class, name = "userApprovalTask"),
@JsonSubTypes.Type(
value = CreateAndRunIngestionPipelineTaskDefinition.class,
name = "createAndRunIngestionPipelineTask"),
@JsonSubTypes.Type(
value = CreateAndRunAIAutomationTaskDefinition.class,
name = "createAndRunAIAutomationTask"),
@JsonSubTypes.Type(value = RunAppTaskDefinition.class, name = "runAppTask"),
@JsonSubTypes.Type(value = ParallelGatewayDefinition.class, name = "parallelGateway"),
@JsonSubTypes.Type(value = SinkTaskDefinition.class, name = "sinkTask"),
@JsonSubTypes.Type(
value = CreateRecognizerFeedbackApprovalTaskDefinition.class,
name = "createRecognizerFeedbackApprovalTask"),
@JsonSubTypes.Type(
value = ApplyRecognizerFeedbackTaskDefinition.class,
name = "applyRecognizerFeedbackTask"),
@JsonSubTypes.Type(
value = RejectRecognizerFeedbackTaskDefinition.class,
name = "rejectRecognizerFeedbackTask"),
@JsonSubTypes.Type(value = PolicyAgentTaskDefinition.class, name = "policyAgentTask")
})
public interface WorkflowNodeDefinitionInterface {
String getType();
String getSubType();
String getName();
String getDisplayName();
String getDescription();
default Object getConfig() {
return null;
}
;
default List<String> getInput() {
return null;
}
;
default List<String> getOutput() {
return null;
}
;
default Object getInputNamespaceMap() {
return null;
}
void setType(String type);
void setSubType(String subType);
void setName(String name);
void setDisplayName(String displayName);
void setDescription(String description);
default void setConfig(Map<String, Object> config) {
/* no-op implementation to be overridden */
}
default void setInput(List<String> inputs) {
/* no-op implementation to be overridden */
}
default void setOutput(List<String> outputs) {
/* no-op implementation to be overridden */
}
default void setInputNamespaceMap(Object inputNamespaceMap) {
/* no-op implementation to be overridden */
}
@JsonIgnore
default String getNodeDisplayName() {
return CommonUtil.nullOrEmpty(getDisplayName()) ? getName() : getDisplayName();
}
@JsonIgnore
default NodeType getNodeType() {
return NodeType.fromValue(getType());
}
@JsonIgnore
default NodeSubType getNodeSubType() {
return NodeSubType.fromValue(getSubType());
}
}
@@ -0,0 +1,26 @@
package org.openmetadata.schema.governance.workflows.elements;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.util.Set;
import org.openmetadata.schema.governance.workflows.elements.triggers.EventBasedEntityTriggerDefinition;
import org.openmetadata.schema.governance.workflows.elements.triggers.NoOpTriggerDefinition;
import org.openmetadata.schema.governance.workflows.elements.triggers.PeriodicBatchEntityTriggerDefinition;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = EventBasedEntityTriggerDefinition.class, name = "eventBasedEntity"),
@JsonSubTypes.Type(value = NoOpTriggerDefinition.class, name = "noOp"),
@JsonSubTypes.Type(
value = PeriodicBatchEntityTriggerDefinition.class,
name = "periodicBatchEntity"),
})
public interface WorkflowTriggerInterface {
// TODO If set as enum, it results in null when the JSON is deserialized.
// Maybe there can be another way to validate it on deserialization.
String getType();
Object getConfig();
Set<String> getOutput();
}
@@ -0,0 +1,20 @@
package org.openmetadata.schema.utils;
public final class EntityInterfaceUtil {
/** Adds quotes to name as required */
// TODO change this FullyQualifiedName
public static String quoteName(String name) {
if (name != null && !name.contains("\"")) {
return name.contains(".") ? "\"" + name + "\"" : name;
}
return name;
}
/** Inverse of {@link #quoteName(String)}: strips a single enclosing pair of quotes if present. */
public static String unquoteName(String name) {
if (name != null && name.length() > 1 && name.startsWith("\"") && name.endsWith("\"")) {
return name.substring(1, name.length() - 1);
}
return name;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,223 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.schema.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import jakarta.validation.constraints.NotNull;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.openmetadata.schema.system.EntityError;
import org.openmetadata.schema.type.Paging;
/**
* Class used for generating JSON response for APIs returning list of objects in the following format: { "data" : [ {
* json for object 1}, {json for object 2}, ... ] }
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"data"})
public class ResultList<T> {
@JsonProperty("data")
@NotNull
private List<T> data;
@JsonProperty("paging")
private Paging paging;
@JsonProperty("errors")
private List<EntityError> errors;
@JsonProperty("warningsCount")
private Integer warningsCount;
/**
* Records read but not indexed for a non-failure reason (e.g. stale-relationship orphans).
* Carried separately from {@link #errors} so callers can surface them as warnings — and record
* them — without failing the batch.
*/
@JsonProperty("warnings")
private List<EntityError> warnings;
public ResultList() {}
public ResultList(List<T> data) {
this.data = data;
this.paging = null;
this.errors = null;
}
/**
* Cursor functionality. User has request 'limit' number of entries. The data provided must have 'limit' + 1 number of
* entries.
*
* <p>--------------------------------------------------------------------------------------------------------------
* Consider forward scrolling:
* --------------------------------------------------------------------------------------------------------------
* Query GET .../entities?limit=pagesize CASE 0: No before or after parameters in the query Returns: page1
* beforeCursor = null, -> Indicates first page afterCursor = last record in page1
*
* <p>Query GET .../entities?limit=pagesize&after={last record in page1} Returns: page2 beforeCursor = first record in
* page2 afterCursor = last record in page2
*
* <p>Query GET .../entities?limit=pagesize&after={last record in page2} CASE 1: Page 3 has less than limit number of
* entries and hence partial page is returned Returns: partial page 3 beforeCursor = first record in page3 afterCursor
* = null -------- FORWARD SCROLLING ENDS -------------
*
* <p>CASE 2: Page 3 has exactly page number of entries and not entries to follow after Returns: page3 beforeCursor =
* first record in page3 afterCursor = null -------- FORWARD SCROLLING ENDS -------------
*
* <p>--------------------------------------------------------------------------------------------------------------
* Consider backward scrolling from the previous state:
* --------------------------------------------------------------------------------------------------------------
*
* <p>Query GET .../entities?limit=pagesize&before={last record in page2 + 1} Returns: page2 beforeCursor = first
* record in page2 afterCursor = last record in page2
*
* <p>Query GET .../entities?limit=pagesize&before={first record page 2} CASE 3: Page 1 does not have {@code limit}
* number entries and hence partial page is returned Returns: page1 beforeCursor = null afterCursor = last record in
* page1 -------- BACKWARD SCROLLING ENDS -------------
*
* <p>CASE 4: Page 1 has exactly page number of entries Returns: page1 beforeCursor = null afterCursor = Empty string
* to start at page1 -------- BACKWARD SCROLLING ENDS -------------
*/
public ResultList(List<T> data, String beforeCursor, String afterCursor, int total) {
this.data = data;
paging =
new Paging()
.withBefore(
beforeCursor == null
? null
: Base64.getUrlEncoder()
.encodeToString(beforeCursor.getBytes(StandardCharsets.UTF_8)))
.withAfter(
afterCursor == null
? null
: Base64.getUrlEncoder()
.encodeToString(afterCursor.getBytes(StandardCharsets.UTF_8)))
.withTotal(total);
}
public ResultList(List<T> data, Integer offset, int total) {
this.data = data;
paging = new Paging().withBefore(null).withAfter(null).withTotal(total).withOffset(offset);
}
/* Conveniently map the data to another type without the need to create a new ResultList */
public <S> ResultList<S> map(Function<T, S> mapper) {
return new ResultList<>(data.stream().map(mapper).collect(Collectors.toList()), paging);
}
/* Conveniently filter the data without the need to create a new ResultList */
public ResultList<T> filter(Predicate<T> predicate) {
return new ResultList<>(data.stream().filter(predicate).collect(Collectors.toList()), paging);
}
public ResultList(List<T> data, Integer offset, Integer limit, Integer total) {
this.data = data;
paging =
new Paging()
.withBefore(null)
.withAfter(null)
.withTotal(total)
.withOffset(offset)
.withLimit(limit);
}
public ResultList(List<T> data, Paging other) {
this.data = data;
paging =
new Paging()
.withBefore(null)
.withAfter(null)
.withTotal(other.getTotal())
.withOffset(other.getOffset())
.withLimit(other.getLimit());
}
public ResultList(
List<T> data, List<EntityError> errors, String beforeCursor, String afterCursor, int total) {
this.data = data;
this.errors = errors;
paging =
new Paging()
.withBefore(
beforeCursor == null
? null
: Base64.getUrlEncoder()
.encodeToString(beforeCursor.getBytes(StandardCharsets.UTF_8)))
.withAfter(
afterCursor == null
? null
: Base64.getUrlEncoder()
.encodeToString(afterCursor.getBytes(StandardCharsets.UTF_8)))
.withTotal(total);
}
@JsonProperty("data")
public List<T> getData() {
return data;
}
@JsonProperty("data")
public void setData(List<T> data) {
this.data = data;
}
@JsonProperty("errors")
public List<EntityError> getErrors() {
return errors;
}
@JsonProperty("errors")
public void setErrors(List<EntityError> data) {
this.errors = data;
}
@JsonProperty("paging")
public Paging getPaging() {
return paging;
}
@JsonProperty("paging")
public ResultList<T> setPaging(Paging paging) {
this.paging = paging;
return this;
}
@JsonProperty("warningsCount")
public Integer getWarningsCount() {
return warningsCount;
}
@JsonProperty("warningsCount")
public void setWarningsCount(Integer warningsCount) {
this.warningsCount = warningsCount;
}
@JsonProperty("warnings")
public List<EntityError> getWarnings() {
return warnings;
}
@JsonProperty("warnings")
public void setWarnings(List<EntityError> warnings) {
this.warnings = warnings;
}
}
@@ -0,0 +1,52 @@
package org.openmetadata.schema.utils;
import java.io.InputStream;
import java.util.Properties;
import java.util.regex.Pattern;
import org.openmetadata.schema.api.OpenMetadataServerVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class VersionUtils {
private static final Logger LOG = LoggerFactory.getLogger(VersionUtils.class);
private VersionUtils() {}
public static OpenMetadataServerVersion getOpenMetadataServerVersion(String resourceName) {
OpenMetadataServerVersion version = new OpenMetadataServerVersion();
try {
InputStream fileInput = VersionUtils.class.getResourceAsStream(resourceName);
Properties props = new Properties();
props.load(fileInput);
version.setVersion(props.getProperty("version", "unknown"));
version.setRevision(props.getProperty("revision", "unknown"));
String timestampAsString = props.getProperty("timestamp");
Long timestamp = timestampAsString != null ? Long.valueOf(timestampAsString) : null;
version.setTimestamp(timestamp);
} catch (Exception ie) {
LOG.warn("Failed to read catalog version file");
}
return version;
}
public static String[] getVersionFromString(String input) {
return input.split(Pattern.quote("."));
}
/**
* Extracts the {@code MAJOR.MINOR} portion of a version string, ignoring the patch level and any
* qualifier such as {@code -SNAPSHOT}. Two versions sharing the same major/minor differ only by a
* patch release (e.g. {@code 1.12.8} and {@code 1.12.9} both yield {@code 1.12}).
*/
public static String getMajorMinorVersion(String version) {
String majorMinor = version;
if (version != null) {
String[] parts = getVersionFromString(version);
if (parts.length >= 2) {
majorMinor = parts[0] + "." + parts[1];
}
}
return majorMinor;
}
}
@@ -0,0 +1,149 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.sdk;
import jakarta.ws.rs.core.Response;
import java.net.URL;
import java.util.List;
import java.util.Map;
import org.openmetadata.schema.ServiceEntityInterface;
import org.openmetadata.schema.entity.app.App;
import org.openmetadata.schema.entity.app.AppMarketPlaceDefinition;
import org.openmetadata.schema.entity.automations.Workflow;
import org.openmetadata.schema.entity.services.ingestionPipelines.IngestionPipeline;
import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineServiceClientResponse;
import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineStatus;
import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineType;
/**
* Client to make API calls to add, deleted, and deploy pipelines on a PipelineService, such as
* Airflow. Core abstractions are as follows:
*
* <ul>
* <li>A PipelineService is a service such as AirFlow to which a pipeline can be deployed
* <li>A Pipeline is a workflow for performing certain tasks. Example - ingestion pipeline is a
* workflow that connects to a database service or other services and collect metadata.
* <li>Pipeline uses `Connection` to a service as dependency. A Pipeline might need to connection
* to database service to collect metadata, OpenMetadata to user metadata over APIs, etc.
* </ul>
*/
public interface PipelineServiceClientInterface {
String HEALTHY_STATUS = "healthy";
String UNHEALTHY_STATUS = "unhealthy";
String STATUS_KEY = "status";
String APP_TRIGGER = "run_application";
String DEPLOYMENT_ERROR = "DEPLOYMENT_ERROR";
String TRIGGER_ERROR = "TRIGGER_ERROR";
Map<String, String> TYPE_TO_TASK =
Map.of(
PipelineType.METADATA.toString(),
"ingestion_task",
PipelineType.PROFILER.toString(),
"profiler_task",
PipelineType.AUTO_CLASSIFICATION.toString(),
"auto_classification_task",
PipelineType.LINEAGE.toString(),
"lineage_task",
PipelineType.DBT.toString(),
"dbt_task",
PipelineType.USAGE.toString(),
"usage_task",
PipelineType.TEST_SUITE.toString(),
"test_suite_task",
PipelineType.DATA_INSIGHT.toString(),
"data_insight_task",
PipelineType.APPLICATION.toString(),
"application_task");
URL validateServiceURL(String serviceURL);
String getBasicAuthenticationHeader(String username, String password);
Boolean validServerClientVersions(String clientVersion, String serverVersion);
Response getHostIp();
/**
* Check the pipeline service status with an exception backoff to make sure we don't raise any
* false positives.
*/
String getServiceStatusBackoff();
/* Check the status of pipeline service to ensure it is healthy */
PipelineServiceClientResponse getServiceStatus();
List<PipelineStatus> getQueuedPipelineStatus(IngestionPipeline ingestionPipeline);
/**
* This workflow can be used to execute any necessary async automations from the pipeline service.
* This will be the new Test Connection endpoint. The UI can create a new workflow and trigger it
* in the server, and keep polling the results.
*/
PipelineServiceClientResponse runAutomationsWorkflow(Workflow workflow);
PipelineServiceClientResponse runApplicationFlow(App application);
PipelineServiceClientResponse validateAppRegistration(AppMarketPlaceDefinition app);
/* Deploy a pipeline to the pipeline service */
PipelineServiceClientResponse deployPipeline(
IngestionPipeline ingestionPipeline, ServiceEntityInterface service);
/* Deploy run the pipeline at the pipeline service */
PipelineServiceClientResponse runPipeline(
IngestionPipeline ingestionPipeline, ServiceEntityInterface service);
/* Deploy run the pipeline at the pipeline service with ad-hoc custom configuration.
* This might not be supported by some pipeline service clients.*/
default PipelineServiceClientResponse runPipeline(
IngestionPipeline ingestionPipeline,
ServiceEntityInterface service,
Map<String, Object> config) {
throw new UnsupportedOperationException(
"This operation is not supported by this pipeline service");
}
/* Stop and delete a pipeline at the pipeline service */
PipelineServiceClientResponse deletePipeline(IngestionPipeline ingestionPipeline);
/* Get the status of a deployed pipeline */
List<PipelineStatus> getQueuedPipelineStatusInternal(IngestionPipeline ingestionPipeline);
/* Toggle the state of an Ingestion Pipeline as enabled/disabled */
PipelineServiceClientResponse toggleIngestion(IngestionPipeline ingestionPipeline);
/* Get the all last run logs of a deployed pipeline */
Map<String, String> getLastIngestionLogs(IngestionPipeline ingestionPipeline, String after);
/* Get logs for a specific pipeline run identified by runId.
* When runId is null or blank, falls back to getLastIngestionLogs (latest run). */
default Map<String, String> getIngestionLogs(
IngestionPipeline ingestionPipeline, String after, String runId) {
return getLastIngestionLogs(ingestionPipeline, after);
}
/* Get the all last run logs of a deployed pipeline */
PipelineServiceClientResponse killIngestion(IngestionPipeline ingestionPipeline);
/* Stop a specific run of a deployed pipeline identified by its run ID.
* Default is a no-op: clients that do not support per-run stopping return success without
* taking any action. The DB status is already marked STOPPED before this is called. */
default PipelineServiceClientResponse killIngestionRun(
IngestionPipeline ingestionPipeline, String runId) {
return new PipelineServiceClientResponse().withCode(200).withPlatform(getPlatform());
}
String getPlatform();
}
@@ -0,0 +1,30 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class AssetServiceException extends WebServiceException {
private static final String BY_NAME_MESSAGE = "AssetService Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "ASSET_SERVICE_ERROR";
public AssetServiceException(String message) {
super(Response.Status.BAD_REQUEST, ERROR_TYPE, message);
}
public AssetServiceException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static AssetServiceException byMessage(
String name, String errorMessage, Response.Status status) {
return new AssetServiceException(status, buildMessageByName(name, errorMessage));
}
public static AssetServiceException byMessage(String name, String errorMessage) {
return new AssetServiceException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,30 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class AttachmentException extends WebServiceException {
private static final String BY_NAME_MESSAGE = "Attachment Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "ATTACHMENT_ERROR";
public AttachmentException(String message) {
super(Response.Status.BAD_REQUEST, ERROR_TYPE, message);
}
public AttachmentException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static AttachmentException byMessage(
String name, String errorMessage, Response.Status status) {
return new AttachmentException(status, buildMessageByName(name, errorMessage));
}
public static AttachmentException byMessage(String name, String errorMessage) {
return new AttachmentException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,30 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class CSVExportException extends WebServiceException {
private static final String BY_NAME_MESSAGE = "CSVExport Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "CSV_EXPORT_ERROR";
public CSVExportException(String message) {
super(Response.Status.BAD_REQUEST, ERROR_TYPE, message);
}
public CSVExportException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static CSVExportException byMessage(
String name, String errorMessage, Response.Status status) {
return new CSVExportException(status, buildMessageByName(name, errorMessage));
}
public static CSVExportException byMessage(String name, String errorMessage) {
return new CSVExportException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,30 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class EntitySpecViolationException extends WebServiceException {
private static final String BY_NAME_MESSAGE = "Entity Spec Violation [%s] due to [%s].";
private static final String ERROR_TYPE = "ENTITY_SPEC_VIOLATION";
public EntitySpecViolationException(String message) {
super(Response.Status.BAD_REQUEST, ERROR_TYPE, message);
}
public EntitySpecViolationException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static EntitySpecViolationException byMessage(
String name, String errorMessage, Response.Status status) {
return new EntitySpecViolationException(status, buildMessageByName(name, errorMessage));
}
public static EntitySpecViolationException byMessage(String name, String errorMessage) {
return new EntitySpecViolationException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,30 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class EntityUpdateException extends WebServiceException {
private static final String BY_NAME_MESSAGE = "Entity Update Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "ENTITY_UPDATE_EXCEPTION";
public EntityUpdateException(String message) {
super(Response.Status.BAD_REQUEST, ERROR_TYPE, message);
}
public EntityUpdateException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static EntityUpdateException byMessage(
String name, String errorMessage, Response.Status status) {
return new EntityUpdateException(status, buildMessageByName(name, errorMessage));
}
public static EntityUpdateException byMessage(String name, String errorMessage) {
return new EntityUpdateException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class PipelineServiceClientException extends WebServiceException {
private static final String BY_NAME_MESSAGE = "Pipeline Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "PIPELINE_SERVICE_ERROR";
public PipelineServiceClientException(String message) {
super(Response.Status.BAD_REQUEST, ERROR_TYPE, message);
}
private PipelineServiceClientException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static PipelineServiceClientException byMessage(
String name, String errorMessage, Response.Status status) {
return new PipelineServiceClientException(status, buildMessageByName(name, errorMessage));
}
public static PipelineServiceClientException byMessage(String name, String errorMessage) {
return new PipelineServiceClientException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class PipelineServiceVersionException extends WebServiceException {
private static final String BY_NAME_MESSAGE =
"Pipeline Service [%s] Version mismatch due to [%s].";
private static final String ERROR_TYPE = "PIPELINE_SERVICE_VERSION_MISMATCH";
public PipelineServiceVersionException(String message) {
super(Response.Status.INTERNAL_SERVER_ERROR, ERROR_TYPE, message);
}
private PipelineServiceVersionException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static PipelineServiceVersionException byMessage(
String name, String errorMessage, Response.Status status) {
return new PipelineServiceVersionException(status, buildMessageByName(name, errorMessage));
}
public static PipelineServiceVersionException byMessage(String name, String errorMessage) {
return new PipelineServiceVersionException(
Response.Status.INTERNAL_SERVER_ERROR, buildMessageByName(name, errorMessage));
}
public static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,24 @@
package org.openmetadata.sdk.exception;
import lombok.Getter;
@Getter
public class SchemaProcessingException extends Exception {
public enum ErrorType {
RESOURCE_NOT_FOUND,
UNSUPPORTED_URL,
OTHER
}
private final ErrorType errorType;
public SchemaProcessingException(String message, ErrorType errorType) {
super(message);
this.errorType = errorType;
}
public SchemaProcessingException(String message, Throwable cause, ErrorType errorType) {
super(message, cause);
this.errorType = errorType;
}
}
@@ -0,0 +1,30 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class SearchException extends WebServiceException {
private static final String BY_NAME_MESSAGE =
"Search Index Not Found Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "SEARCH_ERROR";
public SearchException(String message) {
super(Response.Status.INTERNAL_SERVER_ERROR, ERROR_TYPE, message);
}
private SearchException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static SearchException byMessage(
String name, String errorMessage, Response.Status status) {
return new SearchException(status, buildMessageByName(name, errorMessage));
}
public static SearchException byMessage(String name, String errorMessage) {
return new SearchException(Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,31 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class SearchIndexNotFoundException extends WebServiceException {
private static final String BY_NAME_MESSAGE =
"Search Index Not Found Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "SEARCH_INDEX_NOT_FOUND";
public SearchIndexNotFoundException(String message) {
super(Response.Status.INTERNAL_SERVER_ERROR, ERROR_TYPE, message);
}
private SearchIndexNotFoundException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public static SearchIndexNotFoundException byMessage(
String name, String errorMessage, Response.Status status) {
return new SearchIndexNotFoundException(status, buildMessageByName(name, errorMessage));
}
public static SearchIndexNotFoundException byMessage(String name, String errorMessage) {
return new SearchIndexNotFoundException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,40 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class SuggestionException extends WebServiceException {
private static final String BY_NAME_MESSAGE =
"Search Index Not Found Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "SUGGESTION_EXCEPTION";
public SuggestionException(String message) {
super(Response.Status.INTERNAL_SERVER_ERROR, ERROR_TYPE, message);
}
private SuggestionException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public SuggestionException(Response.Status status, String errorType, String message) {
super(status, errorType, message);
}
public static SuggestionException byMessage(
String name, String errorMessage, Response.Status status) {
return new SuggestionException(status, buildMessageByName(name, errorMessage));
}
public static SuggestionException byMessage(
String name, String errorType, String errorMessage, Response.Status status) {
return new SuggestionException(status, errorType, buildMessageByName(name, errorMessage));
}
public static SuggestionException byMessage(String name, String errorMessage) {
return new SuggestionException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,39 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class UserCreationException extends WebServiceException {
private static final String BY_NAME_MESSAGE = "User Creation Exception [%s] due to [%s].";
private static final String ERROR_TYPE = "USER_CREATION_EXCEPTION";
public UserCreationException(String message) {
super(Response.Status.INTERNAL_SERVER_ERROR, ERROR_TYPE, message);
}
private UserCreationException(Response.Status status, String message) {
super(status, ERROR_TYPE, message);
}
public UserCreationException(Response.Status status, String errorType, String message) {
super(status, errorType, message);
}
public static UserCreationException byMessage(
String name, String errorMessage, Response.Status status) {
return new UserCreationException(status, buildMessageByName(name, errorMessage));
}
public static UserCreationException byMessage(
String name, String errorType, String errorMessage, Response.Status status) {
return new UserCreationException(status, errorType, buildMessageByName(name, errorMessage));
}
public static UserCreationException byMessage(String name, String errorMessage) {
return new UserCreationException(
Response.Status.BAD_REQUEST, buildMessageByName(name, errorMessage));
}
private static String buildMessageByName(String name, String errorMessage) {
return String.format(BY_NAME_MESSAGE, name, errorMessage);
}
}
@@ -0,0 +1,55 @@
package org.openmetadata.sdk.exception;
import jakarta.ws.rs.core.Response;
public class WebServiceException extends RuntimeException {
private final Response.Status status;
private final String errorType;
private final String message;
public WebServiceException(Response.Status status, String errorType, String message) {
super(message);
this.status = status;
this.errorType = errorType;
this.message = message;
}
public WebServiceException(
Response.Status status, String errorType, String message, Throwable cause) {
super(message, cause);
this.status = status;
this.errorType = errorType;
this.message = message;
}
public Response getResponse() {
return Response.status(status)
.type(jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE)
.entity(new ErrorMessage(status.getStatusCode(), errorType, message))
.build();
}
public static class ErrorMessage {
private final int code;
private final String errorType;
private final String message;
public ErrorMessage(int code, String errorType, String message) {
this.code = code;
this.errorType = errorType;
this.message = message;
}
public int getCode() {
return code;
}
public String getErrorType() {
return errorType;
}
public String getMessage() {
return message;
}
}
}
@@ -0,0 +1,54 @@
package org.openmetadata.search;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.jackson.Jacksonized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Jacksonized
@Getter
@Builder
public class IndexMapping {
private static final Logger LOG = LoggerFactory.getLogger(IndexMapping.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Getter String indexName;
String indexMappingFile;
String alias;
List<String> parentAliases;
List<String> childAliases;
public static final String INDEX_NAME_SEPARATOR = "_";
private static final String DATA_ASSET_ALIAS = "dataAsset";
public String getIndexName(String clusterAlias) {
return clusterAlias != null && !clusterAlias.isEmpty()
? clusterAlias + INDEX_NAME_SEPARATOR + indexName
: indexName;
}
public String getAlias(String clusterAlias) {
return clusterAlias != null && !clusterAlias.isEmpty()
? clusterAlias + INDEX_NAME_SEPARATOR + alias
: alias;
}
public List<String> getParentAliases(String clusterAlias) {
return clusterAlias != null && !clusterAlias.isEmpty()
? parentAliases.stream().map(a -> clusterAlias + INDEX_NAME_SEPARATOR + a).toList()
: parentAliases;
}
public List<String> getChildAliases(String clusterAlias) {
return clusterAlias != null && !clusterAlias.isEmpty()
? childAliases.stream().map(a -> clusterAlias + INDEX_NAME_SEPARATOR + a).toList()
: childAliases;
}
public String getIndexMappingFile(String language) {
return String.format(indexMappingFile, language).replaceFirst("^/", "");
}
}
@@ -0,0 +1,139 @@
package org.openmetadata.search;
import jakarta.json.JsonObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import lombok.Getter;
import org.openmetadata.schema.exception.JsonParsingException;
import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration;
import org.openmetadata.schema.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Getter
public class IndexMappingLoader {
private static volatile IndexMappingLoader instance;
private ElasticSearchConfiguration elasticSearchConfiguration;
private String searchIndexMappingLanguage;
private static final String OM_INDEX_MAPPING_FILE_PATH = "elasticsearch/indexMapping.json";
private static final String COLLATE_INDEX_MAPPING_FILE_PATH =
"elasticsearch/collate/indexMapping.json";
private static final Logger LOG = LoggerFactory.getLogger(IndexMappingLoader.class);
@Getter Map<String, IndexMapping> indexMapping = new HashMap<>();
@Getter Map<String, Map<String, Object>> entityIndexMapping = new HashMap<>();
private IndexMappingLoader(ElasticSearchConfiguration elasticSearchConfiguration)
throws IOException {
this.elasticSearchConfiguration = elasticSearchConfiguration;
if (elasticSearchConfiguration.getSearchIndexMappingLanguage() == null) {
this.searchIndexMappingLanguage = "en";
} else {
this.searchIndexMappingLanguage =
elasticSearchConfiguration.getSearchIndexMappingLanguage().toString().toLowerCase();
}
loadIndexMapping();
loadEntityIndexMapping();
}
private IndexMappingLoader() throws IOException {
this.searchIndexMappingLanguage = "en";
loadIndexMapping();
loadEntityIndexMapping();
}
public static void init(ElasticSearchConfiguration elasticSearchConfiguration)
throws IOException {
synchronized (IndexMappingLoader.class) {
if (instance == null) {
instance = new IndexMappingLoader(elasticSearchConfiguration);
}
}
}
public static void init() throws IOException {
synchronized (IndexMappingLoader.class) {
if (instance == null) {
instance = new IndexMappingLoader();
}
}
}
public static IndexMappingLoader getInstance() {
IndexMappingLoader result = instance;
if (result != null) {
return result;
} else {
throw new IllegalStateException("IndexMappingLoader is not initialized. Call init() first.");
}
}
private void loadIndexMapping() throws IOException {
Set<String> entities;
try (InputStream inputStream =
IndexMappingLoader.class.getClassLoader().getResourceAsStream(OM_INDEX_MAPPING_FILE_PATH)) {
if (inputStream == null) {
throw new IOException("Could not find " + OM_INDEX_MAPPING_FILE_PATH + " in classpath");
}
JsonObject jsonPayload =
JsonUtils.readJson(new String(inputStream.readAllBytes())).asJsonObject();
entities = jsonPayload.keySet();
for (String s : entities) {
indexMapping.put(s, JsonUtils.readValue(jsonPayload.get(s).toString(), IndexMapping.class));
}
} catch (Exception e) {
throw new JsonParsingException("Failed to load indexMapping.json", e);
}
try (InputStream inputStream2 =
IndexMappingLoader.class
.getClassLoader()
.getResourceAsStream(COLLATE_INDEX_MAPPING_FILE_PATH)) {
if (inputStream2 != null) {
JsonObject jsonPayload =
JsonUtils.readJson(new String(inputStream2.readAllBytes())).asJsonObject();
entities = jsonPayload.keySet();
for (String s : entities) {
indexMapping.put(
s, JsonUtils.readValue(jsonPayload.get(s).toString(), IndexMapping.class));
}
}
} catch (Exception e) {
LOG.warn("Failed to load indexMapping.json");
}
}
private void loadEntityIndexMapping() throws IOException {
if (entityIndexMapping == null) {
throw new IllegalStateException(
"Entity index map is not loaded. Call loadIndexMapping() first.");
}
for (Map.Entry<String, IndexMapping> entry : indexMapping.entrySet()) {
String entityName = entry.getKey();
IndexMapping indexMapping = entry.getValue();
try (InputStream inputStream =
IndexMappingLoader.class
.getClassLoader()
.getResourceAsStream(indexMapping.getIndexMappingFile(searchIndexMappingLanguage))) {
if (inputStream == null) {
throw new IOException(
"Could not find "
+ indexMapping.getIndexMappingFile(searchIndexMappingLanguage)
+ " in classpath");
}
Map<String, Object> jsonMap =
JsonUtils.getMapFromJson(new String(inputStream.readAllBytes()));
entityIndexMapping.put(entityName, jsonMap);
} catch (Exception e) {
throw new JsonParsingException("Failed to load index mapping for " + entityName, e);
}
}
}
}
@@ -0,0 +1,261 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.service.clients.pipeline;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Supplier;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.api.configuration.pipelineServiceClient.PipelineServiceClientConfiguration;
import org.openmetadata.schema.entity.services.ingestionPipelines.IngestionPipeline;
import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineServiceClientResponse;
import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineStatus;
import org.openmetadata.sdk.PipelineServiceClientInterface;
import org.openmetadata.sdk.exception.PipelineServiceClientException;
import org.openmetadata.sdk.exception.PipelineServiceVersionException;
/**
* Client to make API calls to add, deleted, and deploy pipelines on a PipelineService, such as
* Airflow. Core abstractions are as follows:
*
* <ul>
* <li>A PipelineService is a service such as AirFlow to which a pipeline can be deployed
* <li>A Pipeline is a workflow for performing certain tasks. Example - ingestion pipeline is a
* workflow that connects to a database service or other services and collect metadata.
* <li>Pipeline uses `Connection` to a service as dependency. A Pipeline might need to connection
* to database service to collect metadata, OpenMetadata to user metadata over APIs, etc.
* </ul>
*/
@Slf4j
public abstract class PipelineServiceClient implements PipelineServiceClientInterface {
protected final boolean pipelineServiceClientEnabled;
protected final String hostIp;
protected final boolean ingestionIpInfoEnabled;
@Getter @Setter private String platform;
protected static final String AUTH_HEADER = "Authorization";
protected static final String CONTENT_HEADER = "Content-Type";
protected static final String CONTENT_TYPE = "application/json";
private static final Integer MAX_ATTEMPTS = 3;
private static final Integer BACKOFF_TIME_SECONDS = 5;
private static final String DISABLED_STATUS = "disabled";
protected static final String SERVER_VERSION;
static {
String rawServerVersion;
try {
rawServerVersion = getServerVersion();
} catch (IOException e) {
rawServerVersion = "unknown";
}
SERVER_VERSION = rawServerVersion;
}
public PipelineServiceClient(
PipelineServiceClientConfiguration pipelineServiceClientConfiguration) {
this.pipelineServiceClientEnabled = pipelineServiceClientConfiguration.getEnabled();
this.hostIp = pipelineServiceClientConfiguration.getHostIp();
this.ingestionIpInfoEnabled = pipelineServiceClientConfiguration.getIngestionIpInfoEnabled();
}
public final URL validateServiceURL(String serviceURL) {
try {
return new URL(serviceURL);
} catch (MalformedURLException e) {
throw new PipelineServiceClientException(serviceURL + " Malformed.");
}
}
public final String getBasicAuthenticationHeader(String username, String password) {
String valueToEncode = username + ":" + password;
return "Basic " + Base64.getEncoder().encodeToString(valueToEncode.getBytes());
}
public static String getServerVersion() throws IOException {
InputStream fileInput = PipelineServiceClient.class.getResourceAsStream("/catalog/VERSION");
Properties props = new Properties();
if (fileInput != null) {
props.load(fileInput);
}
return props.getProperty("version", "unknown");
}
public final String getVersionFromString(String version) {
if (version != null) {
return Pattern.compile("(\\d+)\\.(\\d+)(?:\\.(\\d+))?")
.matcher(version)
.results()
.map(this::formatVersionMatch)
.findFirst()
.orElseThrow(
() ->
new PipelineServiceVersionException(
String.format("Cannot extract version x.y from %s", version)));
} else {
throw new PipelineServiceVersionException("Received version as null");
}
}
private String formatVersionMatch(MatchResult match) {
String patch = match.group(3) != null ? match.group(3) : "0";
return match.group(1) + "." + match.group(2) + "." + patch;
}
private String getMajorMinorVersion(String fullVersion) {
String[] versionParts = fullVersion.split("\\.");
if (versionParts.length >= 2) {
return versionParts[0] + "." + versionParts[1];
}
return fullVersion;
}
@Override
public final Boolean validServerClientVersions(String clientVersion, String serverVersion) {
String clientFullVersion = getVersionFromString(clientVersion);
String serverFullVersion = getVersionFromString(serverVersion);
return getMajorMinorVersion(clientFullVersion).equals(getMajorMinorVersion(serverFullVersion));
}
public String buildVersionMismatchErrorMessage(String ingestionVersion, String serverVersion) {
if (getVersionFromString(ingestionVersion).compareTo(getVersionFromString(serverVersion)) < 0) {
return String.format(
"Ingestion version [%s] is older than Server Version [%s]. Please upgrade your ingestion client.",
ingestionVersion, serverVersion);
}
return String.format(
"Server version [%s] is older than Ingestion Version [%s]. Please upgrade your server or downgrade the ingestion client.",
serverVersion, ingestionVersion);
}
/** To build the response of getServiceStatus */
protected PipelineServiceClientResponse buildHealthyStatus(String ingestionVersion) {
return new PipelineServiceClientResponse()
.withCode(200)
.withVersion(ingestionVersion)
.withPlatform(this.getPlatform());
}
/** To build the response of getServiceStatus */
protected PipelineServiceClientResponse buildUnhealthyStatus(String reason) {
return new PipelineServiceClientResponse()
.withCode(500)
.withReason(reason)
.withPlatform(this.getPlatform());
}
public final Response getHostIp() {
if (this.ingestionIpInfoEnabled) {
return getHostIpInternal();
}
return Response.status(Response.Status.NO_CONTENT).build();
}
private Response getHostIpInternal() {
Map<String, String> body;
try {
body = CommonUtil.nullOrEmpty(this.hostIp) ? requestGetHostIp() : Map.of("ip", this.hostIp);
return Response.ok(body, MediaType.APPLICATION_JSON_TYPE).build();
} catch (Exception e) {
LOG.error("Failed to get Pipeline Service host IP. {}", e.getMessage());
// We don't want the request to fail for an informative ping
body =
Map.of(
"ip",
"Failed to find the IP of Airflow Container. Please make sure https://api.ipify.org, "
+ "https://api.my-ip.io/ip reachable from your network or that the `hostIp` setting is configured.");
return Response.ok(body, MediaType.APPLICATION_JSON_TYPE).build();
}
}
/**
* Check the pipeline service status with an exception backoff to make sure we don't raise any
* false positives.
*/
public String getServiceStatusBackoff() {
RetryConfig retryConfig =
RetryConfig.<String>custom()
.maxAttempts(MAX_ATTEMPTS)
.waitDuration(Duration.ofMillis(BACKOFF_TIME_SECONDS * 1_000L))
.retryOnResult(response -> !HEALTHY_STATUS.equals(response))
.failAfterMaxAttempts(false)
.build();
Retry retry = Retry.of("getServiceStatus", retryConfig);
Supplier<String> responseSupplier =
() -> {
try {
PipelineServiceClientResponse status = getServiceStatus();
return status.getCode() != 200 ? UNHEALTHY_STATUS : HEALTHY_STATUS;
} catch (Exception e) {
throw new RuntimeException(e);
}
};
return retry.executeSupplier(responseSupplier);
}
/* Check the status of pipeline service to ensure it is healthy */
public PipelineServiceClientResponse getServiceStatus() {
if (pipelineServiceClientEnabled) {
return getServiceStatusInternal();
}
return buildHealthyStatus(DISABLED_STATUS).withPlatform(DISABLED_STATUS);
}
public List<PipelineStatus> getQueuedPipelineStatus(IngestionPipeline ingestionPipeline) {
List<PipelineStatus> result = new ArrayList<>();
if (pipelineServiceClientEnabled) {
try {
List<PipelineStatus> internal = getQueuedPipelineStatusInternal(ingestionPipeline);
if (internal != null) {
result.addAll(internal);
}
} catch (Exception e) {
LOG.warn(
"Failed to fetch queued pipeline status for {}: {}. Returning stored statuses only.",
ingestionPipeline.getFullyQualifiedName(),
e.getMessage());
}
}
return result;
}
protected abstract PipelineServiceClientResponse getServiceStatusInternal();
/* Get the Pipeline Service host IP to whitelist in source systems. Should return a map in the shape "ip: 111.11.11.1" */
protected abstract Map<String, String> requestGetHostIp();
}
@@ -0,0 +1,116 @@
/*
* Copyright 2025 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.service.logstorage;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Interface for pluggable log storage implementations for ingestion pipelines.
* Each implementation should support storing, streaming, and retrieving logs
* for pipeline runs identified by FQN and runId.
*/
public interface LogStorageInterface {
/**
* Initialize the log storage with configuration
* @param config Configuration map specific to the implementation
*/
void initialize(Map<String, Object> config) throws IOException;
/**
* Append log content for a pipeline run
* @param pipelineFQN Fully qualified name of the pipeline
* @param runId Unique run identifier
* @param logContent Log content to append
*/
void appendLogs(String pipelineFQN, UUID runId, String logContent) throws IOException;
/**
* Stream log content for a pipeline run
* @param pipelineFQN Fully qualified name of the pipeline
* @param runId Unique run identifier
* @return InputStream for reading logs
*/
InputStream getLogInputStream(String pipelineFQN, UUID runId) throws IOException;
/**
* Get logs with pagination support
* @param pipelineFQN Fully qualified name of the pipeline
* @param runId Unique run identifier
* @param afterCursor Cursor for pagination (null for beginning)
* @param limit Maximum number of lines to return
* @return Map containing "logs" (content), "after" (next cursor), and "total" (total size)
*/
Map<String, Object> getLogs(String pipelineFQN, UUID runId, String afterCursor, int limit)
throws IOException;
/**
* Get the latest run ID for a pipeline
* @param pipelineFQN Fully qualified name of the pipeline
* @return Latest run ID or null if no runs exist
*/
UUID getLatestRunId(String pipelineFQN) throws IOException;
/**
* List available runs for a pipeline
* @param pipelineFQN Fully qualified name of the pipeline
* @param limit Maximum number of runs to return
* @return List of run IDs sorted by timestamp (newest first)
*/
List<UUID> listRuns(String pipelineFQN, int limit) throws IOException;
/**
* Delete logs for a specific run
* @param pipelineFQN Fully qualified name of the pipeline
* @param runId Unique run identifier
*/
void deleteLogs(String pipelineFQN, UUID runId) throws IOException;
/**
* Delete all logs for a pipeline
* @param pipelineFQN Fully qualified name of the pipeline
*/
void deleteAllLogs(String pipelineFQN) throws IOException;
/**
* Check if logs exist for a run
* @param pipelineFQN Fully qualified name of the pipeline
* @param runId Unique run identifier
* @return true if logs exist
*/
boolean logsExist(String pipelineFQN, UUID runId) throws IOException;
/**
* Get the storage type identifier
* @return Storage type (e.g., "s3", "file", "database")
*/
String getStorageType();
/**
* Close and finalize the log stream for a specific pipeline run.
* This ensures any buffered data is written and the stream is properly closed.
* @param pipelineFQN Fully qualified name of the pipeline
* @param runId Unique run identifier
*/
void closeStream(String pipelineFQN, UUID runId) throws IOException;
/**
* Clean up resources
*/
void close() throws IOException;
}