Skip to content

Commit

Permalink
feat(ui): Support batch deprecation from the UI (Batch actions part 6…
Browse files Browse the repository at this point in the history
…/7) (#5572)
  • Loading branch information
jjoyce0510 authored Aug 4, 2022
1 parent 89dd04e commit da77752
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
import com.linkedin.datahub.graphql.resolvers.mutate.BatchRemoveOwnersResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.BatchRemoveTagsResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.BatchRemoveTermsResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.BatchUpdateDeprecationResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.BatchSetDomainResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.MutableTypeResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.RemoveLinkResolver;
Expand Down Expand Up @@ -723,6 +724,7 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("setDomain", new SetDomainResolver(this.entityClient, this.entityService))
.dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService))
.dataFetcher("updateDeprecation", new UpdateDeprecationResolver(this.entityClient, this.entityService))
.dataFetcher("batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService))
.dataFetcher("unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService))
.dataFetcher("createSecret", new CreateSecretResolver(this.entityClient, this.secretService))
.dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.linkedin.datahub.graphql.resolvers.mutate;

import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.BatchUpdateDeprecationInput;
import com.linkedin.datahub.graphql.generated.ResourceRefInput;
import com.linkedin.datahub.graphql.resolvers.mutate.util.DeprecationUtils;
import com.linkedin.datahub.graphql.resolvers.mutate.util.LabelUtils;
import com.linkedin.metadata.entity.EntityService;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

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


@Slf4j
@RequiredArgsConstructor
public class BatchUpdateDeprecationResolver implements DataFetcher<CompletableFuture<Boolean>> {

private final EntityService _entityService;

@Override
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();
final BatchUpdateDeprecationInput input = bindArgument(environment.getArgument("input"), BatchUpdateDeprecationInput.class);
final List<ResourceRefInput> resources = input.getResources();

return CompletableFuture.supplyAsync(() -> {

// First, validate the resources
validateInputResources(resources, context);

try {
// Then execute the bulk update
batchUpdateDeprecation(input.getDeprecated(), input.getNote(), input.getDecommissionTime(), resources, context);
return true;
} catch (Exception e) {
log.error("Failed to perform update against input {}, {}", input.toString(), e.getMessage());
throw new RuntimeException(String.format("Failed to perform update against input %s", input.toString()), e);
}
});
}

private void validateInputResources(List<ResourceRefInput> resources, QueryContext context) {
for (ResourceRefInput resource : resources) {
validateInputResource(resource, context);
}
}

private void validateInputResource(ResourceRefInput resource, QueryContext context) {
final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn());
if (!DeprecationUtils.isAuthorizedToUpdateDeprecationForEntity(context, resourceUrn)) {
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
}
LabelUtils.validateResource(resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService);
}

private void batchUpdateDeprecation(boolean deprecated,
@Nullable String note,
@Nullable Long decommissionTime,
List<ResourceRefInput> resources,
QueryContext context) {
log.debug("Batch updating deprecation. deprecated: {}, note: {}, decommissionTime: {}, resources: {}", deprecated, note, decommissionTime, resources);
try {
DeprecationUtils.updateDeprecationForResources(
deprecated,
note,
decommissionTime,
resources,
UrnUtils.getUrn(context.getActorUrn()),
_entityService);
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to batch update deprecated to %s for resources with urns %s!",
deprecated,
resources.stream().map(ResourceRefInput::getResourceUrn).collect(Collectors.toList())),
e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.linkedin.datahub.graphql.resolvers.mutate.util;

import com.google.common.collect.ImmutableList;

import com.linkedin.common.Deprecation;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.data.template.SetMode;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.authorization.ConjunctivePrivilegeGroup;
import com.linkedin.datahub.graphql.authorization.DisjunctivePrivilegeGroup;
import com.linkedin.datahub.graphql.generated.ResourceRefInput;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.authorization.PoliciesConfig;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.mxe.MetadataChangeProposal;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*;


@Slf4j
public class DeprecationUtils {
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP = new ConjunctivePrivilegeGroup(ImmutableList.of(
PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()
));

private DeprecationUtils() { }

public static boolean isAuthorizedToUpdateDeprecationForEntity(@Nonnull QueryContext context, Urn entityUrn) {
final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of(
ALL_PRIVILEGES_GROUP,
new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.EDIT_ENTITY_DEPRECATION_PRIVILEGE.getType()))
));

return AuthorizationUtils.isAuthorized(
context.getAuthorizer(),
context.getActorUrn(),
entityUrn.getEntityType(),
entityUrn.toString(),
orPrivilegeGroups);
}

public static void updateDeprecationForResources(
boolean deprecated,
@Nullable String note,
@Nullable Long decommissionTime,
List<ResourceRefInput> resources,
Urn actor,
EntityService entityService
) {
final List<MetadataChangeProposal> changes = new ArrayList<>();
for (ResourceRefInput resource : resources) {
changes.add(buildUpdateDeprecationProposal(deprecated, note, decommissionTime, resource, actor, entityService));
}
ingestChangeProposals(changes, entityService, actor);
}

private static MetadataChangeProposal buildUpdateDeprecationProposal(
boolean deprecated,
@Nullable String note,
@Nullable Long decommissionTime,
ResourceRefInput resource,
Urn actor,
EntityService entityService
) {
Deprecation deprecation = (Deprecation) getAspectFromEntity(
resource.getResourceUrn(),
Constants.DEPRECATION_ASPECT_NAME,
entityService,
new Deprecation());
deprecation.setActor(actor);
deprecation.setDeprecated(deprecated);
deprecation.setDecommissionTime(decommissionTime, SetMode.REMOVE_IF_NULL);
if (note != null) {
deprecation.setNote(note);
} else {
// Note is required field in GMS. Set to empty string if not provided.
deprecation.setNote("");
}
return buildMetadataChangeProposal(UrnUtils.getUrn(resource.getResourceUrn()), Constants.DEPRECATION_ASPECT_NAME, deprecation, actor, entityService);
}

private static void ingestChangeProposals(List<MetadataChangeProposal> changes, EntityService entityService, Urn actor) {
// TODO: Replace this with a batch ingest proposals endpoint.
for (MetadataChangeProposal change : changes) {
entityService.ingestProposal(change, getAuditStamp(actor));
}
}
}
31 changes: 31 additions & 0 deletions datahub-graphql-core/src/main/resources/entity.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,11 @@ type Mutation {
"Input required to set deprecation for an Entity."
input: UpdateDeprecationInput!): Boolean

"""
Updates the deprecation status for a batch of assets.
"""
batchUpdateDeprecation(input: BatchUpdateDeprecationInput!): Boolean

"""
Update a particular Corp User's editable properties
"""
Expand Down Expand Up @@ -6923,6 +6928,32 @@ input UpdateDeprecationInput {
note: String
}


"""
Input provided when updating the deprecation status for a batch of assets.
"""
input BatchUpdateDeprecationInput {
"""
Whether the Entity is marked as deprecated.
"""
deprecated: Boolean!

"""
Optional - The time user plan to decommission this entity
"""
decommissionTime: Long

"""
Optional - Additional information about the entity deprecation plan
"""
note: String

"""
The target assets to attach the tags to
"""
resources: [ResourceRefInput]!
}

"""
Input provided when adding tags to a batch of assets
"""
Expand Down
Loading

0 comments on commit da77752

Please sign in to comment.