Repository: kylin Updated Branches: refs/heads/KYLIN-1826 [created] 4b4e0b8ee
KYLIN-1826, add external hive interface, project, table.. Signed-off-by: terry-chelsea <hzfen...@corp.netease.com> Signed-off-by: shaofengshi <shaofeng...@apache.org> Project: http://git-wip-us.apache.org/repos/asf/kylin/repo Commit: http://git-wip-us.apache.org/repos/asf/kylin/commit/be7128bf Tree: http://git-wip-us.apache.org/repos/asf/kylin/tree/be7128bf Diff: http://git-wip-us.apache.org/repos/asf/kylin/diff/be7128bf Branch: refs/heads/KYLIN-1826 Commit: be7128bff25448202bf888f808fda1e95d6cec2b Parents: b08871e Author: terry-chelsea <hzfen...@corp.netease.com> Authored: Fri Oct 28 20:38:35 2016 +0800 Committer: shaofengshi <shaofeng...@apache.org> Committed: Fri Oct 28 22:34:34 2016 +0800 ---------------------------------------------------------------------- .../apache/kylin/common/KylinConfigBase.java | 4 + .../apache/kylin/metadata/model/TableDesc.java | 11 + .../kylin/metadata/project/ProjectInstance.java | 11 + .../kylin/metadata/project/ProjectManager.java | 22 +- .../hive/ITHiveSourceTableLoaderTest.java | 3 +- .../kylin/rest/controller/CubeController.java | 17 ++ .../rest/controller/ProjectController.java | 28 ++- .../rest/controller/StreamingController.java | 4 + .../kylin/rest/controller/TableController.java | 24 ++- .../rest/request/CreateProjectRequest.java | 9 + .../rest/request/UpdateProjectRequest.java | 10 + .../apache/kylin/rest/service/BasicService.java | 14 +- .../apache/kylin/rest/service/CacheService.java | 4 +- .../apache/kylin/rest/service/CubeService.java | 10 +- .../kylin/rest/service/ProjectService.java | 6 +- .../apache/kylin/source/hive/HiveClient.java | 2 +- .../source/hive/HiveSourceTableLoader.java | 22 +- .../hive/external/ExternalHiveClient.java | 80 +++++++ .../kylin/source/hive/external/HiveManager.java | 206 +++++++++++++++++++ webapp/app/js/controllers/page.js | 1 + webapp/app/js/controllers/sourceMeta.js | 4 +- webapp/app/js/model/projectConfig.js | 1 + .../app/partials/projects/project_create.html | 9 + webapp/app/partials/projects/projects.html | 1 + 24 files changed, 476 insertions(+), 27 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java ---------------------------------------------------------------------- diff --git a/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java b/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java index 79ee084..5063110 100644 --- a/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java +++ b/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java @@ -365,6 +365,10 @@ abstract public class KylinConfigBase implements Serializable { public String getCliWorkingDir() { return getOptional("kylin.job.remote.cli.working.dir"); } + + public String getExternalHiveRootDirectory() { + return getOptional("kylin.external.hive.root.directory", null); + } public boolean isEmptySegmentAllowed() { return Boolean.parseBoolean(getOptional("kylin.job.allow.empty.segment", "true")); http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/core-metadata/src/main/java/org/apache/kylin/metadata/model/TableDesc.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/model/TableDesc.java b/core-metadata/src/main/java/org/apache/kylin/metadata/model/TableDesc.java index e163d1d..abd348d 100644 --- a/core-metadata/src/main/java/org/apache/kylin/metadata/model/TableDesc.java +++ b/core-metadata/src/main/java/org/apache/kylin/metadata/model/TableDesc.java @@ -45,6 +45,8 @@ public class TableDesc extends RootPersistentEntity implements ISourceAware { private int sourceType = ISourceAware.ID_HIVE; @JsonProperty("table_type") private String tableType; + @JsonProperty("hive") + private String hive; private static final String materializedTableNamePrefix = "kylin_intermediate_"; @@ -60,6 +62,7 @@ public class TableDesc extends RootPersistentEntity implements ISourceAware { this.columns = other.getColumns(); this.database.setName(other.getDatabase()); this.tableType = other.getTableType(); + this.hive = other.getHive(); } public ColumnDesc findColumnByName(String name) { @@ -250,4 +253,12 @@ public class TableDesc extends RootPersistentEntity implements ISourceAware { this.tableType = tableType; } + public String getHive() { + return hive; + } + + public void setHive(String hive) { + this.hive = hive; + } + } http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java b/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java index 1afc603..b0ce889 100644 --- a/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java +++ b/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectInstance.java @@ -49,6 +49,9 @@ public class ProjectInstance extends RootPersistentEntity { @JsonProperty("name") private String name; + + @JsonProperty("hive") + private String hive; @JsonProperty("tables") private Set<String> tables = new TreeSet<String>(); @@ -153,6 +156,14 @@ public class ProjectInstance extends RootPersistentEntity { public void setName(String name) { this.name = name; } + + public String getHive() { + return hive; + } + + public void setHive(String hive) { + this.hive = hive; + } public boolean containsRealization(final RealizationType type, final String realization) { return Iterables.any(this.realizationEntries, new Predicate<RealizationEntry>() { http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectManager.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectManager.java b/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectManager.java index 1bf9804..e86c1a2 100644 --- a/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectManager.java +++ b/core-metadata/src/main/java/org/apache/kylin/metadata/project/ProjectManager.java @@ -152,6 +152,15 @@ public class ProjectManager { return currentProject; } + + public ProjectInstance createProject(String projectName, String owner, String description, String hiveName) throws IOException { + logger.info("Creating project " + projectName + ", hive name " + hiveName); + ProjectInstance currentProject = this.createProject(projectName, owner, description); + currentProject.setHive(hiveName); + updateProject(currentProject); + + return currentProject; + } public ProjectInstance dropProject(String projectName) throws IOException { if (projectName == null) @@ -183,9 +192,9 @@ public class ProjectManager { } //update project itself - public ProjectInstance updateProject(ProjectInstance project, String newName, String newDesc) throws IOException { + public ProjectInstance updateProject(ProjectInstance project, String newName, String newDesc, String hiveName) throws IOException { if (!project.getName().equals(newName)) { - ProjectInstance newProject = this.createProject(newName, project.getOwner(), newDesc); + ProjectInstance newProject = this.createProject(newName, project.getOwner(), newDesc, hiveName); newProject.setCreateTimeUTC(project.getCreateTimeUTC()); newProject.recordUpdateTime(System.currentTimeMillis()); @@ -200,8 +209,13 @@ public class ProjectManager { return newProject; } else { project.setName(newName); - project.setDescription(newDesc); - + if(newDesc != null) { + project.setDescription(newDesc); + } + if(hiveName != null) { + project.setHive(hiveName); + } + if (project.getUuid() == null) project.updateRandomUuid(); http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/kylin-it/src/test/java/org/apache/kylin/source/hive/ITHiveSourceTableLoaderTest.java ---------------------------------------------------------------------- diff --git a/kylin-it/src/test/java/org/apache/kylin/source/hive/ITHiveSourceTableLoaderTest.java b/kylin-it/src/test/java/org/apache/kylin/source/hive/ITHiveSourceTableLoaderTest.java index c4f0777..c33840c 100644 --- a/kylin-it/src/test/java/org/apache/kylin/source/hive/ITHiveSourceTableLoaderTest.java +++ b/kylin-it/src/test/java/org/apache/kylin/source/hive/ITHiveSourceTableLoaderTest.java @@ -45,7 +45,8 @@ public class ITHiveSourceTableLoaderTest extends HBaseMetadataTestCase { public void test() throws IOException { KylinConfig config = getTestConfig(); String[] toLoad = new String[] { "DEFAULT.TEST_KYLIN_FACT", "EDW.TEST_CAL_DT" }; - Set<String> loaded = HiveSourceTableLoader.reloadHiveTables(toLoad, config); + String project = "learn_kylin"; + Set<String> loaded = HiveSourceTableLoader.reloadHiveTables(toLoad, project, config); assertTrue(loaded.size() == toLoad.length); for (String str : toLoad) http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java index 5397df7..d371230 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java +++ b/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java @@ -56,6 +56,7 @@ import org.apache.kylin.rest.service.CubeService; import org.apache.kylin.rest.service.JobService; import org.apache.kylin.rest.service.KafkaConfigService; import org.apache.kylin.rest.service.StreamingService; +import org.apache.kylin.source.hive.external.HiveManager; import org.apache.kylin.source.kafka.config.KafkaConfig; import org.apache.kylin.storage.hbase.cube.v1.coprocessor.observer.ObserverEnabler; import org.slf4j.Logger; @@ -343,6 +344,9 @@ public class CubeController extends BasicController { String newCubeName = cubeRequest.getCubeName(); String project = cubeRequest.getProject(); + if(!this.isBasedSameSource(cubeName, project)) { + throw new InternalErrorException("Can not move cube to project with different hive source!"); + } CubeInstance cube = cubeService.getCubeManager().getCube(cubeName); if (cube == null) { throw new InternalErrorException("Cannot find cube " + cubeName); @@ -364,6 +368,19 @@ public class CubeController extends BasicController { return newCube; } + + private boolean isBasedSameSource(String cubeName, String projectName) { + CubeInstance cube = cubeService.getCubeManager().getCube(cubeName); + if(cube == null) + return true; + + ProjectInstance project = cubeService.getProjectManager().getProject(projectName); + if(project == null) { + return true; + } + ProjectInstance cubeProject = cubeService.getProjectByCube(cubeName); + return HiveManager.isSameHiveSource(project.getHive(), cubeProject.getHive()); + } @RequestMapping(value = "/{cubeName}/enable", method = { RequestMethod.PUT }) @ResponseBody http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/controller/ProjectController.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/ProjectController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/ProjectController.java index 496e44a..bee4a63 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/controller/ProjectController.java +++ b/server-base/src/main/java/org/apache/kylin/rest/controller/ProjectController.java @@ -33,6 +33,7 @@ import org.apache.kylin.rest.request.UpdateProjectRequest; import org.apache.kylin.rest.service.AccessService; import org.apache.kylin.rest.service.CubeService; import org.apache.kylin.rest.service.ProjectService; +import org.apache.kylin.source.hive.external.HiveManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -199,7 +200,9 @@ public class ProjectController extends BasicController { if (StringUtils.isEmpty(projectRequest.getName())) { throw new InternalErrorException("A project name must be given to create a project"); } - + + String hiveName = projectRequest.getHive(); + checkHiveIsValid(hiveName); ProjectInstance createdProj = null; try { createdProj = projectService.createProject(projectRequest); @@ -217,7 +220,7 @@ public class ProjectController extends BasicController { if (StringUtils.isEmpty(projectRequest.getFormerProjectName())) { throw new InternalErrorException("A project name must be given to update a project"); } - + checkHiveIsValid(projectRequest.getNewHiveName()); ProjectInstance updatedProj = null; try { ProjectInstance currentProject = projectService.getProjectManager().getProject(projectRequest.getFormerProjectName()); @@ -254,4 +257,25 @@ public class ProjectController extends BasicController { public void setCubeService(CubeService cubeService) { this.cubeService = cubeService; } + + /** + * Check input hive name is valid + * @param hiveName + */ + private void checkHiveIsValid(String hiveName) { + try { + if(HiveManager.getInstance().isSupportExternalHives()) { + HiveManager.getInstance().getHiveCommand(hiveName); + HiveManager.getInstance().getHiveConfigFile(hiveName); + return; + } + } catch (Exception e) { + logger.error("Can not find hive " + hiveName + ", support hives : " + + HiveManager.getInstance().getExternalHiveName()); + throw new InternalErrorException("Can not find hive " + hiveName + " by current external hives configuration"); + } + if(hiveName != null) { + throw new InternalErrorException(HiveManager.NOT_SUPPORT_ERROR_MESSAGE); + } + } } http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/controller/StreamingController.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/StreamingController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/StreamingController.java index f3374c3..addf544 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/controller/StreamingController.java +++ b/server-base/src/main/java/org/apache/kylin/rest/controller/StreamingController.java @@ -29,6 +29,7 @@ import org.apache.kylin.engine.mr.HadoopUtil; import org.apache.kylin.engine.streaming.StreamingConfig; import org.apache.kylin.metadata.MetadataManager; import org.apache.kylin.metadata.model.TableDesc; +import org.apache.kylin.metadata.project.ProjectManager; import org.apache.kylin.rest.exception.BadRequestException; import org.apache.kylin.rest.exception.ForbiddenException; import org.apache.kylin.rest.exception.InternalErrorException; @@ -103,6 +104,8 @@ public class StreamingController extends BasicController { public StreamingRequest saveStreamingConfig(@RequestBody StreamingRequest streamingRequest) { String project = streamingRequest.getProject(); + ProjectManager projectMgr = ProjectManager.getInstance(KylinConfig.getInstanceFromEnv()); + String hive = projectMgr.getProject(project).getHive(); TableDesc tableDesc = deserializeTableDesc(streamingRequest); StreamingConfig streamingConfig = deserializeSchemalDesc(streamingRequest); KafkaConfig kafkaConfig = deserializeKafkaSchemalDesc(streamingRequest); @@ -110,6 +113,7 @@ public class StreamingController extends BasicController { try { tableDesc.setUuid(UUID.randomUUID().toString()); + tableDesc.setHive(hive); MetadataManager metaMgr = MetadataManager.getInstance(KylinConfig.getInstanceFromEnv()); metaMgr.saveSourceTable(tableDesc); cubeMgmtService.syncTableToProject(new String[] { tableDesc.getIdentity() }, project); http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/controller/TableController.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/TableController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/TableController.java index eefeba8..81ccb4b 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/controller/TableController.java +++ b/server-base/src/main/java/org/apache/kylin/rest/controller/TableController.java @@ -37,6 +37,7 @@ import org.apache.kylin.metadata.MetadataConstants; import org.apache.kylin.metadata.MetadataManager; import org.apache.kylin.metadata.model.ColumnDesc; import org.apache.kylin.metadata.model.TableDesc; +import org.apache.kylin.metadata.project.ProjectManager; import org.apache.kylin.rest.exception.InternalErrorException; import org.apache.kylin.rest.request.CardinalityRequest; import org.apache.kylin.rest.request.HiveTableRequest; @@ -48,6 +49,7 @@ import org.apache.kylin.rest.service.ModelService; import org.apache.kylin.rest.service.ProjectService; import org.apache.kylin.rest.service.StreamingService; import org.apache.kylin.source.hive.HiveClient; +import org.apache.kylin.source.hive.external.HiveManager; import org.apache.kylin.source.kafka.config.KafkaConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -145,7 +147,7 @@ public class TableController extends BasicController { @ResponseBody public Map<String, String[]> loadHiveTable(@PathVariable String tables, @PathVariable String project, @RequestBody HiveTableRequest request) throws IOException { String submitter = SecurityContextHolder.getContext().getAuthentication().getName(); - String[] loaded = cubeMgmtService.reloadHiveTable(tables); + String[] loaded = cubeMgmtService.reloadHiveTable(tables, project); if (request.isCalculate()) { cubeMgmtService.calculateCardinalityIfNotPresent(loaded, submitter); } @@ -233,8 +235,11 @@ public class TableController extends BasicController { public Map<String, String> addStreamingTable(@RequestBody StreamingRequest request) throws IOException { Map<String, String> result = new HashMap<String, String>(); String project = request.getProject(); + String hiveName = getHiveNameByProject(project); + TableDesc desc = JsonUtil.readValue(request.getTableData(), TableDesc.class); desc.setUuid(UUID.randomUUID().toString()); + desc.setHive(hiveName); MetadataManager metaMgr = MetadataManager.getInstance(KylinConfig.getInstanceFromEnv()); metaMgr.saveSourceTable(desc); cubeMgmtService.syncTableToProject(new String[] { desc.getName() }, project); @@ -311,11 +316,13 @@ public class TableController extends BasicController { */ @RequestMapping(value = "/hive", method = { RequestMethod.GET }) @ResponseBody - private static List<String> showHiveDatabases() throws IOException { - HiveClient hiveClient = new HiveClient(); + private static List<String> showHiveDatabases(@RequestParam(value = "project", required = true) String project) + throws IOException { List<String> results = null; try { + String hiveName = getHiveNameByProject(project); + HiveClient hiveClient = HiveManager.getInstance().createHiveClient(hiveName); results = hiveClient.getHiveDbNames(); } catch (Exception e) { e.printStackTrace(); @@ -332,11 +339,13 @@ public class TableController extends BasicController { */ @RequestMapping(value = "/hive/{database}", method = { RequestMethod.GET }) @ResponseBody - private static List<String> showHiveTables(@PathVariable String database) throws IOException { - HiveClient hiveClient = new HiveClient(); + private static List<String> showHiveTables(@RequestParam(value = "project", required = true) String project, + @PathVariable String database) throws IOException { List<String> results = null; try { + String hiveName = getHiveNameByProject(project); + HiveClient hiveClient = HiveManager.getInstance().createHiveClient(hiveName); results = hiveClient.getHiveTableNames(database); } catch (Exception e) { e.printStackTrace(); @@ -344,6 +353,11 @@ public class TableController extends BasicController { } return results; } + + private static String getHiveNameByProject(String project) { + ProjectManager projectMgr = ProjectManager.getInstance(KylinConfig.getInstanceFromEnv()); + return projectMgr.getProject(project).getHive(); + } public void setCubeService(CubeService cubeService) { this.cubeMgmtService = cubeService; http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/request/CreateProjectRequest.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/request/CreateProjectRequest.java b/server-base/src/main/java/org/apache/kylin/rest/request/CreateProjectRequest.java index 71cd1c4..745172f 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/request/CreateProjectRequest.java +++ b/server-base/src/main/java/org/apache/kylin/rest/request/CreateProjectRequest.java @@ -22,6 +22,7 @@ package org.apache.kylin.rest.request; */ public class CreateProjectRequest { private String name; + private String hive; private String description; public CreateProjectRequest() { @@ -43,4 +44,12 @@ public class CreateProjectRequest { this.description = description; } + public String getHive() { + return hive; + } + + public void setHive(String hive) { + this.hive = hive; + } + } http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/request/UpdateProjectRequest.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/request/UpdateProjectRequest.java b/server-base/src/main/java/org/apache/kylin/rest/request/UpdateProjectRequest.java index 29ba162..8dca4d9 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/request/UpdateProjectRequest.java +++ b/server-base/src/main/java/org/apache/kylin/rest/request/UpdateProjectRequest.java @@ -23,6 +23,7 @@ package org.apache.kylin.rest.request; public class UpdateProjectRequest { private String formerProjectName; private String newProjectName; + private String newHiveName; private String newDescription; public UpdateProjectRequest() { @@ -52,4 +53,13 @@ public class UpdateProjectRequest { public void setNewProjectName(String newProjectName) { this.newProjectName = newProjectName; } + + public String getNewHiveName() { + return newHiveName; + } + + public void setNewHiveName(String newHiveName) { + this.newHiveName = newHiveName; + } + } http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java b/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java index abf0638..e96bddf 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java +++ b/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java @@ -26,6 +26,7 @@ import java.util.Set; import org.apache.kylin.common.KylinConfig; import org.apache.kylin.cube.CubeDescManager; +import org.apache.kylin.cube.CubeInstance; import org.apache.kylin.cube.CubeManager; import org.apache.kylin.engine.mr.CubingJob; import org.apache.kylin.engine.mr.steps.CubingExecutableUtil; @@ -143,7 +144,18 @@ public abstract class BasicService { }))); return results; } - + + public ProjectInstance getProjectByCube(String cubeName) { + CubeInstance cube = getCubeManager().getCube(cubeName); + List<ProjectInstance> projList = this.getProjectManager().findProjects(cube.getType(), cube.getName()); + if (projList == null || projList.size() == 0) { + throw new RuntimeException("Cannot find the project containing the cube " + cube.getName()); + } else if (projList.size() >= 2) { + throw new RuntimeException("Find more than one project containing the cube " + cube.getName() + ". It does't meet the uniqueness requirement"); + } + return projList.get(0); + } + protected List<CubingJob> listAllCubingJobs(final String cubeName, final String projectName, final Set<ExecutableState> statusList) { return listAllCubingJobs(cubeName, projectName, statusList, getExecutableManager().getAllOutputs()); } http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/service/CacheService.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/CacheService.java b/server-base/src/main/java/org/apache/kylin/rest/service/CacheService.java index 2160e3d..6e091d8 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/service/CacheService.java +++ b/server-base/src/main/java/org/apache/kylin/rest/service/CacheService.java @@ -48,6 +48,7 @@ import org.apache.kylin.metadata.realization.RealizationType; import org.apache.kylin.query.enumerator.OLAPQuery; import org.apache.kylin.query.schema.OLAPSchemaFactory; import org.apache.kylin.rest.controller.QueryController; +import org.apache.kylin.source.hive.external.HiveManager; import org.apache.kylin.source.kafka.KafkaConfigManager; import org.apache.kylin.storage.hbase.HBaseConnection; import org.apache.kylin.storage.hybrid.HybridManager; @@ -210,7 +211,8 @@ public class CacheService extends BasicService { KafkaConfigManager.clearCache(); StreamingManager.clearCache(); HBaseConnection.clearConnCache(); - + HiveManager.clearCache(); + cleanAllDataCache(); removeAllOLAPDataSources(); break; http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java b/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java index 4cd527c..e6db717 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java +++ b/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java @@ -482,7 +482,11 @@ public class CubeService extends BasicService { String outPath = HiveColumnCardinalityJob.OUTPUT_PATH + "/" + tableName; String param = "-table " + tableName + " -output " + outPath; - + if(table.getHive() != null) { + logger.warn("Can not calculate cardinality for tables which loaded from external hive source !"); + return; + } + MapReduceExecutable step1 = new MapReduceExecutable(); step1.setMapReduceJobClass(HiveColumnCardinalityJob.class); @@ -561,8 +565,8 @@ public class CubeService extends BasicService { } @PreAuthorize(Constant.ACCESS_HAS_ROLE_MODELER + " or " + Constant.ACCESS_HAS_ROLE_ADMIN) - public String[] reloadHiveTable(String tables) throws IOException { - Set<String> loaded = HiveSourceTableLoader.reloadHiveTables(tables.split(","), getConfig()); + public String[] reloadHiveTable(String tables, String project) throws IOException { + Set<String> loaded = HiveSourceTableLoader.reloadHiveTables(tables.split(","), project, getConfig()); return (String[]) loaded.toArray(new String[loaded.size()]); } http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/server-base/src/main/java/org/apache/kylin/rest/service/ProjectService.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/ProjectService.java b/server-base/src/main/java/org/apache/kylin/rest/service/ProjectService.java index b4cceb2..00e4da6 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/service/ProjectService.java +++ b/server-base/src/main/java/org/apache/kylin/rest/service/ProjectService.java @@ -53,13 +53,14 @@ public class ProjectService extends BasicService { public ProjectInstance createProject(CreateProjectRequest projectRequest) throws IOException { String projectName = projectRequest.getName(); String description = projectRequest.getDescription(); + String hiveName = projectRequest.getHive(); ProjectInstance currentProject = getProjectManager().getProject(projectName); if (currentProject != null) { throw new InternalErrorException("The project named " + projectName + " already exists"); } String owner = SecurityContextHolder.getContext().getAuthentication().getName(); - ProjectInstance createdProject = getProjectManager().createProject(projectName, owner, description); + ProjectInstance createdProject = getProjectManager().createProject(projectName, owner, description, hiveName); accessService.init(createdProject, AclPermission.ADMINISTRATION); logger.debug("New project created."); @@ -71,12 +72,13 @@ public class ProjectService extends BasicService { String formerProjectName = projectRequest.getFormerProjectName(); String newProjectName = projectRequest.getNewProjectName(); String newDescription = projectRequest.getNewDescription(); + String newHiveName = projectRequest.getNewHiveName(); if (currentProject == null) { throw new InternalErrorException("The project named " + formerProjectName + " does not exists"); } - ProjectInstance updatedProject = getProjectManager().updateProject(currentProject, newProjectName, newDescription); + ProjectInstance updatedProject = getProjectManager().updateProject(currentProject, newProjectName, newDescription, newHiveName); logger.debug("Project updated."); http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/source-hive/src/main/java/org/apache/kylin/source/hive/HiveClient.java ---------------------------------------------------------------------- diff --git a/source-hive/src/main/java/org/apache/kylin/source/hive/HiveClient.java b/source-hive/src/main/java/org/apache/kylin/source/hive/HiveClient.java index a99b304..891c635 100644 --- a/source-hive/src/main/java/org/apache/kylin/source/hive/HiveClient.java +++ b/source-hive/src/main/java/org/apache/kylin/source/hive/HiveClient.java @@ -104,7 +104,7 @@ public class HiveClient { executeHQL(sql); } - private HiveMetaStoreClient getMetaStoreClient() throws Exception { + protected HiveMetaStoreClient getMetaStoreClient() throws Exception { if (metaStoreClient == null) { metaStoreClient = new HiveMetaStoreClient(hiveConf); } http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/source-hive/src/main/java/org/apache/kylin/source/hive/HiveSourceTableLoader.java ---------------------------------------------------------------------- diff --git a/source-hive/src/main/java/org/apache/kylin/source/hive/HiveSourceTableLoader.java b/source-hive/src/main/java/org/apache/kylin/source/hive/HiveSourceTableLoader.java index 70b097c..aceb791 100644 --- a/source-hive/src/main/java/org/apache/kylin/source/hive/HiveSourceTableLoader.java +++ b/source-hive/src/main/java/org/apache/kylin/source/hive/HiveSourceTableLoader.java @@ -33,6 +33,8 @@ import org.apache.kylin.metadata.MetadataConstants; import org.apache.kylin.metadata.MetadataManager; import org.apache.kylin.metadata.model.ColumnDesc; import org.apache.kylin.metadata.model.TableDesc; +import org.apache.kylin.metadata.project.ProjectManager; +import org.apache.kylin.source.hive.external.HiveManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,7 +57,7 @@ public class HiveSourceTableLoader { public static final String TABLE_FOLDER_NAME = "table"; public static final String TABLE_EXD_FOLDER_NAME = "table_exd"; - public static Set<String> reloadHiveTables(String[] hiveTables, KylinConfig config) throws IOException { + public static Set<String> reloadHiveTables(String[] hiveTables, String project, KylinConfig config) throws IOException { Map<String, Set<String>> db2tables = Maps.newHashMap(); for (String table : hiveTables) { @@ -71,7 +73,7 @@ public class HiveSourceTableLoader { // extract from hive Set<String> loadedTables = Sets.newHashSet(); for (String database : db2tables.keySet()) { - List<String> loaded = extractHiveTables(database, db2tables.get(database), config); + List<String> loaded = extractHiveTables(database, db2tables.get(database), project, config); loadedTables.addAll(loaded); } @@ -84,13 +86,23 @@ public class HiveSourceTableLoader { metaMgr.removeTableExd(hiveTable); } - private static List<String> extractHiveTables(String database, Set<String> tables, KylinConfig config) throws IOException { + private static List<String> extractHiveTables(String database, Set<String> tables, String project, KylinConfig config) throws IOException { List<String> loadedTables = Lists.newArrayList(); MetadataManager metaMgr = MetadataManager.getInstance(KylinConfig.getInstanceFromEnv()); + ProjectManager projectMgr = ProjectManager.getInstance(KylinConfig.getInstanceFromEnv()); + String hiveName = projectMgr.getProject(project).getHive(); for (String tableName : tables) { Table table = null; - HiveClient hiveClient = new HiveClient(); + HiveClient hiveClient = HiveManager.getInstance().createHiveClient(hiveName); + String tableIdentity = database + "." + tableName; + TableDesc tableDesc = metaMgr.getTableDesc(tableIdentity); + if(tableDesc != null && !HiveManager.isSameHiveSource(tableDesc.getHive(), hiveName)) { + throw new IllegalArgumentException("Table " + tableIdentity + " that in hive " + + hiveName + " has been loaded in hive " + tableDesc.getHive() + + ", please rename the table or unload the old one."); + } + List<FieldSchema> partitionFields = null; List<FieldSchema> fields = null; try { @@ -108,13 +120,13 @@ public class HiveSourceTableLoader { long tableSize = hiveClient.getFileSizeForTable(table); long tableFileNum = hiveClient.getFileNumberForTable(table); - TableDesc tableDesc = metaMgr.getTableDesc(database + "." + tableName); if (tableDesc == null) { tableDesc = new TableDesc(); tableDesc.setDatabase(database.toUpperCase()); tableDesc.setName(tableName.toUpperCase()); tableDesc.setUuid(UUID.randomUUID().toString()); tableDesc.setLastModified(0); + tableDesc.setHive(hiveName); } if (table.getTableType() != null) { tableDesc.setTableType(table.getTableType()); http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/source-hive/src/main/java/org/apache/kylin/source/hive/external/ExternalHiveClient.java ---------------------------------------------------------------------- diff --git a/source-hive/src/main/java/org/apache/kylin/source/hive/external/ExternalHiveClient.java b/source-hive/src/main/java/org/apache/kylin/source/hive/external/ExternalHiveClient.java new file mode 100644 index 0000000..b7d1559 --- /dev/null +++ b/source-hive/src/main/java/org/apache/kylin/source/hive/external/ExternalHiveClient.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.kylin.source.hive.external; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.metastore.HiveMetaStore; +import org.apache.hadoop.hive.metastore.HiveMetaStoreClient; +import org.apache.kylin.source.hive.HiveClient; + +/** + * Hive meta API client for external hive + * @author hzfengyu + */ +public class ExternalHiveClient extends HiveClient { + private final static String LOCAL_FS_SCHEMA = "file://"; + + public ExternalHiveClient(String location) { + URL uri = null; + if(location != null) { + try { + uri = new URL(LOCAL_FS_SCHEMA + location); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Can not find hive config file " + location); + } + } else { + uri = Thread.currentThread().getContextClassLoader().getResource("hive-site.xml"); + } + + /** + * In HiveConf, hiveSiteURL is a static variable, so we should use a global lock. + * If uri is null, HiveConf will use the file from java classpath. + */ + synchronized(ExternalHiveClient.class) { + hiveConf.setHiveSiteLocation(uri); + hiveConf = new HiveConf(HiveClient.class); + } + } + + public ExternalHiveClient(Map<String, String> configMap, String location) { + this(location); + appendConfiguration(configMap); + } + + @Override + protected HiveMetaStoreClient getMetaStoreClient() throws Exception { + /** + * HMSHandler is a LocalThread variable, in tomcat we should check it. + * When to remove hive meta store client in thread local variable: + * 1: when create new hive client. but HMSHandler exist in current thread. + * 2: when change hive client in current thread + */ + if (metaStoreClient == null) { + HiveMetaStore.HMSHandler.removeRawStore(); + metaStoreClient = new HiveMetaStoreClient(hiveConf); + } else if(HiveMetaStore.HMSHandler.getRawStore() != null && HiveMetaStore.HMSHandler.getRawStore().getConf() != this.hiveConf) { + HiveMetaStore.HMSHandler.getRawStore().shutdown(); + HiveMetaStore.HMSHandler.getRawStore().setConf(this.hiveConf); + } + return metaStoreClient; + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/source-hive/src/main/java/org/apache/kylin/source/hive/external/HiveManager.java ---------------------------------------------------------------------- diff --git a/source-hive/src/main/java/org/apache/kylin/source/hive/external/HiveManager.java b/source-hive/src/main/java/org/apache/kylin/source/hive/external/HiveManager.java new file mode 100644 index 0000000..5fbf111 --- /dev/null +++ b/source-hive/src/main/java/org/apache/kylin/source/hive/external/HiveManager.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.kylin.source.hive.external; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.kylin.common.KylinConfig; +import org.apache.kylin.engine.mr.HadoopUtil; +import org.apache.kylin.source.hive.HiveClient; +import org.apache.log4j.Logger; + + +/** + * Manager all hive client, default client and external clients. + * This is a Singleton object. + * @author hzfengyu + */ +public class HiveManager { + private volatile static HiveManager HIVE_MANAGER_CACHE = null; + private static final Logger logger = Logger.getLogger(HiveManager.class); + private static final String DEFAULT_HIVE_NAME = "default(null)"; + private static final String HIVE_CONFIG_FILE_LOCATION = "conf/hive-site.xml"; + private static final String HIVE_COMMAND_LOCATION = "bin/hive"; + public static final String NOT_SUPPORT_ERROR_MESSAGE = + "Do not support external hive, set kylin.external.hive.root.directory to support it"; + + //hive root directory, if this equals to null or empty meaning just use default hive + private File externalHiveRootDir = null; + private ConcurrentHashMap<String, HiveClient> externalHiveMap = null; + private HiveClient defaultHiveClient = null; + + public static HiveManager getInstance() { + //using default kylin config + if(HIVE_MANAGER_CACHE == null) { + KylinConfig config = KylinConfig.getInstanceFromEnv(); + synchronized(HiveManager.class) { + if(HIVE_MANAGER_CACHE == null) { + HIVE_MANAGER_CACHE = new HiveManager(config); + } + } + } + return HIVE_MANAGER_CACHE; + } + + private HiveManager(KylinConfig config) { + String externalRootDir = config.getExternalHiveRootDirectory(); + if(externalRootDir != null && !externalRootDir.isEmpty()) { + File file = new File(externalRootDir); + //check to ensure hive root file exist + this.externalHiveRootDir = file; + if(!(file.exists() && file.isDirectory())) { + logger.warn("Hive root directory " + file.getAbsolutePath() + " do not exist !"); + this.externalHiveRootDir = null; + } + } else { + this.externalHiveRootDir = null; + } + this.externalHiveMap = new ConcurrentHashMap<String, HiveClient>(); + } + + public List<String> getExternalHiveName() { + List<String> hiveNames = new LinkedList<String>(); + hiveNames.add(DEFAULT_HIVE_NAME); + if(this.externalHiveRootDir == null) + return hiveNames; + + //take every diectory in hive root dir is a hive source. take directory name as hive name + for(File file : this.externalHiveRootDir.listFiles()) { + if(!file.isDirectory()) { + logger.warn("File " + file.getAbsolutePath() + " in hive root directory is normal file."); + continue; + } + hiveNames.add(file.getName()); + } + return hiveNames; + } + + private void checkIsSupportExternalHive() { + if(this.externalHiveRootDir == null) { + throw new IllegalArgumentException(NOT_SUPPORT_ERROR_MESSAGE); + } + } + + public String getHiveConfigFile(String hiveName) { + if(hiveName == null) { + return null; + } + checkIsSupportExternalHive(); + + File hiveRootFile = new File(this.externalHiveRootDir, hiveName); + if(!(hiveRootFile.exists() && hiveRootFile.isDirectory())) { + throw new IllegalArgumentException("Hive " + hiveName + " root directory " + hiveRootFile.getAbsolutePath() + " do not exist."); + } + + File hiveConfigFile = new File(hiveRootFile, HIVE_CONFIG_FILE_LOCATION); + if(!(hiveConfigFile.exists() && hiveConfigFile.isFile())) { + throw new IllegalArgumentException("Hive " + hiveName + " config file " + hiveConfigFile.getAbsolutePath() + " do not exist."); + } + return hiveConfigFile.getAbsolutePath(); + } + + public HiveClient createHiveClient(String hiveName) { + //use internal hive client while do not appoint a hive + if(hiveName == null) { + if(defaultHiveClient == null) + defaultHiveClient = new ExternalHiveClient(null); + return this.defaultHiveClient; + } + checkIsSupportExternalHive(); + + HiveClient client = this.externalHiveMap.get(hiveName); + if(client != null) + return client; + String configFileLocation = getHiveConfigFile(hiveName); + if(configFileLocation == null) { + throw new IllegalArgumentException("Can not find hive " + hiveName + " config file in local hive root directory " + + this.externalHiveRootDir.getAbsolutePath()); + } + + try { + client = new ExternalHiveClient(configFileLocation); + this.externalHiveMap.put(hiveName, client); + } catch (Exception e) { + throw new IllegalArgumentException("Can not create hive client for " + hiveName + ", config file " + configFileLocation); + } + return client; + } + + public HiveClient createHiveClientWithConfig(Map<String, String> configMap, String hiveName) { + HiveClient client = this.createHiveClient(hiveName); + client.appendConfiguration(configMap); + return client; + } + + public String getHiveCommand(String hiveName) { + if(hiveName == null) { + return "hive"; + } + checkIsSupportExternalHive(); + + File hiveRootFile = new File(this.externalHiveRootDir, hiveName); + if(!(hiveRootFile.exists() && hiveRootFile.isDirectory())) { + throw new IllegalArgumentException("Hive " + hiveName + " root directory " + hiveRootFile.getAbsolutePath() + " do not exist."); + } + + File hiveCmdFile = new File(hiveRootFile, HIVE_COMMAND_LOCATION); + if(!(hiveCmdFile.exists() && hiveCmdFile.isFile())) { + throw new IllegalArgumentException("Hive " + hiveName + " bin file " + hiveCmdFile.getAbsolutePath() + " do not exist."); + } + return hiveCmdFile.getAbsolutePath(); + } + + public String getHiveTableLocation(String database, String tableName, String hiveName) throws Exception { + HiveClient hiveClient = this.createHiveClient(hiveName); + try { + String tableLocation = hiveClient.getHiveTableLocation(database, tableName); + return tableLocation; + } catch (Exception e) { + logger.error("Get hive " + hiveName + " table " + tableName + " location error !"); + throw e; + } + } + + public String getHiveTableLocation(String fullTableName, String hiveName) throws Exception { + String[] tables = HadoopUtil.parseHiveTableName(fullTableName); + String database = tables[0]; + String tableName = tables[1]; + return getHiveTableLocation(database, tableName, hiveName); + } + + public static void clearCache() { + HIVE_MANAGER_CACHE = null; + getInstance(); + } + + public boolean isSupportExternalHives() { + return this.externalHiveRootDir != null; + } + + public static boolean isSameHiveSource(String first, String second) { + if(first == null) { + return second == null; + } else { + return first.equalsIgnoreCase(second); + } + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/webapp/app/js/controllers/page.js ---------------------------------------------------------------------- diff --git a/webapp/app/js/controllers/page.js b/webapp/app/js/controllers/page.js index c65a264..c07cf70 100644 --- a/webapp/app/js/controllers/page.js +++ b/webapp/app/js/controllers/page.js @@ -215,6 +215,7 @@ var projCtrl = function ($scope, $location, $modalInstance, ProjectService, Mess var requestBody = { formerProjectName: $scope.state.oldProjName, newProjectName: $scope.proj.name, + newHiveName: $scope.proj.hive, newDescription: $scope.proj.description }; ProjectService.update({}, requestBody, function (newProj) { http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/webapp/app/js/controllers/sourceMeta.js ---------------------------------------------------------------------- diff --git a/webapp/app/js/controllers/sourceMeta.js b/webapp/app/js/controllers/sourceMeta.js index e3ab0ac..1c08aa0 100755 --- a/webapp/app/js/controllers/sourceMeta.js +++ b/webapp/app/js/controllers/sourceMeta.js @@ -177,7 +177,7 @@ KylinApp $scope.loadHive = function () { if($scope.hiveLoaded) return; - TableService.showHiveDatabases({}, function (databases) { + TableService.showHiveDatabases({project:$scope.projectName}, function (databases) { $scope.dbNum = databases.length; if (databases.length > 0) { $scope.hiveMap = {}; @@ -293,7 +293,7 @@ KylinApp $scope.showToggle = function(node) { if(node.expanded == false){ - TableService.showHiveTables({"database": node.label},function (hive_tables){ + TableService.showHiveTables({"database": node.label, project:$scope.projectName},function (hive_tables){ var tables = []; for (var i = 0; i < hive_tables.length; i++) { tables.push(hive_tables[i]); http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/webapp/app/js/model/projectConfig.js ---------------------------------------------------------------------- diff --git a/webapp/app/js/model/projectConfig.js b/webapp/app/js/model/projectConfig.js index 270eb7a..2a84b47 100644 --- a/webapp/app/js/model/projectConfig.js +++ b/webapp/app/js/model/projectConfig.js @@ -20,6 +20,7 @@ KylinApp.constant('projectConfig', { theaditems: [ {attr: 'name', name: 'Name'}, {attr: 'owner', name: 'Owner'}, + {attr: 'hive', name: 'Hive Name'}, {attr: 'description', name: 'Description'}, {attr: 'create_time', name: 'Create Time'} ] http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/webapp/app/partials/projects/project_create.html ---------------------------------------------------------------------- diff --git a/webapp/app/partials/projects/project_create.html b/webapp/app/partials/projects/project_create.html index 36cbaf5..f9e33ea 100644 --- a/webapp/app/partials/projects/project_create.html +++ b/webapp/app/partials/projects/project_create.html @@ -36,6 +36,15 @@ </div> </div> <div class="form-group"> + <label><b>Project External Hive</b></label> + + <div class="clearfix"> + <input name="description_input" type="text" class="form-control" + placeholder="External hive that the project take as input source, ignore it if you are using default hive." + ng-model="proj.hive"></textarea> + </div> + </div> + <div class="form-group"> <label><b>Project Description</b></label> <div class="clearfix"> http://git-wip-us.apache.org/repos/asf/kylin/blob/be7128bf/webapp/app/partials/projects/projects.html ---------------------------------------------------------------------- diff --git a/webapp/app/partials/projects/projects.html b/webapp/app/partials/projects/projects.html index 96e4a91..10b4dba 100644 --- a/webapp/app/partials/projects/projects.html +++ b/webapp/app/partials/projects/projects.html @@ -54,6 +54,7 @@ {{ project.name}} </td> <td>{{ project.owner}}</td> + <td>{{ project.hive}}</td> <td>{{ project.description}}</td> <td>{{ project.create_time_utc | utcToConfigTimeZone}}</td> <td>