Skip to content

Commit

Permalink
feat(Tests): Metadata Tests Models + APIs + UI (Part 1) (#4989)
Browse files Browse the repository at this point in the history
  • Loading branch information
jjoyce0510 authored May 25, 2022
1 parent 8ac2bd6 commit 944be9a
Show file tree
Hide file tree
Showing 44 changed files with 2,247 additions and 11 deletions.
1 change: 1 addition & 0 deletions datahub-graphql-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ graphqlCodegen {
"$projectDir/src/main/resources/ingestion.graphql".toString(),
"$projectDir/src/main/resources/auth.graphql".toString(),
"$projectDir/src/main/resources/timeline.graphql".toString(),
"$projectDir/src/main/resources/tests.graphql".toString(),
]
outputDir = new File("$projectDir/src/mainGeneratedGraphQL/java")
packageName = "com.linkedin.datahub.graphql.generated"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class Constants {
public static final String RECOMMENDATIONS_SCHEMA_FILE = "recommendation.graphql";
public static final String INGESTION_SCHEMA_FILE = "ingestion.graphql";
public static final String TIMELINE_SCHEMA_FILE = "timeline.graphql";
public static final String TESTS_SCHEMA_FILE = "tests.graphql";
public static final String BROWSE_PATH_DELIMITER = "/";
public static final String VERSION_STAMP_FIELD_NAME = "versionStamp";
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.linkedin.datahub.graphql.generated.LineageRelationship;
import com.linkedin.datahub.graphql.generated.ListAccessTokenResult;
import com.linkedin.datahub.graphql.generated.ListDomainsResult;
import com.linkedin.datahub.graphql.generated.ListTestsResult;
import com.linkedin.datahub.graphql.generated.MLFeature;
import com.linkedin.datahub.graphql.generated.MLFeatureProperties;
import com.linkedin.datahub.graphql.generated.MLFeatureTable;
Expand All @@ -56,6 +57,8 @@
import com.linkedin.datahub.graphql.generated.RecommendationContent;
import com.linkedin.datahub.graphql.generated.SearchAcrossLineageResult;
import com.linkedin.datahub.graphql.generated.SearchResult;
import com.linkedin.datahub.graphql.generated.Test;
import com.linkedin.datahub.graphql.generated.TestResult;
import com.linkedin.datahub.graphql.generated.UserUsageCounts;
import com.linkedin.datahub.graphql.generated.VisualConfiguration;
import com.linkedin.datahub.graphql.resolvers.MeResolver;
Expand Down Expand Up @@ -132,6 +135,11 @@
import com.linkedin.datahub.graphql.resolvers.search.SearchAcrossLineageResolver;
import com.linkedin.datahub.graphql.resolvers.search.SearchResolver;
import com.linkedin.datahub.graphql.resolvers.tag.SetTagColorResolver;
import com.linkedin.datahub.graphql.resolvers.test.CreateTestResolver;
import com.linkedin.datahub.graphql.resolvers.test.DeleteTestResolver;
import com.linkedin.datahub.graphql.resolvers.test.ListTestsResolver;
import com.linkedin.datahub.graphql.resolvers.test.TestResultsResolver;
import com.linkedin.datahub.graphql.resolvers.test.UpdateTestResolver;
import com.linkedin.datahub.graphql.resolvers.timeline.GetSchemaBlameResolver;
import com.linkedin.datahub.graphql.resolvers.type.AspectInterfaceTypeResolver;
import com.linkedin.datahub.graphql.resolvers.type.EntityInterfaceTypeResolver;
Expand Down Expand Up @@ -172,6 +180,7 @@
import com.linkedin.datahub.graphql.types.mlmodel.MLPrimaryKeyType;
import com.linkedin.datahub.graphql.types.notebook.NotebookType;
import com.linkedin.datahub.graphql.types.tag.TagType;
import com.linkedin.datahub.graphql.types.test.TestType;
import com.linkedin.datahub.graphql.types.usage.UsageType;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.config.IngestionConfiguration;
Expand Down Expand Up @@ -265,6 +274,7 @@ public class GmsGraphQLEngine {
private final VersionedDatasetType versionedDatasetType;
private final DataPlatformInstanceType dataPlatformInstanceType;
private final AccessTokenMetadataType accessTokenMetadataType;
private final TestType testType;

/**
* Configures the graph objects that can be fetched primary key.
Expand Down Expand Up @@ -357,6 +367,7 @@ public GmsGraphQLEngine(
this.versionedDatasetType = new VersionedDatasetType(entityClient);
this.dataPlatformInstanceType = new DataPlatformInstanceType(entityClient);
this.accessTokenMetadataType = new AccessTokenMetadataType(entityClient);
this.testType = new TestType(entityClient);
// Init Lists
this.entityTypes = ImmutableList.of(
datasetType,
Expand All @@ -380,7 +391,8 @@ public GmsGraphQLEngine(
assertionType,
versionedDatasetType,
dataPlatformInstanceType,
accessTokenMetadataType
accessTokenMetadataType,
testType
);
this.loadableTypes = new ArrayList<>(entityTypes);
this.ownerTypes = ImmutableList.of(corpUserType, corpGroupType);
Expand Down Expand Up @@ -435,6 +447,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) {
configureDataProcessInstanceResolvers(builder);
configureVersionedDatasetResolvers(builder);
configureAccessAccessTokenMetadataResolvers(builder);
configureTestResultResolvers(builder);
}

public GraphQLEngine.Builder builder() {
Expand All @@ -447,6 +460,7 @@ public GraphQLEngine.Builder builder() {
.addSchema(fileBasedSchema(RECOMMENDATIONS_SCHEMA_FILE))
.addSchema(fileBasedSchema(INGESTION_SCHEMA_FILE))
.addSchema(fileBasedSchema(TIMELINE_SCHEMA_FILE))
.addSchema(fileBasedSchema(TESTS_SCHEMA_FILE))
.addDataLoaders(loaderSuppliers(loadableTypes))
.addDataLoader("Aspect", context -> createDataLoader(aspectType, context))
.addDataLoader("UsageQueryResult", context -> createDataLoader(usageType, context))
Expand Down Expand Up @@ -569,6 +583,8 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("ingestionSource", new GetIngestionSourceResolver(this.entityClient))
.dataFetcher("executionRequest", new GetIngestionExecutionRequestResolver(this.entityClient))
.dataFetcher("getSchemaBlame", new GetSchemaBlameResolver(this.timelineService))
.dataFetcher("test", getResolver(testType))
.dataFetcher("listTests", new ListTestsResolver(entityClient))
);
}

Expand Down Expand Up @@ -632,6 +648,9 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("createIngestionExecutionRequest", new CreateIngestionExecutionRequestResolver(this.entityClient, this.ingestionConfiguration))
.dataFetcher("cancelIngestionExecutionRequest", new CancelIngestionExecutionRequestResolver(this.entityClient))
.dataFetcher("deleteAssertion", new DeleteAssertionResolver(this.entityClient, this.entityService))
.dataFetcher("createTest", new CreateTestResolver(this.entityClient))
.dataFetcher("updateTest", new UpdateTestResolver(this.entityClient))
.dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient))
);
}

Expand Down Expand Up @@ -687,6 +706,12 @@ private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder
.type("PolicyMatchCriterionValue", typeWiring -> typeWiring
.dataFetcher("entity", new EntityTypeResolver(entityTypes,
(env) -> ((PolicyMatchCriterionValue) env.getSource()).getEntity()))
)
.type("ListTestsResult", typeWiring -> typeWiring
.dataFetcher("tests", new LoadableTypeBatchResolver<>(testType,
(env) -> ((ListTestsResult) env.getSource()).getTests().stream()
.map(Test::getUrn)
.collect(Collectors.toList())))
);
}

Expand Down Expand Up @@ -740,6 +765,7 @@ private void configureDatasetResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("health", new DatasetHealthResolver(graphClient, timeseriesAspectService))
.dataFetcher("schemaMetadata", new AspectResolver())
.dataFetcher("assertions", new EntityAssertionsResolver(entityClient, graphClient))
.dataFetcher("testResults", new TestResultsResolver(entityClient))
.dataFetcher("aspects", new WeaklyTypedAspectsResolver(entityClient, entityRegistry))
.dataFetcher("subTypes", new SubTypesResolver(
this.entityClient,
Expand Down Expand Up @@ -1262,6 +1288,16 @@ private void configureDataProcessInstanceResolvers(final RuntimeWiring.Builder b
);
}

private void configureTestResultResolvers(final RuntimeWiring.Builder builder) {
builder.type("TestResult", typeWiring -> typeWiring
.dataFetcher("test", new LoadableTypeResolver<>(testType,
(env) -> {
final TestResult testResult = env.getSource();
return testResult.getTest() != null ? testResult.getTest().getUrn() : null;
}))
);
}

private <T, K> DataLoader<K, DataFetcherResult<T>> createDataLoader(final LoadableType<T, K> graphType, final QueryContext queryContext) {
BatchLoaderContextProvider contextProvider = () -> queryContext;
DataLoaderOptions loaderOptions = DataLoaderOptions.newOptions().setBatchLoaderContextProvider(contextProvider);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class EntityTypeMapper {
.put(EntityType.DOMAIN, "domain")
.put(EntityType.NOTEBOOK, "notebook")
.put(EntityType.DATA_PLATFORM_INSTANCE, "dataPlatformInstance")
.put(EntityType.TEST, "test")
.build();

private static final Map<String, EntityType> ENTITY_NAME_TO_TYPE =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public CompletableFuture<AuthenticatedUser> get(DataFetchingEnvironment environm
platformPrivileges.setManageIngestion(canManageIngestion(context));
platformPrivileges.setManageSecrets(canManageSecrets(context));
platformPrivileges.setManageTokens(canManageTokens(context));
platformPrivileges.setManageTests(canManageTests(context));

// Construct and return authenticated user object.
final AuthenticatedUser authUser = new AuthenticatedUser();
Expand Down Expand Up @@ -101,6 +102,12 @@ private boolean canGeneratePersonalAccessToken(final QueryContext context) {
return isAuthorized(context.getAuthorizer(), context.getActorUrn(), PoliciesConfig.GENERATE_PERSONAL_ACCESS_TOKENS_PRIVILEGE);
}

/**
* Returns true if the authenticated user has privileges to manage (add or remove) tests.
*/
private boolean canManageTests(final QueryContext context) {
return isAuthorized(context.getAuthorizer(), context.getActorUrn(), PoliciesConfig.MANAGE_TESTS_PRIVILEGE);
}

/**
* Returns true if the authenticated user has privileges to manage domains
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.linkedin.datahub.graphql.resolvers.test;

import com.linkedin.data.template.SetMode;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateTestInput;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.key.TestKey;
import com.linkedin.metadata.utils.GenericRecordUtils;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.test.TestInfo;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
import static com.linkedin.datahub.graphql.resolvers.test.TestUtils.*;


/**
* Creates or updates a Test. Requires the MANAGE_TESTS privilege.
*/
public class CreateTestResolver implements DataFetcher<CompletableFuture<String>> {

private final EntityClient _entityClient;

public CreateTestResolver(final EntityClient entityClient) {
_entityClient = entityClient;
}

@Override
public CompletableFuture<String> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();

return CompletableFuture.supplyAsync(() -> {

if (canManageTests(context)) {

final CreateTestInput input = bindArgument(environment.getArgument("input"), CreateTestInput.class);
final MetadataChangeProposal proposal = new MetadataChangeProposal();

// Create new test
// Since we are creating a new Test, we need to generate a unique UUID.
final UUID uuid = UUID.randomUUID();
final String uuidStr = input.getId() == null ? uuid.toString() : input.getId();

// Create the Ingestion source key
final TestKey key = new TestKey();
key.setId(uuidStr);
proposal.setEntityKeyAspect(GenericRecordUtils.serializeAspect(key));

// Create the Test info.
final TestInfo info = mapCreateTestInput(input);
proposal.setEntityType(Constants.TEST_ENTITY_NAME);
proposal.setAspectName(Constants.TEST_INFO_ASPECT_NAME);
proposal.setAspect(GenericRecordUtils.serializeAspect(info));
proposal.setChangeType(ChangeType.UPSERT);

try {
return _entityClient.ingestProposal(proposal, context.getAuthentication());
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to perform update against Test with urn %s", input.toString()), e);
}
}
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
});
}

private static TestInfo mapCreateTestInput(final CreateTestInput input) {
final TestInfo result = new TestInfo();
result.setName(input.getName());
result.setCategory(input.getCategory());
result.setDescription(input.getDescription(), SetMode.IGNORE_NULL);
result.setDefinition(mapDefinition(input.getDefinition()));
return result;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.linkedin.datahub.graphql.resolvers.test;

import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.entity.client.EntityClient;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import static com.linkedin.datahub.graphql.resolvers.test.TestUtils.*;


/**
* Resolver responsible for hard deleting a particular DataHub Test. Requires MANAGE_TESTS
* privilege.
*/
public class DeleteTestResolver implements DataFetcher<CompletableFuture<Boolean>> {

private final EntityClient _entityClient;

public DeleteTestResolver(final EntityClient entityClient) {
_entityClient = entityClient;
}

@Override
public CompletableFuture<Boolean> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();
final String testUrn = environment.getArgument("urn");
final Urn urn = Urn.createFromString(testUrn);
return CompletableFuture.supplyAsync(() -> {
if (canManageTests(context)) {
try {
_entityClient.deleteEntity(urn, context.getAuthentication());
return true;
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to perform delete against Test with urn %s", testUrn), e);
}
}
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.linkedin.datahub.graphql.resolvers.test;

import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.Test;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.ListTestsInput;
import com.linkedin.datahub.graphql.generated.ListTestsResult;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.search.SearchEntity;
import com.linkedin.metadata.search.SearchEntityArray;
import com.linkedin.metadata.search.SearchResult;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
import static com.linkedin.datahub.graphql.resolvers.test.TestUtils.*;


/**
* Resolver used for listing all Tests defined within DataHub. Requires the MANAGE_DOMAINS platform privilege.
*/
public class ListTestsResolver implements DataFetcher<CompletableFuture<ListTestsResult>> {

private static final Integer DEFAULT_START = 0;
private static final Integer DEFAULT_COUNT = 20;

private final EntityClient _entityClient;

public ListTestsResolver(final EntityClient entityClient) {
_entityClient = entityClient;
}

@Override
public CompletableFuture<ListTestsResult> get(final DataFetchingEnvironment environment) throws Exception {

final QueryContext context = environment.getContext();

return CompletableFuture.supplyAsync(() -> {

if (canManageTests(context)) {
final ListTestsInput input = bindArgument(environment.getArgument("input"), ListTestsInput.class);
final Integer start = input.getStart() == null ? DEFAULT_START : input.getStart();
final Integer count = input.getCount() == null ? DEFAULT_COUNT : input.getCount();
final String query = input.getQuery() == null ? "" : input.getQuery();

try {
// First, get all group Urns.
final SearchResult gmsResult = _entityClient.search(
Constants.TEST_ENTITY_NAME,
query,
Collections.emptyMap(),
start,
count,
context.getAuthentication());

// Now that we have entities we can bind this to a result.
final ListTestsResult result = new ListTestsResult();
result.setStart(gmsResult.getFrom());
result.setCount(gmsResult.getPageSize());
result.setTotal(gmsResult.getNumEntities());
result.setTests(mapUnresolvedTests(gmsResult.getEntities()));
return result;
} catch (Exception e) {
throw new RuntimeException("Failed to list tests", e);
}
}
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
});
}

// This method maps urns returned from the list endpoint into Partial Test objects which will be resolved be a separate Batch resolver.
private List<Test> mapUnresolvedTests(final SearchEntityArray entityArray) {
final List<Test> results = new ArrayList<>();
for (final SearchEntity entity : entityArray) {
final Urn urn = entity.getEntity();
final Test unresolvedTest = new Test();
unresolvedTest.setUrn(urn.toString());
unresolvedTest.setType(EntityType.TEST);
results.add(unresolvedTest);
}
return results;
}
}
Loading

0 comments on commit 944be9a

Please sign in to comment.