12 changed files with 665 additions and 632 deletions
@ -0,0 +1,395 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.sync.vc; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.fasterxml.jackson.databind.ObjectWriter; |
||||
|
import com.fasterxml.jackson.databind.SerializationFeature; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.SneakyThrows; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.io.FileUtils; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.eclipse.jgit.api.errors.GitAPIException; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.AdminSettings; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.ExportableEntity; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.dao.settings.AdminSettingsService; |
||||
|
import org.thingsboard.server.queue.util.AfterStartUp; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
import org.thingsboard.server.service.sync.EntitiesExportImportService; |
||||
|
import org.thingsboard.server.service.sync.exporting.data.EntityExportData; |
||||
|
import org.thingsboard.server.service.sync.exporting.data.request.EntityExportSettings; |
||||
|
import org.thingsboard.server.service.sync.importing.data.EntityImportResult; |
||||
|
import org.thingsboard.server.service.sync.importing.data.EntityImportSettings; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntitiesVersionControlSettings; |
||||
|
import org.thingsboard.server.service.sync.vc.data.VersionedEntityInfo; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntityVersion; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntityVersionLoadResult; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntityVersionLoadSettings; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntityVersionSaveSettings; |
||||
|
import org.thingsboard.server.utils.GitRepository; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.io.IOException; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.nio.file.Files; |
||||
|
import java.nio.file.Path; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.Optional; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.concurrent.ScheduledExecutorService; |
||||
|
import java.util.concurrent.ScheduledFuture; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
import java.util.concurrent.locks.ReadWriteLock; |
||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Service |
||||
|
@TbCoreComponent |
||||
|
@RequiredArgsConstructor |
||||
|
@Slf4j |
||||
|
public class DefaultEntitiesVersionControlService implements EntitiesVersionControlService { |
||||
|
|
||||
|
private final EntitiesExportImportService exportImportService; |
||||
|
|
||||
|
private GitRepository repository; |
||||
|
private final ReadWriteLock repositoryLock = new ReentrantReadWriteLock(); |
||||
|
|
||||
|
private ScheduledExecutorService fetchExecutor; |
||||
|
private ScheduledFuture<?> fetchTask; |
||||
|
|
||||
|
private final AdminSettingsService adminSettingsService; |
||||
|
private static final String SETTINGS_KEY = "vc"; |
||||
|
private final ObjectWriter jsonWriter = new ObjectMapper().writer(SerializationFeature.INDENT_OUTPUT); |
||||
|
|
||||
|
|
||||
|
@AfterStartUp |
||||
|
public void init() { |
||||
|
EntitiesVersionControlSettings settings = getSettings(); |
||||
|
if (settings != null) { |
||||
|
try { |
||||
|
initRepository(settings); |
||||
|
} catch (Exception e) { |
||||
|
log.debug("Failed to init repository", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int fetchPeriod = settings == null || settings.getFetchPeriod() == 0 ? 10 : settings.getFetchPeriod(); |
||||
|
fetchExecutor = Executors.newSingleThreadScheduledExecutor(); |
||||
|
fetchTask = scheduleFetch(fetchPeriod); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public EntityVersion saveEntityVersion(SecurityUser user, EntityId entityId, String branch, String versionName, EntityVersionSaveSettings settings) throws Exception { |
||||
|
return saveEntitiesVersion(user, List.of(entityId), branch, versionName, settings); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public EntityVersion saveEntitiesVersion(SecurityUser user, List<EntityId> entitiesIds, String branch, String versionName, EntityVersionSaveSettings settings) throws Exception { |
||||
|
repositoryLock.writeLock().lock(); |
||||
|
try { |
||||
|
checkRepository(); |
||||
|
checkBranch(user.getTenantId(), branch); |
||||
|
|
||||
|
List<EntityExportData<?>> entityDataList = new ArrayList<>(); |
||||
|
EntityExportSettings exportSettings = EntityExportSettings.builder() |
||||
|
.exportRelations(settings.isSaveRelations()) |
||||
|
.build(); |
||||
|
for (EntityId entityId : entitiesIds) { |
||||
|
EntityExportData<ExportableEntity<EntityId>> entityData = exportImportService.exportEntity(user, entityId, exportSettings); |
||||
|
entityDataList.add(entityData); |
||||
|
} |
||||
|
|
||||
|
fetch(); |
||||
|
if (repository.listBranches().contains(branch)) { |
||||
|
repository.checkout(branch); |
||||
|
repository.merge(branch); |
||||
|
} else { |
||||
|
repository.createAndCheckoutOrphanBranch(branch); |
||||
|
} |
||||
|
|
||||
|
for (EntityExportData<?> entityData : entityDataList) { |
||||
|
String entityDataJson = jsonWriter.writeValueAsString(entityData); |
||||
|
FileUtils.write(new File(repository.getDirectory() + "/" + getRelativePath(entityData.getEntityType(), |
||||
|
entityData.getEntity().getId().toString())), entityDataJson, StandardCharsets.UTF_8); |
||||
|
} |
||||
|
|
||||
|
GitRepository.Commit commit = repository.commit(versionName, "."); |
||||
|
repository.push(); |
||||
|
return toVersion(commit); |
||||
|
} finally { |
||||
|
repositoryLock.writeLock().unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public List<EntityVersion> listEntityVersions(TenantId tenantId, String branch, EntityId externalId) throws Exception { |
||||
|
return listVersions(tenantId, branch, getRelativePath(externalId.getEntityType(), externalId.getId().toString())); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<EntityVersion> listEntityTypeVersions(TenantId tenantId, String branch, EntityType entityType) throws Exception { |
||||
|
return listVersions(tenantId, branch, getRelativePath(entityType, null)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<EntityVersion> listVersions(TenantId tenantId, String branch) throws Exception { |
||||
|
return listVersions(tenantId, branch, null); |
||||
|
} |
||||
|
|
||||
|
private List<EntityVersion> listVersions(TenantId tenantId, String branch, String path) throws Exception { |
||||
|
repositoryLock.readLock().lock(); |
||||
|
try { |
||||
|
checkRepository(); |
||||
|
checkBranch(tenantId, branch); |
||||
|
|
||||
|
return repository.listCommits(branch, path, Integer.MAX_VALUE).stream() |
||||
|
.map(this::toVersion) |
||||
|
.collect(Collectors.toList()); |
||||
|
} finally { |
||||
|
repositoryLock.readLock().unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public List<VersionedEntityInfo> listEntitiesAtVersion(TenantId tenantId, EntityType entityType, String branch, String versionId) throws Exception { |
||||
|
return listEntitiesAtVersion(tenantId, branch, versionId, getRelativePath(entityType, null)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<VersionedEntityInfo> listAllEntitiesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception { |
||||
|
return listEntitiesAtVersion(tenantId, branch, versionId, null); |
||||
|
} |
||||
|
|
||||
|
private List<VersionedEntityInfo> listEntitiesAtVersion(TenantId tenantId, String branch, String versionId, String path) throws Exception { |
||||
|
repositoryLock.readLock().lock(); |
||||
|
try { |
||||
|
checkRepository(); |
||||
|
checkBranch(tenantId, branch); |
||||
|
checkVersion(tenantId, branch, versionId, path); |
||||
|
|
||||
|
return repository.listFilesAtCommit(versionId, path).stream() |
||||
|
.map(filePath -> { |
||||
|
EntityId entityId = fromRelativePath(filePath); |
||||
|
EntityExportData<?> entityData = getEntityDataAtVersion(entityId, versionId); |
||||
|
|
||||
|
VersionedEntityInfo info = new VersionedEntityInfo(); |
||||
|
info.setExternalId(entityId); |
||||
|
info.setEntityName(entityData.getEntity().getName()); |
||||
|
return info; |
||||
|
}) |
||||
|
.collect(Collectors.toList()); |
||||
|
} finally { |
||||
|
repositoryLock.readLock().unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public EntityVersionLoadResult loadEntityVersion(SecurityUser user, EntityId externalId, String branch, String versionId, EntityVersionLoadSettings settings) throws Exception { |
||||
|
return loadAtVersion(user, branch, versionId, getRelativePath(externalId.getEntityType(), externalId.getId().toString()), settings).get(0); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<EntityVersionLoadResult> loadEntityTypeVersion(SecurityUser user, EntityType entityType, String branch, String versionId, EntityVersionLoadSettings settings) throws Exception { |
||||
|
return loadAtVersion(user, branch, versionId, getRelativePath(entityType, null), settings); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<EntityVersionLoadResult> loadAllAtVersion(SecurityUser user, String branch, String versionId, EntityVersionLoadSettings settings) throws Exception { |
||||
|
return loadAtVersion(user, branch, versionId, null, settings); |
||||
|
} |
||||
|
|
||||
|
private List<EntityVersionLoadResult> loadAtVersion(SecurityUser user, String branch, String versionId, String path, EntityVersionLoadSettings settings) throws Exception { |
||||
|
List<EntityExportData<?>> entityDataList = new ArrayList<>(); |
||||
|
repositoryLock.readLock().lock(); |
||||
|
try { |
||||
|
for (VersionedEntityInfo info : listEntitiesAtVersion(user.getTenantId(), branch, versionId, path)) { |
||||
|
EntityExportData<?> entityData = getEntityDataAtVersion(info.getExternalId(), versionId); |
||||
|
entityDataList.add(entityData); |
||||
|
} |
||||
|
} finally { |
||||
|
repositoryLock.readLock().unlock(); |
||||
|
} |
||||
|
|
||||
|
EntityImportSettings importSettings = EntityImportSettings.builder() |
||||
|
.updateRelations(settings.isLoadRelations()) |
||||
|
.findExistingByName(settings.isFindExistingEntityByName()) |
||||
|
.build(); |
||||
|
List<EntityImportResult<?>> importResults = exportImportService.importEntities(user, entityDataList, importSettings); |
||||
|
|
||||
|
return importResults.stream() |
||||
|
.map(importResult -> EntityVersionLoadResult.builder() |
||||
|
.previousEntityVersion(importResult.getOldEntity()) |
||||
|
.newEntityVersion(importResult.getSavedEntity()) |
||||
|
.entityType(importResult.getEntityType()) |
||||
|
.build()) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
@SneakyThrows |
||||
|
private EntityExportData<?> getEntityDataAtVersion(EntityId externalId, String versionId) { |
||||
|
repositoryLock.readLock().lock(); |
||||
|
try { |
||||
|
String entityDataJson = repository.getFileContentAtCommit(getRelativePath(externalId.getEntityType(), externalId.getId().toString()), versionId); |
||||
|
return JacksonUtil.fromString(entityDataJson, EntityExportData.class); |
||||
|
} finally { |
||||
|
repositoryLock.readLock().unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private void fetch() throws GitAPIException { |
||||
|
repositoryLock.writeLock().lock(); |
||||
|
try { |
||||
|
repository.fetch(); |
||||
|
} finally { |
||||
|
repositoryLock.writeLock().unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ScheduledFuture<?> scheduleFetch(int fetchPeriod) { |
||||
|
return fetchExecutor.scheduleWithFixedDelay(() -> { |
||||
|
if (repository == null) return; |
||||
|
try { |
||||
|
fetch(); |
||||
|
} catch (Exception e) { |
||||
|
log.error("Failed to fetch remote repository", e); |
||||
|
} |
||||
|
}, fetchPeriod, fetchPeriod, TimeUnit.SECONDS); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private void checkVersion(TenantId tenantId, String branch, String versionId, String path) throws Exception { |
||||
|
if (listVersions(tenantId, branch, path).stream().noneMatch(version -> version.getId().equals(versionId))) { |
||||
|
throw new IllegalArgumentException("Version not found"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<String> listAllowedBranches(TenantId tenantId) { |
||||
|
return Optional.ofNullable(getSettings()) |
||||
|
.flatMap(settings -> Optional.ofNullable(settings.getTenantsAllowedBranches())) |
||||
|
.flatMap(tenantsAllowedBranches -> Optional.ofNullable(tenantsAllowedBranches.get(tenantId.getId()))) |
||||
|
.orElse(Collections.emptyList()); |
||||
|
} |
||||
|
|
||||
|
private void checkBranch(TenantId tenantId, String branch) { |
||||
|
if (!listAllowedBranches(tenantId).contains(branch)) { |
||||
|
throw new IllegalArgumentException("Tenant does not have access to the branch"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private void checkRepository() { |
||||
|
if (repository == null) { |
||||
|
throw new IllegalStateException("Repository is not initialized"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void initRepository(EntitiesVersionControlSettings settings) throws Exception { |
||||
|
repositoryLock.writeLock().lock(); |
||||
|
try { |
||||
|
if (Files.exists(Path.of(settings.getRepositoryDirectory()))) { |
||||
|
this.repository = GitRepository.open(settings.getRepositoryDirectory(), settings.getUsername(), settings.getPassword()); |
||||
|
} else { |
||||
|
Files.createDirectories(Path.of(settings.getRepositoryDirectory())); |
||||
|
this.repository = GitRepository.clone(settings.getRepositoryUri(), settings.getRepositoryDirectory(), |
||||
|
settings.getUsername(), settings.getPassword()); |
||||
|
} |
||||
|
} finally { |
||||
|
repositoryLock.writeLock().unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void clearRepository() throws IOException { |
||||
|
repositoryLock.writeLock().lock(); |
||||
|
try { |
||||
|
if (repository != null) { |
||||
|
FileUtils.deleteDirectory(new File(repository.getDirectory())); |
||||
|
repository = null; |
||||
|
} |
||||
|
} finally { |
||||
|
repositoryLock.writeLock().unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@SneakyThrows |
||||
|
@Override |
||||
|
public void saveSettings(EntitiesVersionControlSettings settings) { |
||||
|
AdminSettings adminSettings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "vc")) |
||||
|
.orElseGet(() -> { |
||||
|
AdminSettings newAdminSettings = new AdminSettings(); |
||||
|
newAdminSettings.setKey(SETTINGS_KEY); |
||||
|
return newAdminSettings; |
||||
|
}); |
||||
|
adminSettings.setJsonValue(JacksonUtil.valueToTree(settings)); |
||||
|
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings); |
||||
|
|
||||
|
repositoryLock.writeLock().lock(); |
||||
|
try { |
||||
|
clearRepository(); |
||||
|
initRepository(settings); |
||||
|
} finally { |
||||
|
repositoryLock.writeLock().unlock(); |
||||
|
} |
||||
|
|
||||
|
if (settings.getFetchPeriod() != 0) { |
||||
|
fetchTask.cancel(true); |
||||
|
fetchTask = scheduleFetch(settings.getFetchPeriod()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public EntitiesVersionControlSettings getSettings() { |
||||
|
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "vc")) |
||||
|
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), EntitiesVersionControlSettings.class)) |
||||
|
.orElse(null); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private EntityVersion toVersion(GitRepository.Commit commit) { |
||||
|
return new EntityVersion(commit.getId(), commit.getMessage()); |
||||
|
} |
||||
|
|
||||
|
private String getRelativePath(EntityType entityType, String entityId) { |
||||
|
String path = entityType.name().toLowerCase(); |
||||
|
if (entityId != null) { |
||||
|
path += "/" + entityId + ".json"; |
||||
|
} |
||||
|
return path; |
||||
|
} |
||||
|
|
||||
|
private EntityId fromRelativePath(String path) { |
||||
|
EntityType entityType = EntityType.valueOf(StringUtils.substringBefore(path, "/")); |
||||
|
String entityId = StringUtils.substringBetween(path, "/", ".json"); |
||||
|
return EntityIdFactory.getByTypeAndUuid(entityType, entityId); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.sync.vc; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntitiesVersionControlSettings; |
||||
|
import org.thingsboard.server.service.sync.vc.data.VersionedEntityInfo; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntityVersion; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntityVersionLoadResult; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntityVersionLoadSettings; |
||||
|
import org.thingsboard.server.service.sync.vc.data.EntityVersionSaveSettings; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface EntitiesVersionControlService { |
||||
|
|
||||
|
EntityVersion saveEntityVersion(SecurityUser user, EntityId entityId, String branch, String versionName, EntityVersionSaveSettings settings) throws Exception; |
||||
|
|
||||
|
EntityVersion saveEntitiesVersion(SecurityUser user, List<EntityId> entitiesIds, String branch, String versionName, EntityVersionSaveSettings settings) throws Exception; |
||||
|
|
||||
|
|
||||
|
List<EntityVersion> listEntityVersions(TenantId tenantId, String branch, EntityId externalId) throws Exception; |
||||
|
|
||||
|
List<EntityVersion> listEntityTypeVersions(TenantId tenantId, String branch, EntityType entityType) throws Exception; |
||||
|
|
||||
|
List<EntityVersion> listVersions(TenantId tenantId, String branch) throws Exception; |
||||
|
|
||||
|
|
||||
|
List<VersionedEntityInfo> listEntitiesAtVersion(TenantId tenantId, EntityType entityType, String branch, String versionId) throws Exception; // will be good to return entity name also
|
||||
|
|
||||
|
List<VersionedEntityInfo> listAllEntitiesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception; |
||||
|
|
||||
|
|
||||
|
EntityVersionLoadResult loadEntityVersion(SecurityUser user, EntityId externalId, String branch, String versionId, EntityVersionLoadSettings settings) throws Exception; |
||||
|
|
||||
|
List<EntityVersionLoadResult> loadEntityTypeVersion(SecurityUser user, EntityType entityType, String branch, String versionId, EntityVersionLoadSettings settings) throws Exception; |
||||
|
|
||||
|
List<EntityVersionLoadResult> loadAllAtVersion(SecurityUser user, String branch, String versionId, EntityVersionLoadSettings settings) throws Exception; |
||||
|
|
||||
|
|
||||
|
List<String> listAllowedBranches(TenantId tenantId); |
||||
|
|
||||
|
|
||||
|
void saveSettings(EntitiesVersionControlSettings settings); |
||||
|
|
||||
|
EntitiesVersionControlSettings getSettings(); |
||||
|
|
||||
|
} |
||||
@ -1,358 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2022 The Thingsboard Authors |
|
||||
* |
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
* you may not use this file except in compliance with the License. |
|
||||
* You may obtain a copy of the License at |
|
||||
* |
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
* |
|
||||
* Unless required by applicable law or agreed to in writing, software |
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
* See the License for the specific language governing permissions and |
|
||||
* limitations under the License. |
|
||||
*/ |
|
||||
package org.thingsboard.server.service.sync.vcs; |
|
||||
|
|
||||
import com.fasterxml.jackson.core.type.TypeReference; |
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
|
||||
import com.fasterxml.jackson.databind.ObjectWriter; |
|
||||
import com.fasterxml.jackson.databind.SerializationFeature; |
|
||||
import lombok.RequiredArgsConstructor; |
|
||||
import lombok.extern.slf4j.Slf4j; |
|
||||
import org.apache.commons.io.FileUtils; |
|
||||
import org.eclipse.jgit.api.errors.GitAPIException; |
|
||||
import org.springframework.scheduling.annotation.Scheduled; |
|
||||
import org.springframework.stereotype.Service; |
|
||||
import org.thingsboard.common.util.JacksonUtil; |
|
||||
import org.thingsboard.server.common.data.AdminSettings; |
|
||||
import org.thingsboard.server.common.data.EntityType; |
|
||||
import org.thingsboard.server.common.data.ExportableEntity; |
|
||||
import org.thingsboard.server.common.data.id.EntityId; |
|
||||
import org.thingsboard.server.common.data.id.TenantId; |
|
||||
import org.thingsboard.server.dao.settings.AdminSettingsService; |
|
||||
import org.thingsboard.server.dao.tenant.TenantService; |
|
||||
import org.thingsboard.server.queue.util.AfterStartUp; |
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|
||||
import org.thingsboard.server.service.sync.EntitiesExportImportService; |
|
||||
import org.thingsboard.server.service.sync.exporting.EntityExportSettings; |
|
||||
import org.thingsboard.server.service.sync.exporting.data.EntityExportData; |
|
||||
import org.thingsboard.server.service.sync.importing.EntityImportResult; |
|
||||
import org.thingsboard.server.service.sync.importing.EntityImportSettings; |
|
||||
import org.thingsboard.server.service.sync.vcs.data.EntitiesVersionControlSettings; |
|
||||
import org.thingsboard.server.service.sync.vcs.data.EntityVersion; |
|
||||
import org.thingsboard.server.service.sync.vcs.data.GitSettings; |
|
||||
import org.thingsboard.server.utils.git.Repository; |
|
||||
import org.thingsboard.server.utils.git.data.Commit; |
|
||||
|
|
||||
import java.io.File; |
|
||||
import java.io.IOException; |
|
||||
import java.io.UncheckedIOException; |
|
||||
import java.nio.charset.StandardCharsets; |
|
||||
import java.nio.file.Files; |
|
||||
import java.nio.file.Path; |
|
||||
import java.util.Collections; |
|
||||
import java.util.List; |
|
||||
import java.util.Optional; |
|
||||
import java.util.Set; |
|
||||
import java.util.concurrent.locks.Lock; |
|
||||
import java.util.concurrent.locks.ReadWriteLock; |
|
||||
import java.util.concurrent.locks.ReentrantLock; |
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock; |
|
||||
import java.util.stream.Collectors; |
|
||||
|
|
||||
@Service |
|
||||
@TbCoreComponent |
|
||||
@RequiredArgsConstructor |
|
||||
@Slf4j |
|
||||
public class DefaultEntitiesVersionControlService implements EntitiesVersionControlService { |
|
||||
// TODO [viacheslav]: start up only on one of the cores
|
|
||||
|
|
||||
private final TenantService tenantService; |
|
||||
private final EntitiesExportImportService exportImportService; |
|
||||
private final AdminSettingsService adminSettingsService; |
|
||||
|
|
||||
private final ObjectWriter jsonWriter = new ObjectMapper().writer(SerializationFeature.INDENT_OUTPUT); |
|
||||
private static final String SETTINGS_KEY = "vc"; |
|
||||
|
|
||||
private Repository repository; |
|
||||
private final Lock fetchLock = new ReentrantLock(); |
|
||||
private final ReadWriteLock repositoryLock = new ReentrantReadWriteLock(); |
|
||||
|
|
||||
@AfterStartUp |
|
||||
public void init() throws Exception { |
|
||||
try { |
|
||||
EntitiesVersionControlSettings settings = getSettings(); |
|
||||
if (settings != null && settings.getGitSettings() != null) { |
|
||||
this.repository = initRepository(settings.getGitSettings()); |
|
||||
} |
|
||||
} catch (Exception e) { |
|
||||
log.error("Failed to initialize entities version control service", e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@Scheduled(initialDelay = 10 * 1000, fixedDelay = 10 * 1000) |
|
||||
private void fetch() throws Exception { |
|
||||
if (repository == null) return; |
|
||||
tryFetch(); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@Override |
|
||||
public EntityVersion saveEntityVersion(TenantId tenantId, EntityId entityId, String branch, String versionName) throws Exception { |
|
||||
return saveEntitiesVersion(tenantId, List.of(entityId), branch, versionName); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public EntityVersion saveEntitiesVersion(TenantId tenantId, List<EntityId> entitiesIds, String branch, String versionName) throws Exception { |
|
||||
checkRepository(); |
|
||||
checkBranch(tenantId, branch); |
|
||||
|
|
||||
EntityExportSettings exportSettings = EntityExportSettings.builder() |
|
||||
.exportInboundRelations(false) |
|
||||
.exportOutboundRelations(false) |
|
||||
.build(); |
|
||||
List<EntityExportData<ExportableEntity<EntityId>>> entityDataList = entitiesIds.stream() |
|
||||
.map(entityId -> exportImportService.exportEntity(tenantId, entityId, exportSettings)) |
|
||||
.collect(Collectors.toList()); |
|
||||
|
|
||||
tryFetch(); |
|
||||
|
|
||||
repositoryLock.writeLock().lock(); |
|
||||
try { |
|
||||
if (repository.listBranches().contains(branch)) { |
|
||||
repository.checkout(branch); |
|
||||
repository.merge(branch); |
|
||||
} else { |
|
||||
repository.createAndCheckoutOrphanBranch(branch); |
|
||||
} |
|
||||
|
|
||||
for (EntityExportData<ExportableEntity<EntityId>> entityData : entityDataList) { |
|
||||
String entityDataJson = jsonWriter.writeValueAsString(entityData); |
|
||||
FileUtils.write(new File(repository.getDirectory() + "/" + getRelativePathForEntity(entityData.getEntity().getId())), |
|
||||
entityDataJson, StandardCharsets.UTF_8); |
|
||||
} |
|
||||
|
|
||||
Commit commit = repository.commit(versionName, ".", "Tenant " + tenantId); |
|
||||
repository.push(); |
|
||||
return toVersion(commit); |
|
||||
} finally { |
|
||||
repositoryLock.writeLock().unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
@Override |
|
||||
public List<EntityVersion> listEntityVersions(TenantId tenantId, EntityId entityId, String branch, int limit) throws Exception { |
|
||||
return listVersions(tenantId, branch, getRelativePathForEntity(entityId), limit); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public List<EntityVersion> listEntityTypeVersions(TenantId tenantId, EntityType entityType, String branch, int limit) throws Exception { |
|
||||
return listVersions(tenantId, getRelativePathForEntityType(entityType), limit); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public List<EntityVersion> listVersions(TenantId tenantId, String branch, int limit) throws Exception { |
|
||||
return listVersions(tenantId, branch, null, limit); |
|
||||
} |
|
||||
|
|
||||
private List<EntityVersion> listVersions(TenantId tenantId, String branch, String path, int limit) throws Exception { |
|
||||
repositoryLock.readLock().lock(); |
|
||||
try { |
|
||||
checkRepository(); |
|
||||
checkBranch(tenantId, branch); |
|
||||
|
|
||||
return repository.listCommits(branch, path, limit).stream() |
|
||||
.map(this::toVersion) |
|
||||
.collect(Collectors.toList()); |
|
||||
|
|
||||
} finally { |
|
||||
repositoryLock.readLock().unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
@Override |
|
||||
public List<String> listFilesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception { |
|
||||
repositoryLock.readLock().lock(); |
|
||||
try { |
|
||||
if (listVersions(tenantId, branch, Integer.MAX_VALUE).stream() |
|
||||
.noneMatch(version -> version.getId().equals(versionId))) { |
|
||||
throw new IllegalArgumentException("Unknown version"); |
|
||||
} |
|
||||
return repository.listFilesAtCommit(versionId); |
|
||||
} finally { |
|
||||
repositoryLock.readLock().unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
@Override |
|
||||
public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> getEntityAtVersion(TenantId tenantId, I entityId, String branch, String versionId) throws Exception { |
|
||||
repositoryLock.readLock().lock(); |
|
||||
try { |
|
||||
if (listEntityVersions(tenantId, entityId, branch, Integer.MAX_VALUE).stream() |
|
||||
.noneMatch(version -> version.getId().equals(versionId))) { |
|
||||
throw new IllegalArgumentException("Unknown version"); |
|
||||
} |
|
||||
|
|
||||
String entityDataJson = repository.getFileContentAtCommit(getRelativePathForEntity(entityId), versionId); |
|
||||
return parseEntityData(entityDataJson); |
|
||||
} finally { |
|
||||
repositoryLock.readLock().unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@Override |
|
||||
public <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> loadEntityVersion(TenantId tenantId, I entityId, String branch, String versionId) throws Exception { |
|
||||
EntityExportData<E> entityData = getEntityAtVersion(tenantId, entityId, branch, versionId); |
|
||||
return exportImportService.importEntity(tenantId, entityData, EntityImportSettings.builder() |
|
||||
.importInboundRelations(false) |
|
||||
.importOutboundRelations(false) |
|
||||
.updateReferencesToOtherEntities(true) |
|
||||
.build()); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public List<EntityImportResult<ExportableEntity<EntityId>>> loadAllAtVersion(TenantId tenantId, String branch, String versionId) throws Exception { |
|
||||
repositoryLock.readLock().lock(); |
|
||||
try { |
|
||||
List<EntityExportData<ExportableEntity<EntityId>>> entityDataList = listFilesAtVersion(tenantId, branch, versionId).stream() |
|
||||
.map(entityDataFilePath -> { |
|
||||
String entityDataJson; |
|
||||
try { |
|
||||
entityDataJson = repository.getFileContentAtCommit(entityDataFilePath, versionId); |
|
||||
} catch (IOException e) { |
|
||||
throw new UncheckedIOException(e); |
|
||||
} |
|
||||
return parseEntityData(entityDataJson); |
|
||||
}) |
|
||||
.collect(Collectors.toList()); |
|
||||
|
|
||||
return exportImportService.importEntities(tenantId, entityDataList, EntityImportSettings.builder() |
|
||||
.importInboundRelations(false) |
|
||||
.importOutboundRelations(false) |
|
||||
.updateReferencesToOtherEntities(true) |
|
||||
.build()); |
|
||||
} finally { |
|
||||
repositoryLock.readLock().unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void tryFetch() throws GitAPIException { |
|
||||
repositoryLock.readLock().lock(); |
|
||||
try { |
|
||||
if (fetchLock.tryLock()) { |
|
||||
try { |
|
||||
log.info("Fetching remote repository"); |
|
||||
repository.fetch(); |
|
||||
} finally { |
|
||||
fetchLock.unlock(); |
|
||||
} |
|
||||
} |
|
||||
} finally { |
|
||||
repositoryLock.readLock().unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
private String getRelativePathForEntity(EntityId entityId) { |
|
||||
return getRelativePathForEntityType(entityId.getEntityType()) |
|
||||
+ "/" + entityId.getId() + ".json"; |
|
||||
} |
|
||||
|
|
||||
private String getRelativePathForEntityType(EntityType entityType) { |
|
||||
return entityType.name().toLowerCase(); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
private void checkBranch(TenantId tenantId, String branch) { |
|
||||
// TODO [viacheslav]: all branches are available by default?
|
|
||||
if (!getAllowedBranches(tenantId).contains(branch)) { |
|
||||
throw new IllegalArgumentException("Tenant does not have access to this branch"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public Set<String> getAllowedBranches(TenantId tenantId) { |
|
||||
return Optional.ofNullable(getSettings()) |
|
||||
.flatMap(settings -> Optional.ofNullable(settings.getAllowedBranches())) |
|
||||
.flatMap(tenantsAllowedBranches -> Optional.ofNullable(tenantsAllowedBranches.get(tenantId.getId()))) |
|
||||
.orElse(Collections.emptySet()); |
|
||||
} |
|
||||
|
|
||||
private EntityVersion toVersion(Commit commit) { |
|
||||
return new EntityVersion(commit.getId(), commit.getMessage(), commit.getAuthorName()); |
|
||||
} |
|
||||
|
|
||||
private <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> parseEntityData(String entityDataJson) { |
|
||||
return JacksonUtil.fromString(entityDataJson, new TypeReference<EntityExportData<E>>() {}); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
@Override |
|
||||
public void saveSettings(EntitiesVersionControlSettings settings) throws Exception { |
|
||||
repositoryLock.writeLock().lock(); |
|
||||
try { |
|
||||
this.repository = initRepository(settings.getGitSettings()); |
|
||||
} finally { |
|
||||
repositoryLock.writeLock().unlock(); |
|
||||
} |
|
||||
|
|
||||
AdminSettings adminSettings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, SETTINGS_KEY)) |
|
||||
.orElseGet(() -> { |
|
||||
AdminSettings newSettings = new AdminSettings(); |
|
||||
newSettings.setKey(SETTINGS_KEY); |
|
||||
return newSettings; |
|
||||
}); |
|
||||
adminSettings.setJsonValue(JacksonUtil.valueToTree(settings)); |
|
||||
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public EntitiesVersionControlSettings getSettings() { |
|
||||
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, SETTINGS_KEY)) |
|
||||
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), EntitiesVersionControlSettings.class)) |
|
||||
.orElse(null); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
private void checkRepository() { |
|
||||
if (repository == null) { |
|
||||
throw new IllegalStateException("Repository is not initialized"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static Repository initRepository(GitSettings gitSettings) throws Exception { |
|
||||
if (Files.exists(Path.of(gitSettings.getRepositoryDirectory()))) { |
|
||||
return Repository.open(gitSettings.getRepositoryDirectory(), |
|
||||
gitSettings.getUsername(), gitSettings.getPassword()); |
|
||||
} else { |
|
||||
Files.createDirectories(Path.of(gitSettings.getRepositoryDirectory())); |
|
||||
return Repository.clone(gitSettings.getRepositoryUri(), gitSettings.getRepositoryDirectory(), |
|
||||
gitSettings.getUsername(), gitSettings.getPassword()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void resetRepository() throws Exception { |
|
||||
repositoryLock.writeLock().lock(); |
|
||||
try { |
|
||||
if (this.repository != null) { |
|
||||
FileUtils.deleteDirectory(new File(repository.getDirectory())); |
|
||||
this.repository = null; |
|
||||
} |
|
||||
EntitiesVersionControlSettings settings = getSettings(); |
|
||||
this.repository = initRepository(settings.getGitSettings()); |
|
||||
} finally { |
|
||||
repositoryLock.writeLock().unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -1,57 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2022 The Thingsboard Authors |
|
||||
* |
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
* you may not use this file except in compliance with the License. |
|
||||
* You may obtain a copy of the License at |
|
||||
* |
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
* |
|
||||
* Unless required by applicable law or agreed to in writing, software |
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
* See the License for the specific language governing permissions and |
|
||||
* limitations under the License. |
|
||||
*/ |
|
||||
package org.thingsboard.server.service.sync.vcs; |
|
||||
|
|
||||
import org.thingsboard.server.common.data.EntityType; |
|
||||
import org.thingsboard.server.common.data.ExportableEntity; |
|
||||
import org.thingsboard.server.common.data.id.EntityId; |
|
||||
import org.thingsboard.server.common.data.id.TenantId; |
|
||||
import org.thingsboard.server.service.sync.exporting.data.EntityExportData; |
|
||||
import org.thingsboard.server.service.sync.importing.EntityImportResult; |
|
||||
import org.thingsboard.server.service.sync.vcs.data.EntitiesVersionControlSettings; |
|
||||
import org.thingsboard.server.service.sync.vcs.data.EntityVersion; |
|
||||
|
|
||||
import java.util.List; |
|
||||
|
|
||||
public interface EntitiesVersionControlService { |
|
||||
|
|
||||
EntityVersion saveEntityVersion(TenantId tenantId, EntityId entityId, String branch, String versionName) throws Exception; |
|
||||
|
|
||||
EntityVersion saveEntitiesVersion(TenantId tenantId, List<EntityId> entitiesIds, String branch, String versionName) throws Exception; |
|
||||
|
|
||||
|
|
||||
List<EntityVersion> listEntityVersions(TenantId tenantId, EntityId entityId, String branch, int limit) throws Exception; |
|
||||
|
|
||||
List<EntityVersion> listEntityTypeVersions(TenantId tenantId, EntityType entityType, String branch, int limit) throws Exception; |
|
||||
|
|
||||
List<EntityVersion> listVersions(TenantId tenantId, String branch, int limit) throws Exception; |
|
||||
|
|
||||
|
|
||||
List<String> listFilesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception; |
|
||||
|
|
||||
|
|
||||
<E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> getEntityAtVersion(TenantId tenantId, I entityId, String branch, String versionId) throws Exception; |
|
||||
|
|
||||
<E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> loadEntityVersion(TenantId tenantId, I entityId, String branch, String versionId) throws Exception; |
|
||||
|
|
||||
List<EntityImportResult<ExportableEntity<EntityId>>> loadAllAtVersion(TenantId tenantId, String branch, String versionId) throws Exception; |
|
||||
|
|
||||
|
|
||||
void saveSettings(EntitiesVersionControlSettings settings) throws Exception; |
|
||||
|
|
||||
EntitiesVersionControlSettings getSettings(); |
|
||||
|
|
||||
} |
|
||||
Loading…
Reference in new issue