All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openmetadata.service.resources.apps.AppResource Maven / Gradle / Ivy

There is a newer version: 1.5.11
Show newest version
package org.openmetadata.service.resources.apps;

import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.schema.type.Include.ALL;
import static org.openmetadata.service.Entity.APPLICATION;
import static org.openmetadata.service.Entity.BOT;
import static org.openmetadata.service.Entity.FIELD_OWNER;
import static org.openmetadata.service.jdbi3.EntityRepository.getEntitiesFromSeedData;

import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.ServiceEntityInterface;
import org.openmetadata.schema.api.data.RestoreEntity;
import org.openmetadata.schema.entity.app.App;
import org.openmetadata.schema.entity.app.AppMarketPlaceDefinition;
import org.openmetadata.schema.entity.app.AppRunRecord;
import org.openmetadata.schema.entity.app.AppType;
import org.openmetadata.schema.entity.app.CreateApp;
import org.openmetadata.schema.entity.app.ScheduleType;
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.services.connections.metadata.OpenMetadataConnection;
import org.openmetadata.schema.type.EntityHistory;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.MetadataOperation;
import org.openmetadata.sdk.PipelineServiceClient;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.apps.ApplicationHandler;
import org.openmetadata.service.apps.scheduler.AppScheduler;
import org.openmetadata.service.clients.pipeline.PipelineServiceClientFactory;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.exception.EntityNotFoundException;
import org.openmetadata.service.jdbi3.AppRepository;
import org.openmetadata.service.jdbi3.CollectionDAO;
import org.openmetadata.service.jdbi3.IngestionPipelineRepository;
import org.openmetadata.service.jdbi3.ListFilter;
import org.openmetadata.service.resources.Collection;
import org.openmetadata.service.resources.EntityResource;
import org.openmetadata.service.search.SearchRepository;
import org.openmetadata.service.secrets.SecretsManager;
import org.openmetadata.service.secrets.SecretsManagerFactory;
import org.openmetadata.service.secrets.masker.EntityMaskerFactory;
import org.openmetadata.service.security.AuthorizationException;
import org.openmetadata.service.security.Authorizer;
import org.openmetadata.service.security.policyevaluator.OperationContext;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.OpenMetadataConnectionBuilder;
import org.openmetadata.service.util.ResultList;
import org.quartz.SchedulerException;

@Path("/v1/apps")
@Tag(
    name = "Apps",
    description = "Apps are internal/external apps used to something on top of Open-metadata.")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "apps")
@Slf4j
public class AppResource extends EntityResource {
  public static final String COLLECTION_PATH = "v1/apps/";
  private OpenMetadataApplicationConfig openMetadataApplicationConfig;
  private PipelineServiceClient pipelineServiceClient;
  static final String FIELDS = "owner";
  private SearchRepository searchRepository;

  @Override
  public void initialize(OpenMetadataApplicationConfig config) {
    try {
      this.openMetadataApplicationConfig = config;
      this.pipelineServiceClient =
          PipelineServiceClientFactory.createPipelineServiceClient(
              config.getPipelineServiceClientConfiguration());

      // Create an On Demand DAO
      CollectionDAO dao = Entity.getCollectionDAO();
      searchRepository = new SearchRepository(config.getElasticSearchConfiguration());
      AppScheduler.initialize(config, dao, searchRepository);

      // Initialize Default Apps
      List createAppsReq =
          getEntitiesFromSeedData(
              APPLICATION, String.format(".*json/data/%s/.*\\.json$", entityType), CreateApp.class);
      loadDefaultApplications(createAppsReq);
    } catch (Exception ex) {
      LOG.error("Failed in Create App Requests", ex);
    }
  }

  private void loadDefaultApplications(List defaultAppCreateRequests) {
    // Get Create App Requests
    for (CreateApp createApp : defaultAppCreateRequests) {
      try {
        AppMarketPlaceDefinition definition =
            repository
                .getMarketPlace()
                .getByName(
                    null,
                    createApp.getName(),
                    new EntityUtil.Fields(repository.getMarketPlace().getAllowedFields()));
        App app = getAppForInit(createApp.getName());
        if (app == null) {
          app =
              getApplication(definition, createApp, "admin")
                  .withFullyQualifiedName(createApp.getName());
          repository.initializeEntity(app);
        }

        // Schedule
        if (app.getScheduleType().equals(ScheduleType.Scheduled)) {
          ApplicationHandler.getInstance()
              .installApplication(app, Entity.getCollectionDAO(), searchRepository);
        }
      } catch (Exception ex) {
        LOG.error("Failed in Creation/Initialization of Application : {}", createApp.getName(), ex);
      }
    }
  }

  private App getAppForInit(String appName) {
    try {
      return repository.getByName(null, appName, repository.getFields("bot,pipelines"), ALL, false);
    } catch (EntityNotFoundException ex) {
      return null;
    }
  }

  public AppResource(Authorizer authorizer) {
    super(Entity.APPLICATION, authorizer);
  }

  public static class AppList extends ResultList {
    /* Required for serde */
  }

  public static class AppRunList extends ResultList {
    /* Required for serde */
  }

  /**
   * We don't want to store runtime information into the DB
   */
  private void unsetAppRuntimeProperties(App app) {
    app.setOpenMetadataServerConnection(null);
    app.setPrivateConfiguration(null);
  }

  @GET
  @Operation(
      operationId = "listInstalledApplications",
      summary = "List installed application",
      description =
          "Get a list of applications. Use `fields` "
              + "parameter to get only necessary fields. Use cursor-based pagination to limit the number "
              + "entries in the list using `limit` and `before` or `after` query params.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "List of Installed Applications",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = AppList.class)))
      })
  public ResultList list(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(
              description = "Fields requested in the returned resource",
              schema = @Schema(type = "string", example = FIELDS))
          @QueryParam("fields")
          String fieldsParam,
      @Parameter(
              description =
                  "Limit the number of installed applications returned. (1 to 1000000, default = 10)")
          @DefaultValue("10")
          @QueryParam("limit")
          @Min(0)
          @Max(1000000)
          int limitParam,
      @Parameter(
              description = "Returns list of tests before this cursor",
              schema = @Schema(type = "string"))
          @QueryParam("before")
          String before,
      @Parameter(
              description = "Returns list of tests after this cursor",
              schema = @Schema(type = "string"))
          @QueryParam("after")
          String after,
      @Parameter(
              description = "Include all, deleted, or non-deleted entities.",
              schema = @Schema(implementation = Include.class))
          @QueryParam("include")
          @DefaultValue("non-deleted")
          Include include) {
    ListFilter filter = new ListFilter(include);
    return super.listInternal(
        uriInfo, securityContext, fieldsParam, filter, limitParam, before, after);
  }

  @GET
  @Path("/name/{name}/status")
  @Operation(
      operationId = "listAppRunRecords",
      summary = "List App Run Records",
      description =
          "Get a list of applications Run Record."
              + " Use cursor-based pagination to limit the number "
              + "entries in the list using `offset` query params.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "List of Installed Applications Runs",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = AppRunList.class)))
      })
  public Response listAppRuns(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name,
      @Parameter(description = "Limit records. (1 to 1000000, default = 10)")
          @DefaultValue("10")
          @QueryParam("limit")
          @Min(0)
          @Max(1000000)
          int limitParam,
      @Parameter(description = "Offset records. (0 to 1000000, default = 0)")
          @DefaultValue("0")
          @QueryParam("offset")
          @Min(0)
          @Max(1000000)
          int offset,
      @Parameter(
              description = "Filter pipeline status after the given start timestamp",
              schema = @Schema(type = "number"))
          @QueryParam("startTs")
          Long startTs,
      @Parameter(
              description = "Filter pipeline status before the given end timestamp",
              schema = @Schema(type = "number"))
          @QueryParam("endTs")
          Long endTs) {
    App installation = repository.getByName(uriInfo, name, repository.getFields("id,pipelines"));
    if (installation.getAppType().equals(AppType.Internal)) {
      return Response.status(Response.Status.OK)
          .entity(repository.listAppRuns(installation.getId(), limitParam, offset))
          .build();
    }
    if (!installation.getPipelines().isEmpty()) {
      EntityReference pipelineRef = installation.getPipelines().get(0);
      IngestionPipelineRepository ingestionPipelineRepository =
          (IngestionPipelineRepository) Entity.getEntityRepository(Entity.INGESTION_PIPELINE);
      IngestionPipeline ingestionPipeline =
          ingestionPipelineRepository.get(
              uriInfo, pipelineRef.getId(), ingestionPipelineRepository.getFields(FIELD_OWNER));
      return Response.ok(
              ingestionPipelineRepository.listPipelineStatus(
                  ingestionPipeline.getFullyQualifiedName(), startTs, endTs),
              MediaType.APPLICATION_JSON_TYPE)
          .build();
    }
    throw new IllegalArgumentException("App does not have an associated pipeline.");
  }

  @GET
  @Path("/name/{name}/logs")
  @Operation(
      summary = "Retrieve all logs from last ingestion pipeline run for the application",
      description = "Get all logs from last ingestion pipeline run by `Id`.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description =
                "JSON object with the task instance name of the ingestion on each key and log in the value",
            content = @Content(mediaType = "application/json")),
        @ApiResponse(responseCode = "404", description = "Logs for instance {id} is not found")
      })
  public Response getLastLogs(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name,
      @Parameter(
              description = "Returns log chunk after this cursor",
              schema = @Schema(type = "string"))
          @QueryParam("after")
          @DefaultValue("")
          String after) {
    App installation = repository.getByName(uriInfo, name, repository.getFields("id,pipelines"));
    if (installation.getAppType().equals(AppType.Internal)) {
      return Response.status(Response.Status.OK)
          .entity(repository.getLatestAppRuns(installation.getId()))
          .build();
    } else {
      if (!installation.getPipelines().isEmpty()) {
        EntityReference pipelineRef = installation.getPipelines().get(0);
        IngestionPipelineRepository ingestionPipelineRepository =
            (IngestionPipelineRepository) Entity.getEntityRepository(Entity.INGESTION_PIPELINE);
        IngestionPipeline ingestionPipeline =
            ingestionPipelineRepository.get(
                uriInfo, pipelineRef.getId(), ingestionPipelineRepository.getFields(FIELD_OWNER));
        return Response.ok(
                pipelineServiceClient.getLastIngestionLogs(ingestionPipeline, after),
                MediaType.APPLICATION_JSON_TYPE)
            .build();
      }
    }
    throw new BadRequestException("Failed to Get Logs for the Installation.");
  }

  @GET
  @Path("/name/{name}/runs/latest")
  @Operation(
      operationId = "latestAppRunRecord",
      summary = "Get Latest App Run Record",
      description = "Get a latest applications Run Record.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "List of Installed Applications Runs",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = AppRunRecord.class)))
      })
  public Response listLatestAppRun(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name,
      @Parameter(
              description = "Returns log chunk after this cursor",
              schema = @Schema(type = "string"))
          @QueryParam("after")
          @DefaultValue("")
          String after) {
    App installation = repository.getByName(uriInfo, name, repository.getFields("id,pipelines"));
    if (installation.getAppType().equals(AppType.Internal)) {
      return Response.status(Response.Status.OK)
          .entity(repository.getLatestAppRuns(installation.getId()))
          .build();
    } else {
      if (!installation.getPipelines().isEmpty()) {
        EntityReference pipelineRef = installation.getPipelines().get(0);
        IngestionPipelineRepository ingestionPipelineRepository =
            (IngestionPipelineRepository) Entity.getEntityRepository(Entity.INGESTION_PIPELINE);
        IngestionPipeline ingestionPipeline =
            ingestionPipelineRepository.get(
                uriInfo, pipelineRef.getId(), ingestionPipelineRepository.getFields(FIELD_OWNER));
        PipelineStatus latestPipelineStatus =
            ingestionPipelineRepository.getLatestPipelineStatus(ingestionPipeline);
        Map lastIngestionLogs =
            pipelineServiceClient.getLastIngestionLogs(ingestionPipeline, after);
        Map appRun = new HashMap<>();
        appRun.put("pipelineStatus", latestPipelineStatus);
        appRun.put("lastIngestionLogs", lastIngestionLogs);
        return Response.ok(appRun, MediaType.APPLICATION_JSON_TYPE).build();
      }
    }
    throw new BadRequestException("Failed to Get Logs for the Installation.");
  }

  @GET
  @Path("/{id}/versions")
  @Operation(
      operationId = "listAllInstalledApplications",
      summary = "List Installed Application versions",
      description = "Get a list of all the versions of a application identified by `id`",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "List of installed application versions",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = EntityHistory.class)))
      })
  public EntityHistory listVersions(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Id of the app", schema = @Schema(type = "UUID")) @PathParam("id")
          UUID id) {
    return super.listVersionsInternal(securityContext, id);
  }

  @GET
  @Path("/{id}")
  @Operation(
      summary = "Get a app by Id",
      description = "Get a app by `Id`.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "The App",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = App.class))),
        @ApiResponse(responseCode = "404", description = "App for instance {id} is not found")
      })
  public App get(
      @Context UriInfo uriInfo,
      @Parameter(description = "Id of the App", schema = @Schema(type = "UUID")) @PathParam("id")
          UUID id,
      @Context SecurityContext securityContext,
      @Parameter(
              description = "Fields requested in the returned resource",
              schema = @Schema(type = "string", example = FIELDS))
          @QueryParam("fields")
          String fieldsParam,
      @Parameter(
              description = "Include all, deleted, or non-deleted entities.",
              schema = @Schema(implementation = Include.class))
          @QueryParam("include")
          @DefaultValue("non-deleted")
          Include include) {
    return getInternal(uriInfo, securityContext, id, fieldsParam, include);
  }

  @GET
  @Path("/name/{name}")
  @Operation(
      operationId = "getAppByName",
      summary = "Get a App by name",
      description = "Get a App by `name`.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "The App",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = App.class))),
        @ApiResponse(responseCode = "404", description = "App for instance {name} is not found")
      })
  public App getByName(
      @Context UriInfo uriInfo,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name,
      @Context SecurityContext securityContext,
      @Parameter(
              description = "Fields requested in the returned resource",
              schema = @Schema(type = "string", example = FIELDS))
          @QueryParam("fields")
          String fieldsParam,
      @Parameter(
              description = "Include all, deleted, or non-deleted entities.",
              schema = @Schema(implementation = Include.class))
          @QueryParam("include")
          @DefaultValue("non-deleted")
          Include include) {
    return getByNameInternal(uriInfo, securityContext, name, fieldsParam, include);
  }

  @GET
  @Path("/{id}/versions/{version}")
  @Operation(
      operationId = "getSpecificAppVersion",
      summary = "Get a version of the App",
      description = "Get a version of the App by given `id`",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "App",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = App.class))),
        @ApiResponse(
            responseCode = "404",
            description = "App for instance {id} and version {version} is not found")
      })
  public App getVersion(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Id of the App", schema = @Schema(type = "UUID")) @PathParam("id")
          UUID id,
      @Parameter(
              description = "App version number in the form `major`.`minor`",
              schema = @Schema(type = "string", example = "0.1 or 1.1"))
          @PathParam("version")
          String version) {
    return super.getVersionInternal(securityContext, id, version);
  }

  @POST
  @Operation(
      operationId = "createApplication",
      summary = "Create a Application",
      description = "Create a application",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "The Application",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = App.class))),
        @ApiResponse(responseCode = "400", description = "Bad request")
      })
  public Response create(
      @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateApp create) {
    AppMarketPlaceDefinition definition =
        repository
            .getMarketPlace()
            .getByName(
                uriInfo,
                create.getName(),
                new EntityUtil.Fields(repository.getMarketPlace().getAllowedFields()));
    App app = getApplication(definition, create, securityContext.getUserPrincipal().getName());
    if (app.getScheduleType().equals(ScheduleType.Scheduled)) {
      ApplicationHandler.getInstance()
          .installApplication(app, Entity.getCollectionDAO(), searchRepository);
      ApplicationHandler.getInstance()
          .configureApplication(app, Entity.getCollectionDAO(), searchRepository);
    }
    // We don't want to store this information
    unsetAppRuntimeProperties(app);
    return create(uriInfo, securityContext, app);
  }

  @PATCH
  @Path("/{id}")
  @Operation(
      operationId = "patchApplication",
      summary = "Updates a App",
      description = "Update an existing App using JsonPatch.",
      externalDocs =
          @ExternalDocumentation(
              description = "JsonPatch RFC",
              url = "https://tools.ietf.org/html/rfc6902"))
  @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
  public Response patchApplication(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Id of the App", schema = @Schema(type = "UUID")) @PathParam("id")
          UUID id,
      @RequestBody(
              description = "JsonPatch with array of operations",
              content =
                  @Content(
                      mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
                      examples = {
                        @ExampleObject("[{op:remove, path:/a},{op:add, path: /b, value: val}]")
                      }))
          JsonPatch patch)
      throws SchedulerException {
    App app = repository.get(null, id, repository.getFields("bot,pipelines"));
    if (app.getSystem()) {
      throw new IllegalArgumentException(
          CatalogExceptionMessage.systemEntityModifyNotAllowed(app.getName(), "SystemApp"));
    }
    AppScheduler.getInstance().deleteScheduledApplication(app);
    Response response = patchInternal(uriInfo, securityContext, id, patch);
    App updatedApp = (App) response.getEntity();
    if (app.getScheduleType().equals(ScheduleType.Scheduled)) {
      ApplicationHandler.getInstance()
          .installApplication(updatedApp, Entity.getCollectionDAO(), searchRepository);
    }
    // We don't want to store this information
    unsetAppRuntimeProperties(updatedApp);
    return response;
  }

  @PATCH
  @Path("/name/{fqn}")
  @Operation(
      operationId = "patchApplication",
      summary = "Updates a App by name.",
      description = "Update an existing App using JsonPatch.",
      externalDocs =
          @ExternalDocumentation(
              description = "JsonPatch RFC",
              url = "https://tools.ietf.org/html/rfc6902"))
  @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
  public Response patchApplication(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("fqn")
          String fqn,
      @RequestBody(
              description = "JsonPatch with array of operations",
              content =
                  @Content(
                      mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
                      examples = {
                        @ExampleObject("[{op:remove, path:/a},{op:add, path: /b, value: val}]")
                      }))
          JsonPatch patch)
      throws SchedulerException {
    App app = repository.getByName(null, fqn, repository.getFields("bot,pipelines"));
    if (app.getSystem()) {
      throw new IllegalArgumentException(
          CatalogExceptionMessage.systemEntityModifyNotAllowed(app.getName(), "SystemApp"));
    }
    AppScheduler.getInstance().deleteScheduledApplication(app);
    Response response = patchInternal(uriInfo, securityContext, fqn, patch);
    App updatedApp = (App) response.getEntity();
    if (app.getScheduleType().equals(ScheduleType.Scheduled)) {
      ApplicationHandler.getInstance()
          .installApplication(updatedApp, Entity.getCollectionDAO(), searchRepository);
    }
    // We don't want to store this information
    unsetAppRuntimeProperties(updatedApp);
    return response;
  }

  @PUT
  @Operation(
      operationId = "createOrUpdateApp",
      summary = "Create Or Update App",
      description = "Create or Update App, it it does not exist or update an existing KPI.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "The updated Application Objective ",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = App.class)))
      })
  public Response createOrUpdate(
      @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateApp create)
      throws SchedulerException {
    AppMarketPlaceDefinition definition =
        repository
            .getMarketPlace()
            .getByName(
                uriInfo,
                create.getName(),
                new EntityUtil.Fields(repository.getMarketPlace().getAllowedFields()));
    App app = getApplication(definition, create, securityContext.getUserPrincipal().getName());
    AppScheduler.getInstance().deleteScheduledApplication(app);
    if (app.getScheduleType().equals(ScheduleType.Scheduled)) {
      ApplicationHandler.getInstance()
          .installApplication(app, Entity.getCollectionDAO(), searchRepository);
    }
    // We don't want to store this information
    unsetAppRuntimeProperties(app);
    return createOrUpdate(uriInfo, securityContext, app);
  }

  @DELETE
  @Path("/name/{name}")
  @Operation(
      operationId = "uninstallAppByName",
      summary = "Delete a App by name",
      description = "Delete a App by `name`.",
      responses = {
        @ApiResponse(responseCode = "200", description = "OK"),
        @ApiResponse(
            responseCode = "400",
            description = "System entity {name} of type SystemApp can not be deleted."),
        @ApiResponse(responseCode = "404", description = "App for instance {name} is not found")
      })
  public Response delete(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Hard delete the entity. (Default = `false`)")
          @QueryParam("hardDelete")
          @DefaultValue("false")
          boolean hardDelete,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name) {
    App app =
        repository.getByName(uriInfo, name, repository.getFields("bot,pipelines"), ALL, false);
    if (app.getSystem()) {
      throw new IllegalArgumentException(
          CatalogExceptionMessage.systemEntityDeleteNotAllowed(app.getName(), "SystemApp"));
    }
    // Remove from Pipeline Service
    deleteApp(securityContext, app, hardDelete);
    return deleteByName(uriInfo, securityContext, name, true, hardDelete);
  }

  @DELETE
  @Path("/{id}")
  @Operation(
      operationId = "uninstallAppByName",
      summary = "Delete a App by Id",
      description = "Delete a App by `Id`.",
      responses = {
        @ApiResponse(responseCode = "200", description = "OK"),
        @ApiResponse(
            responseCode = "400",
            description = "System entity {name} of type SystemApp can not be deleted."),
        @ApiResponse(responseCode = "404", description = "App for instance {id} is not found")
      })
  public Response delete(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Hard delete the entity. (Default = `false`)")
          @QueryParam("hardDelete")
          @DefaultValue("false")
          boolean hardDelete,
      @Parameter(description = "Id of the App", schema = @Schema(type = "UUID")) @PathParam("id")
          UUID id) {
    App app = repository.get(uriInfo, id, repository.getFields("bot,pipelines"), ALL, false);
    if (app.getSystem()) {
      throw new IllegalArgumentException(
          CatalogExceptionMessage.systemEntityDeleteNotAllowed(app.getName(), "SystemApp"));
    }
    // Remove from Pipeline Service
    deleteApp(securityContext, app, hardDelete);
    // Remove from repository
    return delete(uriInfo, securityContext, id, true, hardDelete);
  }

  @PUT
  @Path("/restore")
  @Operation(
      operationId = "restore",
      summary = "Restore a soft deleted KPI",
      description = "Restore a soft deleted App.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "Successfully restored the App. ",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = App.class)))
      })
  public Response restoreApp(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Valid RestoreEntity restore) {
    Response response = restoreEntity(uriInfo, securityContext, restore.getId());
    if (response.getStatus() == Response.Status.OK.getStatusCode()) {
      App app = (App) response.getEntity();
      if (app.getScheduleType().equals(ScheduleType.Scheduled)) {
        ApplicationHandler.getInstance()
            .installApplication(app, Entity.getCollectionDAO(), searchRepository);
      }
      // We don't want to store this information
      unsetAppRuntimeProperties(app);
    }
    return response;
  }

  @POST
  @Path("/schedule/{name}")
  @Operation(
      operationId = "scheduleApplication",
      summary = "Schedule an Application",
      description = "Schedule a application to be run on demand.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "The Application",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = Response.class))),
        @ApiResponse(
            responseCode = "404",
            description = "Application for instance {id} is not found")
      })
  public Response scheduleApplication(
      @Context UriInfo uriInfo,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name,
      @Context SecurityContext securityContext) {
    App app =
        repository.getByName(uriInfo, name, new EntityUtil.Fields(repository.getAllowedFields()));
    if (app.getScheduleType().equals(ScheduleType.Scheduled)) {
      ApplicationHandler.getInstance()
          .installApplication(app, repository.getDaoCollection(), searchRepository);
      return Response.status(Response.Status.OK).entity("App is Scheduled.").build();
    }
    throw new IllegalArgumentException("App is not of schedule type Scheduled.");
  }

  @POST
  @Path("/configure/{name}")
  @Operation(
      operationId = "configureApplication",
      summary = "Configure an Application",
      description = "Schedule a application to be run on demand.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "The Application",
            content =
                @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = Response.class))),
        @ApiResponse(
            responseCode = "404",
            description = "Application for instance {id} is not found")
      })
  public Response configureApplication(
      @Context UriInfo uriInfo,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name,
      @Context SecurityContext securityContext) {
    App app =
        repository.getByName(uriInfo, name, new EntityUtil.Fields(repository.getAllowedFields()));
    // The application will have the updated appConfiguration we can use to run the `configure`
    // logic
    try {
      ApplicationHandler.getInstance()
          .configureApplication(app, repository.getDaoCollection(), searchRepository);
      return Response.status(Response.Status.OK).entity("App has been configured.").build();
    } catch (RuntimeException e) {
      return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
          .entity(String.format("Error configuring app [%s]", e.getMessage()))
          .build();
    }
  }

  @POST
  @Path("/trigger/{name}")
  @Operation(
      operationId = "triggerApplicationRun",
      summary = "Trigger an Application run",
      description = "Trigger a Application run by id.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "Application trigger status code",
            content = @Content(mediaType = "application/json")),
        @ApiResponse(
            responseCode = "404",
            description = "Application for instance {id} is not found")
      })
  public Response triggerApplicationRun(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name) {
    EntityUtil.Fields fields = getFields(String.format("%s,bot,pipelines", FIELD_OWNER));
    App app = repository.getByName(uriInfo, name, fields);
    if (app.getAppType().equals(AppType.Internal)) {
      ApplicationHandler.getInstance()
          .triggerApplicationOnDemand(app, Entity.getCollectionDAO(), searchRepository);
      return Response.status(Response.Status.OK).entity("Application Triggered").build();
    } else {
      if (!app.getPipelines().isEmpty()) {
        EntityReference pipelineRef = app.getPipelines().get(0);
        IngestionPipelineRepository ingestionPipelineRepository =
            (IngestionPipelineRepository) Entity.getEntityRepository(Entity.INGESTION_PIPELINE);

        IngestionPipeline ingestionPipeline =
            ingestionPipelineRepository.get(
                uriInfo, pipelineRef.getId(), ingestionPipelineRepository.getFields(FIELD_OWNER));
        ingestionPipeline.setOpenMetadataServerConnection(app.getOpenMetadataServerConnection());
        decryptOrNullify(securityContext, ingestionPipeline, app.getBot().getName(), true);
        ServiceEntityInterface service =
            Entity.getEntity(ingestionPipeline.getService(), "", Include.NON_DELETED);
        PipelineServiceClientResponse response =
            pipelineServiceClient.runPipeline(ingestionPipeline, service);
        return Response.status(response.getCode()).entity(response).build();
      }
    }
    throw new BadRequestException("Failed to trigger application.");
  }

  @POST
  @Path("/deploy/{name}")
  @Operation(
      operationId = "deployApplicationToQuartzOrIngestion",
      summary = "Deploy App to Quartz or Ingestion",
      description = "Deploy App to Quartz or Ingestion.",
      responses = {
        @ApiResponse(
            responseCode = "200",
            description = "Application trigger status code",
            content = @Content(mediaType = "application/json")),
        @ApiResponse(
            responseCode = "404",
            description = "Application for instance {id} is not found")
      })
  public Response deployApplicationFlow(
      @Context UriInfo uriInfo,
      @Context SecurityContext securityContext,
      @Parameter(description = "Name of the App", schema = @Schema(type = "string"))
          @PathParam("name")
          String name) {
    EntityUtil.Fields fields = getFields(String.format("%s,bot,pipelines", FIELD_OWNER));
    App app = repository.getByName(uriInfo, name, fields);
    if (app.getAppType().equals(AppType.Internal)) {
      ApplicationHandler.getInstance()
          .installApplication(app, Entity.getCollectionDAO(), searchRepository);
      return Response.status(Response.Status.OK).entity("Application Deployed").build();
    } else {
      if (!app.getPipelines().isEmpty()) {
        EntityReference pipelineRef = app.getPipelines().get(0);
        IngestionPipelineRepository ingestionPipelineRepository =
            (IngestionPipelineRepository) Entity.getEntityRepository(Entity.INGESTION_PIPELINE);

        IngestionPipeline ingestionPipeline =
            ingestionPipelineRepository.get(
                uriInfo, pipelineRef.getId(), ingestionPipelineRepository.getFields(FIELD_OWNER));

        ingestionPipeline.setOpenMetadataServerConnection(app.getOpenMetadataServerConnection());
        decryptOrNullify(securityContext, ingestionPipeline, app.getBot().getName(), true);
        ServiceEntityInterface service =
            Entity.getEntity(ingestionPipeline.getService(), "", Include.NON_DELETED);
        PipelineServiceClientResponse status =
            pipelineServiceClient.deployPipeline(ingestionPipeline, service);
        if (status.getCode() == 200) {
          ingestionPipelineRepository.createOrUpdate(uriInfo, ingestionPipeline);
        } else {
          ingestionPipeline.setDeployed(false);
        }
        return Response.status(status.getCode()).entity(status).build();
      }
    }
    throw new BadRequestException("Failed to trigger application.");
  }

  private void decryptOrNullify(
      SecurityContext securityContext,
      IngestionPipeline ingestionPipeline,
      String botName,
      boolean forceNotMask) {
    SecretsManager secretsManager = SecretsManagerFactory.getSecretsManager();
    try {
      authorizer.authorize(
          securityContext,
          new OperationContext(entityType, MetadataOperation.VIEW_ALL),
          getResourceContextById(ingestionPipeline.getId()));
    } catch (AuthorizationException e) {
      ingestionPipeline.getSourceConfig().setConfig(null);
    }
    secretsManager.decryptIngestionPipeline(ingestionPipeline);
    OpenMetadataConnection openMetadataServerConnection =
        new OpenMetadataConnectionBuilder(openMetadataApplicationConfig, botName).build();
    ingestionPipeline.setOpenMetadataServerConnection(
        secretsManager.encryptOpenMetadataConnection(openMetadataServerConnection, false));
    if (authorizer.shouldMaskPasswords(securityContext) && !forceNotMask) {
      EntityMaskerFactory.getEntityMasker().maskIngestionPipeline(ingestionPipeline);
    }
  }

  private App getApplication(
      AppMarketPlaceDefinition marketPlaceDefinition,
      CreateApp createAppRequest,
      String updatedBy) {
    EntityReference owner = repository.validateOwner(createAppRequest.getOwner());
    App app =
        new App()
            .withId(UUID.randomUUID())
            .withName(marketPlaceDefinition.getName())
            .withDisplayName(createAppRequest.getDisplayName())
            .withDescription(createAppRequest.getDescription())
            .withOwner(owner)
            .withUpdatedBy(updatedBy)
            .withUpdatedAt(System.currentTimeMillis())
            .withDeveloper(marketPlaceDefinition.getDeveloper())
            .withDeveloperUrl(marketPlaceDefinition.getDeveloperUrl())
            .withPrivacyPolicyUrl(marketPlaceDefinition.getPrivacyPolicyUrl())
            .withSupportEmail(marketPlaceDefinition.getSupportEmail())
            .withClassName(marketPlaceDefinition.getClassName())
            .withAppType(marketPlaceDefinition.getAppType())
            .withScheduleType(marketPlaceDefinition.getScheduleType())
            .withAppConfiguration(createAppRequest.getAppConfiguration())
            .withRuntime(marketPlaceDefinition.getRuntime())
            .withPermission(marketPlaceDefinition.getPermission())
            .withAppSchedule(createAppRequest.getAppSchedule())
            .withAppLogoUrl(marketPlaceDefinition.getAppLogoUrl())
            .withAppScreenshots(marketPlaceDefinition.getAppScreenshots())
            .withFeatures(marketPlaceDefinition.getFeatures())
            .withSourcePythonClass(marketPlaceDefinition.getSourcePythonClass())
            .withAllowConfiguration(marketPlaceDefinition.getAllowConfiguration())
            .withSystem(marketPlaceDefinition.getSystem());

    // validate Bot if provided
    validateAndAddBot(app, createAppRequest.getBot());

    return app;
  }

  private void validateAndAddBot(App app, String botName) {
    if (!CommonUtil.nullOrEmpty(botName)) {
      app.setBot(Entity.getEntityReferenceByName(BOT, botName, Include.NON_DELETED));
    } else {
      app.setBot(repository.createNewAppBot(app));
    }
  }

  private void deleteApp(SecurityContext securityContext, App installedApp, boolean hardDelete) {
    if (installedApp.getAppType().equals(AppType.Internal)) {
      try {
        AppScheduler.getInstance().deleteScheduledApplication(installedApp);
      } catch (SchedulerException ex) {
        LOG.error("Failed in delete Application from Scheduler.", ex);
        throw new InternalServerErrorException("Failed in Delete App from Scheduler.");
      }
    } else {
      if (!nullOrEmpty(installedApp.getPipelines())) {
        EntityReference pipelineRef = installedApp.getPipelines().get(0);
        IngestionPipelineRepository ingestionPipelineRepository =
            (IngestionPipelineRepository) Entity.getEntityRepository(Entity.INGESTION_PIPELINE);

        IngestionPipeline ingestionPipeline =
            ingestionPipelineRepository.get(
                null, pipelineRef.getId(), ingestionPipelineRepository.getFields(FIELD_OWNER));
        try {
          pipelineServiceClient.deletePipeline(ingestionPipeline);
        } catch (Exception ex) {
          LOG.error("Failed in Pipeline Service Client : ", ex);
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy