ArtifactRepository tenant aware. (#539)
* ArtifactRepository tenant aware. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * No need to have this protected. Updated event to boot > 1.3 Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Remove conditional. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Remove Debug log. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Cleanup Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Missing validation and readability. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Fix test after change. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Fix computation is DosFilter Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Fix session state on RESTful APIs. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Performance improvement controllermanagement Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Added cross tenant test. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Typos. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>
This commit is contained in:
@@ -16,8 +16,8 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.context.event.ContextRefreshedEvent;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +26,7 @@ import org.springframework.stereotype.Component;
|
|||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@ConditionalOnProperty(prefix = "hawkbit.device.simulator", name = "autostart", matchIfMissing = true)
|
@ConditionalOnProperty(prefix = "hawkbit.device.simulator", name = "autostart", matchIfMissing = true)
|
||||||
public class SimulatorStartup implements ApplicationListener<ContextRefreshedEvent> {
|
public class SimulatorStartup implements ApplicationListener<ApplicationReadyEvent> {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(SimulatorStartup.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(SimulatorStartup.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -42,7 +42,7 @@ public class SimulatorStartup implements ApplicationListener<ContextRefreshedEve
|
|||||||
private AmqpProperties amqpProperties;
|
private AmqpProperties amqpProperties;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(final ContextRefreshedEvent event) {
|
public void onApplicationEvent(final ApplicationReadyEvent event) {
|
||||||
simulationProperties.getAutostarts().forEach(autostart -> {
|
simulationProperties.getAutostarts().forEach(autostart -> {
|
||||||
for (int i = 0; i < autostart.getAmount(); i++) {
|
for (int i = 0; i < autostart.getAmount(); i++) {
|
||||||
final String deviceId = autostart.getName() + i;
|
final String deviceId = autostart.getName() + i;
|
||||||
|
|||||||
@@ -54,6 +54,11 @@
|
|||||||
<artifactId>allure-junit-adaptor</artifactId>
|
<artifactId>allure-junit-adaptor</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-logging</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<!-- MongoDB -->
|
<!-- MongoDB -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>de.flapdoodle.embed</groupId>
|
<groupId>de.flapdoodle.embed</groupId>
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||||
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
|
||||||
import org.springframework.data.mongodb.core.query.Criteria;
|
import org.springframework.data.mongodb.core.query.Criteria;
|
||||||
import org.springframework.data.mongodb.core.query.Query;
|
import org.springframework.data.mongodb.core.query.Query;
|
||||||
import org.springframework.data.mongodb.gridfs.GridFsOperations;
|
import org.springframework.data.mongodb.gridfs.GridFsOperations;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
@@ -44,6 +44,7 @@ import com.mongodb.gridfs.GridFSFile;
|
|||||||
* The file management which looks up all the file in the file tore.
|
* The file management which looks up all the file in the file tore.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@Validated
|
||||||
public class MongoDBArtifactStore implements ArtifactRepository {
|
public class MongoDBArtifactStore implements ArtifactRepository {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(MongoDBArtifactStore.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(MongoDBArtifactStore.class);
|
||||||
@@ -56,9 +57,14 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
|||||||
private static final String FILENAME = "filename";
|
private static final String FILENAME = "filename";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mongoDB field which automatically calculated by the mongoDB.
|
* The mongoDB field which holds the tenant of the file to download.
|
||||||
*/
|
*/
|
||||||
private static final String MD5 = "md5";
|
private static final String TENANT = "tenant";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query by {@link TenantAware} field.
|
||||||
|
*/
|
||||||
|
private static final String TENANT_QUERY = "metadata." + TENANT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mongoDB field which holds the SHA1 hash, stored in the meta data
|
* The mongoDB field which holds the SHA1 hash, stored in the meta data
|
||||||
@@ -68,10 +74,11 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
|||||||
|
|
||||||
private static final String ID = "_id";
|
private static final String ID = "_id";
|
||||||
|
|
||||||
@Autowired
|
private final GridFsOperations gridFs;
|
||||||
private GridFsOperations gridFs;
|
|
||||||
|
|
||||||
MongoTemplate mongoTemplate;
|
MongoDBArtifactStore(final GridFsOperations gridFs) {
|
||||||
|
this.gridFs = gridFs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a {@link GridFSDBFile} from the store by it's SHA1 hash.
|
* Retrieves a {@link GridFSDBFile} from the store by it's SHA1 hash.
|
||||||
@@ -82,36 +89,28 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
|||||||
* @return The DbArtifact object or {@code null} if no file exists.
|
* @return The DbArtifact object or {@code null} if no file exists.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public DbArtifact getArtifactBySha1(final String sha1Hash) {
|
public DbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
|
||||||
return map(gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash))));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return map(gridFs.findOne(new Query()
|
||||||
* Retrieves a {@link GridFSDBFile} from the store by it's MD5 hash.
|
.addCriteria(Criteria.where(FILENAME).is(sha1Hash).and(TENANT_QUERY).is(sanitizeTenant(tenant)))));
|
||||||
*
|
|
||||||
* @param md5Hash
|
|
||||||
* the md5-hash of the file to lookup.
|
|
||||||
* @return The gridfs file object or {@code null} if no file exists.
|
|
||||||
*/
|
|
||||||
public DbArtifact getArtifactByMd5(final String md5Hash) {
|
|
||||||
return map(gridFs.findOne(new Query().addCriteria(Criteria.where(MD5).is(md5Hash))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DbArtifact store(final InputStream content, final String filename, final String contentType) {
|
public DbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||||
return store(content, filename, contentType, null);
|
final String contentType) {
|
||||||
|
return store(tenant, content, filename, contentType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DbArtifact store(final InputStream content, final String filename, final String contentType,
|
public DbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||||
final DbArtifactHash hash) {
|
final String contentType, final DbArtifactHash hash) {
|
||||||
File tempFile = null;
|
File tempFile = null;
|
||||||
try {
|
try {
|
||||||
LOGGER.debug("storing file {} of content {}", filename, contentType);
|
LOGGER.debug("storing file {} of content {}", filename, contentType);
|
||||||
tempFile = File.createTempFile("uploadFile", null);
|
tempFile = File.createTempFile("uploadFile", null);
|
||||||
try (final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempFile))) {
|
try (final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempFile))) {
|
||||||
try (BufferedInputStream bis = new BufferedInputStream(content)) {
|
try (BufferedInputStream bis = new BufferedInputStream(content)) {
|
||||||
return store(bis, contentType, bos, tempFile, hash);
|
return store(tenant, bis, contentType, bos, tempFile, hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (final IOException | MongoException e1) {
|
} catch (final IOException | MongoException e1) {
|
||||||
@@ -123,10 +122,15 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String sanitizeTenant(final String tenant) {
|
||||||
|
return tenant.trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteBySha1(final String sha1Hash) {
|
public void deleteBySha1(final String tenant, final String sha1Hash) {
|
||||||
try {
|
try {
|
||||||
deleteArtifact(gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash))));
|
deleteArtifact(gridFs.findOne(new Query()
|
||||||
|
.addCriteria(Criteria.where(FILENAME).is(sha1Hash).and(TENANT_QUERY).is(sanitizeTenant(tenant)))));
|
||||||
} catch (final MongoException e) {
|
} catch (final MongoException e) {
|
||||||
throw new ArtifactStoreException(e.getMessage(), e);
|
throw new ArtifactStoreException(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@@ -143,18 +147,21 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DbArtifact store(final InputStream content, final String contentType, final OutputStream os,
|
private DbArtifact store(final String t, final InputStream content, final String contentType, final OutputStream os,
|
||||||
final File tempFile, final DbArtifactHash hash) {
|
final File tempFile, final DbArtifactHash hash) {
|
||||||
final GridFsArtifact storedArtifact;
|
final GridFsArtifact storedArtifact;
|
||||||
|
final String tenant = sanitizeTenant(t);
|
||||||
try {
|
try {
|
||||||
final String sha1Hash = computeSHA1Hash(content, os, hash != null ? hash.getSha1() : null);
|
final String sha1Hash = computeSHA1Hash(content, os, hash != null ? hash.getSha1() : null);
|
||||||
// upload if it does not exist already, check if file exists, not
|
// upload if it does not exist already, check if file exists, not
|
||||||
// tenant specific.
|
// tenant specific.
|
||||||
final GridFSDBFile result = gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash)));
|
final GridFSDBFile result = gridFs.findOne(
|
||||||
if (null == result) {
|
new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash).and(TENANT_QUERY).is(tenant)));
|
||||||
|
if (result == null) {
|
||||||
try (FileInputStream inputStream = new FileInputStream(tempFile)) {
|
try (FileInputStream inputStream = new FileInputStream(tempFile)) {
|
||||||
final BasicDBObject metadata = new BasicDBObject();
|
final BasicDBObject metadata = new BasicDBObject();
|
||||||
metadata.put(SHA1, sha1Hash);
|
metadata.put(SHA1, sha1Hash);
|
||||||
|
metadata.put(TENANT, tenant);
|
||||||
storedArtifact = map(gridFs.store(inputStream, sha1Hash, contentType, metadata));
|
storedArtifact = map(gridFs.store(inputStream, sha1Hash, contentType, metadata));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -244,4 +251,13 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
|||||||
artifact.setHashes(new DbArtifactHash(fsFile.getFilename(), fsFile.getMD5()));
|
artifact.setHashes(new DbArtifactHash(fsFile.getFilename(), fsFile.getMD5()));
|
||||||
return artifact;
|
return artifact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByTenant(final String tenant) {
|
||||||
|
try {
|
||||||
|
gridFs.delete(new Query().addCriteria(Criteria.where(TENANT_QUERY).is(sanitizeTenant(tenant))));
|
||||||
|
} catch (final MongoClientException e) {
|
||||||
|
throw new ArtifactStoreException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
import org.springframework.data.mongodb.gridfs.GridFsOperations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto configuration for the {@link MongoDBArtifactStore}.
|
* Auto configuration for the {@link MongoDBArtifactStore}.
|
||||||
@@ -25,7 +26,7 @@ public class MongoDBArtifactStoreAutoConfiguration {
|
|||||||
* @return Default {@link ArtifactRepository} implementation.
|
* @return Default {@link ArtifactRepository} implementation.
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public ArtifactRepository artifactRepository() {
|
ArtifactRepository artifactRepository(final GridFsOperations gridFs) {
|
||||||
return new MongoDBArtifactStore();
|
return new MongoDBArtifactStore(gridFs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,54 +20,75 @@ import org.eclipse.hawkbit.artifact.TestConfiguration;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.data.mongodb.gridfs.GridFsOperations;
|
|
||||||
import org.springframework.test.context.TestPropertySource;
|
|
||||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
|
|
||||||
import ru.yandex.qatools.allure.annotations.Description;
|
import ru.yandex.qatools.allure.annotations.Description;
|
||||||
import ru.yandex.qatools.allure.annotations.Features;
|
import ru.yandex.qatools.allure.annotations.Features;
|
||||||
|
import ru.yandex.qatools.allure.annotations.Step;
|
||||||
import ru.yandex.qatools.allure.annotations.Stories;
|
import ru.yandex.qatools.allure.annotations.Stories;
|
||||||
|
|
||||||
@Features("Component Tests - Repository")
|
@Features("Component Tests - Repository")
|
||||||
@Stories("Artifact Store MongoDB")
|
@Stories("Artifact Store MongoDB")
|
||||||
@RunWith(SpringJUnit4ClassRunner.class)
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
@SpringApplicationConfiguration(classes = { MongoDBArtifactStoreAutoConfiguration.class, TestConfiguration.class })
|
@SpringBootTest(classes = { MongoDBArtifactStoreAutoConfiguration.class, TestConfiguration.class }, properties = {
|
||||||
@TestPropertySource(properties = { "spring.data.mongodb.port=0", "spring.mongodb.embedded.version=3.2.7" })
|
"spring.data.mongodb.port=0", "spring.mongodb.embedded.version=3.4.4" })
|
||||||
public class MongoDBArtifactStoreTest {
|
public class MongoDBArtifactStoreTest {
|
||||||
|
private static final String TENANT = "test_tenant";
|
||||||
|
private static final String TENANT2 = "test_tenant2";
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MongoDBArtifactStore artifactStoreUnderTest;
|
private MongoDBArtifactStore artifactStoreUnderTest;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private GridFsOperations gridFs;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Description("Ensures that search by SHA1 hash (which is used by hawkBit as artifact ID) finds the expected results.")
|
@Description("Ensures that search by SHA1 hash (which is used by hawkBit as artifact ID) finds the expected results.")
|
||||||
public void findArtifactBySHA1Hash() throws NoSuchAlgorithmException {
|
public void findArtifactBySHA1Hash() throws NoSuchAlgorithmException {
|
||||||
|
|
||||||
|
final String sha1 = storeRandomArifactAndVerify(TENANT);
|
||||||
|
final String sha2 = storeRandomArifactAndVerify(TENANT2);
|
||||||
|
|
||||||
|
assertThat(artifactStoreUnderTest.getArtifactBySha1(TENANT2, sha1)).isNull();
|
||||||
|
assertThat(artifactStoreUnderTest.getArtifactBySha1(TENANT, sha2)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Step
|
||||||
|
private String storeRandomArifactAndVerify(final String tenant) throws NoSuchAlgorithmException {
|
||||||
final int filelengthBytes = 128;
|
final int filelengthBytes = 128;
|
||||||
final String filename = "testfile.json";
|
final String filename = "testfile.json";
|
||||||
final String contentType = "application/json";
|
final String contentType = "application/json";
|
||||||
|
|
||||||
final DigestInputStream digestInputStream = digestInputStream(generateInputStream(filelengthBytes), "SHA-1");
|
final DigestInputStream digestInputStream = digestInputStream(generateInputStream(filelengthBytes), "SHA-1");
|
||||||
artifactStoreUnderTest.store(digestInputStream, filename, contentType);
|
artifactStoreUnderTest.store(tenant, digestInputStream, filename, contentType);
|
||||||
assertThat(artifactStoreUnderTest.getArtifactBySha1(
|
|
||||||
BaseEncoding.base16().lowerCase().encode(digestInputStream.getMessageDigest().digest()))).isNotNull();
|
final String sha1 = BaseEncoding.base16().lowerCase().encode(digestInputStream.getMessageDigest().digest());
|
||||||
|
assertThat(artifactStoreUnderTest.getArtifactBySha1(tenant, sha1)).isNotNull();
|
||||||
|
return sha1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Description("Ensures that search by MD5 hash finds the expected results.")
|
@Description("Deletes file from repository identified by SHA1 hash as filename.")
|
||||||
public void findArtifactByMD5Hash() throws NoSuchAlgorithmException {
|
public void deleteArtifactBySHA1Hash() throws NoSuchAlgorithmException {
|
||||||
final int filelengthBytes = 128;
|
|
||||||
final String filename = "testfile.json";
|
|
||||||
final String contentType = "application/json";
|
|
||||||
|
|
||||||
final DigestInputStream digestInputStream = digestInputStream(generateInputStream(filelengthBytes), "MD5");
|
final String sha1 = storeRandomArifactAndVerify(TENANT);
|
||||||
artifactStoreUnderTest.store(digestInputStream, filename, contentType);
|
|
||||||
assertThat(artifactStoreUnderTest.getArtifactByMd5(
|
artifactStoreUnderTest.deleteBySha1(TENANT, sha1);
|
||||||
BaseEncoding.base16().lowerCase().encode(digestInputStream.getMessageDigest().digest()))).isNotNull();
|
assertThat(artifactStoreUnderTest.getArtifactBySha1(TENANT, sha1)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verfies that all data of a tenant is erased if repository is asked to do so. "
|
||||||
|
+ "Data of other tenants is not affected.")
|
||||||
|
public void deleteTenant() throws NoSuchAlgorithmException {
|
||||||
|
|
||||||
|
final String shaDeleted = storeRandomArifactAndVerify(TENANT);
|
||||||
|
final String shaUndeleted = storeRandomArifactAndVerify("another_tenant");
|
||||||
|
|
||||||
|
artifactStoreUnderTest.deleteByTenant("tenant_that_does_not_exist");
|
||||||
|
artifactStoreUnderTest.deleteByTenant(TENANT);
|
||||||
|
assertThat(artifactStoreUnderTest.getArtifactBySha1(TENANT, shaDeleted)).isNull();
|
||||||
|
assertThat(artifactStoreUnderTest.getArtifactBySha1("another_tenant", shaUndeleted)).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteArrayInputStream generateInputStream(final int length) {
|
private static ByteArrayInputStream generateInputStream(final int length) {
|
||||||
|
|||||||
@@ -22,22 +22,22 @@ public class S3Artifact extends DbArtifact {
|
|||||||
|
|
||||||
private final AmazonS3 amazonS3;
|
private final AmazonS3 amazonS3;
|
||||||
private final S3RepositoryProperties s3Properties;
|
private final S3RepositoryProperties s3Properties;
|
||||||
private final String sha1;
|
private final String key;
|
||||||
|
|
||||||
S3Artifact(final AmazonS3 amazonS3, final S3RepositoryProperties s3Properties, final String sha1) {
|
S3Artifact(final AmazonS3 amazonS3, final S3RepositoryProperties s3Properties, final String key) {
|
||||||
this.amazonS3 = amazonS3;
|
this.amazonS3 = amazonS3;
|
||||||
this.s3Properties = s3Properties;
|
this.s3Properties = s3Properties;
|
||||||
this.sha1 = sha1;
|
this.key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getFileInputStream() {
|
public InputStream getFileInputStream() {
|
||||||
return amazonS3.getObject(s3Properties.getBucketName(), sha1).getObjectContent();
|
return amazonS3.getObject(s3Properties.getBucketName(), key).getObjectContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "S3Artifact [sha1=" + sha1 + ", getArtifactId()=" + getArtifactId() + ", getHashes()=" + getHashes()
|
return "S3Artifact [key=" + key + ", getArtifactId()=" + getArtifactId() + ", getHashes()=" + getHashes()
|
||||||
+ ", getSize()=" + getSize() + ", getContentType()=" + getContentType() + "]";
|
+ ", getSize()=" + getSize() + ", getContentType()=" + getContentType() + "]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,17 @@ import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
|||||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
import com.amazonaws.AmazonClientException;
|
import com.amazonaws.AmazonClientException;
|
||||||
import com.amazonaws.RequestClientOptions;
|
import com.amazonaws.RequestClientOptions;
|
||||||
import com.amazonaws.services.s3.AmazonS3;
|
import com.amazonaws.services.s3.AmazonS3;
|
||||||
import com.amazonaws.services.s3.Headers;
|
import com.amazonaws.services.s3.Headers;
|
||||||
import com.amazonaws.services.s3.model.DeleteObjectRequest;
|
import com.amazonaws.services.s3.model.DeleteObjectRequest;
|
||||||
|
import com.amazonaws.services.s3.model.ObjectListing;
|
||||||
import com.amazonaws.services.s3.model.ObjectMetadata;
|
import com.amazonaws.services.s3.model.ObjectMetadata;
|
||||||
import com.amazonaws.services.s3.model.S3Object;
|
import com.amazonaws.services.s3.model.S3Object;
|
||||||
|
import com.amazonaws.services.s3.model.S3ObjectSummary;
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
|
|
||||||
@@ -48,6 +51,7 @@ import com.google.common.io.ByteStreams;
|
|||||||
* across several buckets.
|
* across several buckets.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
|
@Validated
|
||||||
public class S3Repository implements ArtifactRepository {
|
public class S3Repository implements ArtifactRepository {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(S3Repository.class);
|
private static final Logger LOG = LoggerFactory.getLogger(S3Repository.class);
|
||||||
@@ -73,16 +77,17 @@ public class S3Repository implements ArtifactRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DbArtifact store(final InputStream content, final String filename, final String contentType) {
|
public DbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||||
return store(content, filename, contentType, null);
|
final String contentType) {
|
||||||
|
return store(tenant, content, filename, contentType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5
|
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5
|
||||||
// is not used security related
|
// is not used security related
|
||||||
@SuppressWarnings("squid:S2070")
|
@SuppressWarnings("squid:S2070")
|
||||||
public DbArtifact store(final InputStream content, final String filename, final String contentType,
|
public DbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||||
final DbArtifactHash hash) {
|
final String contentType, final DbArtifactHash hash) {
|
||||||
final MessageDigest mdSHA1;
|
final MessageDigest mdSHA1;
|
||||||
final MessageDigest mdMD5;
|
final MessageDigest mdMD5;
|
||||||
try {
|
try {
|
||||||
@@ -102,7 +107,7 @@ public class S3Repository implements ArtifactRepository {
|
|||||||
final String sha1Hash16 = BaseEncoding.base16().lowerCase().encode(mdSHA1.digest());
|
final String sha1Hash16 = BaseEncoding.base16().lowerCase().encode(mdSHA1.digest());
|
||||||
final String md5Hash16 = BaseEncoding.base16().lowerCase().encode(mdMD5.digest());
|
final String md5Hash16 = BaseEncoding.base16().lowerCase().encode(mdMD5.digest());
|
||||||
|
|
||||||
return store(sha1Hash16, md5Hash16, contentType, file, hash);
|
return store(tenant, sha1Hash16, md5Hash16, contentType, file, hash);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
throw new ArtifactStoreException(e.getMessage(), e);
|
throw new ArtifactStoreException(e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -112,15 +117,16 @@ public class S3Repository implements ArtifactRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DbArtifact store(final String sha1Hash16, final String mdMD5Hash16, final String contentType,
|
private DbArtifact store(final String tenant, final String sha1Hash16, final String mdMD5Hash16,
|
||||||
final File file, final DbArtifactHash hash) {
|
final String contentType, final File file, final DbArtifactHash hash) {
|
||||||
final S3Artifact s3Artifact = createS3Artifact(sha1Hash16, mdMD5Hash16, contentType, file);
|
final S3Artifact s3Artifact = createS3Artifact(tenant, sha1Hash16, mdMD5Hash16, contentType, file);
|
||||||
checkHashes(s3Artifact, hash);
|
checkHashes(s3Artifact, hash);
|
||||||
|
final String key = objectKey(tenant, sha1Hash16);
|
||||||
|
|
||||||
LOG.info("Storing file {} with length {} to AWS S3 bucket {} as SHA1 {}", file.getName(), file.length(),
|
LOG.info("Storing file {} with length {} to AWS S3 bucket {} as SHA1 {}", file.getName(), file.length(),
|
||||||
s3Properties.getBucketName(), sha1Hash16);
|
s3Properties.getBucketName(), key);
|
||||||
|
|
||||||
if (exists(sha1Hash16)) {
|
if (exists(key)) {
|
||||||
LOG.debug("Artifact {} already exists on S3 bucket {}, don't need to upload twice", sha1Hash16,
|
LOG.debug("Artifact {} already exists on S3 bucket {}, don't need to upload twice", sha1Hash16,
|
||||||
s3Properties.getBucketName());
|
s3Properties.getBucketName());
|
||||||
return s3Artifact;
|
return s3Artifact;
|
||||||
@@ -129,7 +135,7 @@ public class S3Repository implements ArtifactRepository {
|
|||||||
try (final InputStream inputStream = new BufferedInputStream(new FileInputStream(file),
|
try (final InputStream inputStream = new BufferedInputStream(new FileInputStream(file),
|
||||||
RequestClientOptions.DEFAULT_STREAM_BUFFER_SIZE)) {
|
RequestClientOptions.DEFAULT_STREAM_BUFFER_SIZE)) {
|
||||||
final ObjectMetadata objectMetadata = createObjectMetadata(mdMD5Hash16, contentType, file);
|
final ObjectMetadata objectMetadata = createObjectMetadata(mdMD5Hash16, contentType, file);
|
||||||
amazonS3.putObject(s3Properties.getBucketName(), sha1Hash16, inputStream, objectMetadata);
|
amazonS3.putObject(s3Properties.getBucketName(), key, inputStream, objectMetadata);
|
||||||
|
|
||||||
return s3Artifact;
|
return s3Artifact;
|
||||||
} catch (final IOException | AmazonClientException e) {
|
} catch (final IOException | AmazonClientException e) {
|
||||||
@@ -137,14 +143,15 @@ public class S3Repository implements ArtifactRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private S3Artifact createS3Artifact(final String sha1Hash16, final String mdMD5Hash16, final String contentType,
|
private S3Artifact createS3Artifact(final String tenant, final String sha1Hash, final String mdMD5Hash16,
|
||||||
final File file) {
|
final String contentType, final File file) {
|
||||||
final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, sha1Hash16);
|
final String key = objectKey(tenant, sha1Hash);
|
||||||
|
final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, key);
|
||||||
s3Artifact.setContentType(contentType);
|
s3Artifact.setContentType(contentType);
|
||||||
s3Artifact.setArtifactId(sha1Hash16);
|
s3Artifact.setArtifactId(sha1Hash);
|
||||||
s3Artifact.setSize(file.length());
|
s3Artifact.setSize(file.length());
|
||||||
s3Artifact.setContentType(contentType);
|
s3Artifact.setContentType(contentType);
|
||||||
s3Artifact.setHashes(new DbArtifactHash(sha1Hash16, mdMD5Hash16));
|
s3Artifact.setHashes(new DbArtifactHash(sha1Hash, mdMD5Hash16));
|
||||||
return s3Artifact;
|
return s3Artifact;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,26 +169,34 @@ public class S3Repository implements ArtifactRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteBySha1(final String sha1Hash) {
|
public void deleteBySha1(final String tenant, final String sha1Hash) {
|
||||||
LOG.info("Deleting S3 object from bucket {} and key {}", s3Properties.getBucketName(), sha1Hash);
|
final String key = objectKey(tenant, sha1Hash);
|
||||||
amazonS3.deleteObject(new DeleteObjectRequest(s3Properties.getBucketName(), sha1Hash));
|
|
||||||
|
LOG.info("Deleting S3 object from bucket {} and key {}", s3Properties.getBucketName(), key);
|
||||||
|
amazonS3.deleteObject(new DeleteObjectRequest(s3Properties.getBucketName(), key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String objectKey(final String tenant, final String sha1Hash) {
|
||||||
|
return sanitizeTenant(tenant) + "/" + sha1Hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DbArtifact getArtifactBySha1(final String sha1) {
|
public DbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
|
||||||
LOG.info("Retrieving S3 object from bucket {} and key {}", s3Properties.getBucketName(), sha1);
|
final String key = objectKey(tenant, sha1Hash);
|
||||||
final S3Object s3Object = amazonS3.getObject(s3Properties.getBucketName(), sha1);
|
|
||||||
|
LOG.info("Retrieving S3 object from bucket {} and key {}", s3Properties.getBucketName(), key);
|
||||||
|
final S3Object s3Object = amazonS3.getObject(s3Properties.getBucketName(), key);
|
||||||
if (s3Object == null) {
|
if (s3Object == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ObjectMetadata s3ObjectMetadata = s3Object.getObjectMetadata();
|
final ObjectMetadata s3ObjectMetadata = s3Object.getObjectMetadata();
|
||||||
|
|
||||||
final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, sha1);
|
final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, key);
|
||||||
s3Artifact.setArtifactId(sha1);
|
s3Artifact.setArtifactId(sha1Hash);
|
||||||
s3Artifact.setSize(s3ObjectMetadata.getContentLength());
|
s3Artifact.setSize(s3ObjectMetadata.getContentLength());
|
||||||
// the MD5Content is stored in the ETag
|
// the MD5Content is stored in the ETag
|
||||||
s3Artifact.setHashes(new DbArtifactHash(sha1,
|
s3Artifact.setHashes(new DbArtifactHash(sha1Hash,
|
||||||
BaseEncoding.base16().lowerCase().encode(BaseEncoding.base64().decode(s3ObjectMetadata.getETag()))));
|
BaseEncoding.base16().lowerCase().encode(BaseEncoding.base64().decode(s3ObjectMetadata.getETag()))));
|
||||||
s3Artifact.setContentType(s3ObjectMetadata.getContentType());
|
s3Artifact.setContentType(s3ObjectMetadata.getContentType());
|
||||||
return s3Artifact;
|
return s3Artifact;
|
||||||
@@ -220,4 +235,25 @@ public class S3Repository implements ArtifactRepository {
|
|||||||
private boolean exists(final String sha1) {
|
private boolean exists(final String sha1) {
|
||||||
return amazonS3.doesObjectExist(s3Properties.getBucketName(), sha1);
|
return amazonS3.doesObjectExist(s3Properties.getBucketName(), sha1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByTenant(final String tenant) {
|
||||||
|
final String folder = sanitizeTenant(tenant);
|
||||||
|
|
||||||
|
LOG.info("Deleting S3 object folder (tenant) from bucket {} and key {}", s3Properties.getBucketName(), folder);
|
||||||
|
|
||||||
|
// Delete artifacts
|
||||||
|
ObjectListing objects = amazonS3.listObjects(s3Properties.getBucketName(), folder + "/");
|
||||||
|
do {
|
||||||
|
for (final S3ObjectSummary objectSummary : objects.getObjectSummaries()) {
|
||||||
|
amazonS3.deleteObject(s3Properties.getBucketName(), objectSummary.getKey());
|
||||||
|
}
|
||||||
|
objects = amazonS3.listNextBatchOfObjects(objects);
|
||||||
|
} while (objects.isTruncated());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sanitizeTenant(final String tenant) {
|
||||||
|
return tenant.trim().toUpperCase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import ru.yandex.qatools.allure.annotations.Stories;
|
|||||||
@Features("Unit Tests - S3 Repository")
|
@Features("Unit Tests - S3 Repository")
|
||||||
@Stories("S3 Artifact Repository")
|
@Stories("S3 Artifact Repository")
|
||||||
public class S3RepositoryTest {
|
public class S3RepositoryTest {
|
||||||
|
private static final String TENANT = "test_tenant";
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private AmazonS3 amazonS3Mock;
|
private AmazonS3 amazonS3Mock;
|
||||||
@@ -89,8 +90,9 @@ public class S3RepositoryTest {
|
|||||||
storeRandomBytes(rndBytes, knownContentType);
|
storeRandomBytes(rndBytes, knownContentType);
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
Mockito.verify(amazonS3Mock).putObject(eq(s3Properties.getBucketName()), eq(knownSHA1),
|
Mockito.verify(amazonS3Mock).putObject(eq(s3Properties.getBucketName()),
|
||||||
inputStreamCaptor.capture(), objectMetaDataCaptor.capture());
|
eq(TENANT.toUpperCase() + "/" + knownSHA1), inputStreamCaptor.capture(),
|
||||||
|
objectMetaDataCaptor.capture());
|
||||||
|
|
||||||
final ObjectMetadata recordedObjectMetadata = objectMetaDataCaptor.getValue();
|
final ObjectMetadata recordedObjectMetadata = objectMetaDataCaptor.getValue();
|
||||||
assertThat(recordedObjectMetadata.getContentType()).isEqualTo(knownContentType);
|
assertThat(recordedObjectMetadata.getContentType()).isEqualTo(knownContentType);
|
||||||
@@ -115,7 +117,7 @@ public class S3RepositoryTest {
|
|||||||
when(s3ObjectMetadataMock.getContentType()).thenReturn(knownContentType);
|
when(s3ObjectMetadataMock.getContentType()).thenReturn(knownContentType);
|
||||||
|
|
||||||
// test
|
// test
|
||||||
final DbArtifact artifactBySha1 = s3RepositoryUnderTest.getArtifactBySha1(knownSHA1Hash);
|
final DbArtifact artifactBySha1 = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
assertThat(artifactBySha1.getArtifactId()).isEqualTo(knownSHA1Hash);
|
assertThat(artifactBySha1.getArtifactId()).isEqualTo(knownSHA1Hash);
|
||||||
@@ -150,7 +152,7 @@ public class S3RepositoryTest {
|
|||||||
when(amazonS3Mock.getObject(s3Properties.getBucketName(), knownSHA1Hash)).thenReturn(null);
|
when(amazonS3Mock.getObject(s3Properties.getBucketName(), knownSHA1Hash)).thenReturn(null);
|
||||||
|
|
||||||
// test
|
// test
|
||||||
final DbArtifact artifactBySha1NotExists = s3RepositoryUnderTest.getArtifactBySha1(knownSHA1Hash);
|
final DbArtifact artifactBySha1NotExists = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
assertThat(artifactBySha1NotExists).isNull();
|
assertThat(artifactBySha1NotExists).isNull();
|
||||||
@@ -201,7 +203,7 @@ public class S3RepositoryTest {
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
final String knownFileName = "randomBytes";
|
final String knownFileName = "randomBytes";
|
||||||
try (InputStream content = new BufferedInputStream(new ByteArrayInputStream(rndBytes))) {
|
try (InputStream content = new BufferedInputStream(new ByteArrayInputStream(rndBytes))) {
|
||||||
s3RepositoryUnderTest.store(content, knownFileName, contentType, hashes);
|
s3RepositoryUnderTest.store(TENANT, content, knownFileName, contentType, hashes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
|||||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
@@ -44,6 +45,7 @@ import com.google.common.io.Files;
|
|||||||
* are stored in different sub-directories based on the last four digits of the
|
* are stored in different sub-directories based on the last four digits of the
|
||||||
* SHA1-hash {@code (/basepath/[two digit sha1]/[two digit sha1])}.
|
* SHA1-hash {@code (/basepath/[two digit sha1]/[two digit sha1])}.
|
||||||
*/
|
*/
|
||||||
|
@Validated
|
||||||
public class ArtifactFilesystemRepository implements ArtifactRepository {
|
public class ArtifactFilesystemRepository implements ArtifactRepository {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ArtifactFilesystemRepository.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ArtifactFilesystemRepository.class);
|
||||||
@@ -64,16 +66,17 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ArtifactFilesystem store(final InputStream content, final String filename, final String contentType) {
|
public ArtifactFilesystem store(final String tenant, final InputStream content, final String filename,
|
||||||
return store(content, filename, contentType, null);
|
final String contentType) {
|
||||||
|
return store(tenant, content, filename, contentType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5
|
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5
|
||||||
// is not used security related
|
// is not used security related
|
||||||
@SuppressWarnings("squid:S2070")
|
@SuppressWarnings("squid:S2070")
|
||||||
public ArtifactFilesystem store(final InputStream content, final String filename, final String contentType,
|
public ArtifactFilesystem store(final String tenant, final InputStream content, final String filename,
|
||||||
final DbArtifactHash hash) {
|
final String contentType, final DbArtifactHash hash) {
|
||||||
|
|
||||||
final MessageDigest mdSHA1;
|
final MessageDigest mdSHA1;
|
||||||
final MessageDigest mdMD5;
|
final MessageDigest mdMD5;
|
||||||
@@ -86,17 +89,17 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
|||||||
|
|
||||||
final File file = createTempFile();
|
final File file = createTempFile();
|
||||||
final DbArtifact artifact = store(content, contentType, hash, mdSHA1, mdMD5, file);
|
final DbArtifact artifact = store(content, contentType, hash, mdSHA1, mdMD5, file);
|
||||||
return renameFileToSHA1Naming(file, artifact);
|
return renameFileToSHA1Naming(tenant, file, artifact);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteBySha1(final String sha1Hash) {
|
public void deleteBySha1(final String tenant, final String sha1Hash) {
|
||||||
FileUtils.deleteQuietly(getFile(sha1Hash));
|
FileUtils.deleteQuietly(getFile(tenant, sha1Hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ArtifactFilesystem getArtifactBySha1(final String sha1) {
|
public ArtifactFilesystem getArtifactBySha1(final String tenant, final String sha1) {
|
||||||
final File file = getFile(sha1);
|
final File file = getFile(tenant, sha1);
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -131,8 +134,8 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
|||||||
return artifact;
|
return artifact;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ArtifactFilesystem renameFileToSHA1Naming(final File file, final DbArtifact artifact) {
|
private ArtifactFilesystem renameFileToSHA1Naming(final String tenant, final File file, final DbArtifact artifact) {
|
||||||
final File fileSHA1Naming = getFile(artifact.getHashes().getSha1());
|
final File fileSHA1Naming = getFile(tenant, artifact.getHashes().getSha1());
|
||||||
final ArtifactFilesystem fileSystemArtifact = new ArtifactFilesystem(fileSHA1Naming);
|
final ArtifactFilesystem fileSystemArtifact = new ArtifactFilesystem(fileSHA1Naming);
|
||||||
if (fileSHA1Naming.exists()) {
|
if (fileSHA1Naming.exists()) {
|
||||||
FileUtils.deleteQuietly(file);
|
FileUtils.deleteQuietly(file);
|
||||||
@@ -179,18 +182,18 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getFile(final String sha1) {
|
private File getFile(final String tenant, final String sha1) {
|
||||||
final File aritfactDirectory = getSha1DirectoryPath(sha1).toFile();
|
final File aritfactDirectory = getSha1DirectoryPath(tenant, sha1).toFile();
|
||||||
aritfactDirectory.mkdirs();
|
aritfactDirectory.mkdirs();
|
||||||
return new File(aritfactDirectory, sha1);
|
return new File(aritfactDirectory, sha1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path getSha1DirectoryPath(final String sha1) {
|
private Path getSha1DirectoryPath(final String tenant, final String sha1) {
|
||||||
final int length = sha1.length();
|
final int length = sha1.length();
|
||||||
final List<String> folders = Splitter.fixedLength(2).splitToList(sha1.substring(length - 4, length));
|
final List<String> folders = Splitter.fixedLength(2).splitToList(sha1.substring(length - 4, length));
|
||||||
final String folder1 = folders.get(0);
|
final String folder1 = folders.get(0);
|
||||||
final String folder2 = folders.get(1);
|
final String folder2 = folders.get(1);
|
||||||
return Paths.get(artifactResourceProperties.getPath(), folder1, folder2);
|
return Paths.get(artifactResourceProperties.getPath(), sanitizeTenant(tenant), folder1, folder2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DigestOutputStream openFileOutputStream(final File file, final MessageDigest mdSHA1,
|
private DigestOutputStream openFileOutputStream(final File file, final MessageDigest mdSHA1,
|
||||||
@@ -198,4 +201,13 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
|||||||
return new DigestOutputStream(
|
return new DigestOutputStream(
|
||||||
new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(file)), mdMD5), mdSHA1);
|
new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(file)), mdMD5), mdSHA1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByTenant(final String tenant) {
|
||||||
|
FileUtils.deleteQuietly(Paths.get(artifactResourceProperties.getPath(), sanitizeTenant(tenant)).toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sanitizeTenant(final String tenant) {
|
||||||
|
return tenant.trim().toUpperCase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import ru.yandex.qatools.allure.annotations.Stories;
|
|||||||
@Features("Unit Tests - Artifact File System Repository")
|
@Features("Unit Tests - Artifact File System Repository")
|
||||||
@Stories("Test storing artifact binaries in the file-system")
|
@Stories("Test storing artifact binaries in the file-system")
|
||||||
public class ArtifactFilesystemRepositoryTest {
|
public class ArtifactFilesystemRepositoryTest {
|
||||||
|
private static final String TENANT = "test_tenant";
|
||||||
|
|
||||||
private final ArtifactFilesystemProperties artifactResourceProperties = new ArtifactFilesystemProperties();
|
private final ArtifactFilesystemProperties artifactResourceProperties = new ArtifactFilesystemProperties();
|
||||||
|
|
||||||
@@ -51,8 +52,8 @@ public class ArtifactFilesystemRepositoryTest {
|
|||||||
final byte[] fileContent = randomBytes();
|
final byte[] fileContent = randomBytes();
|
||||||
final ArtifactFilesystem artifact = storeRandomArtifact(fileContent);
|
final ArtifactFilesystem artifact = storeRandomArtifact(fileContent);
|
||||||
|
|
||||||
final DbArtifact artifactBySha1 = artifactFilesystemRepository
|
final DbArtifact artifactBySha1 = artifactFilesystemRepository.getArtifactBySha1(TENANT,
|
||||||
.getArtifactBySha1(artifact.getHashes().getSha1());
|
artifact.getHashes().getSha1());
|
||||||
assertThat(artifactBySha1).isNotNull();
|
assertThat(artifactBySha1).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,16 +62,33 @@ public class ArtifactFilesystemRepositoryTest {
|
|||||||
public void deleteStoredArtifactBySHA1Hash() {
|
public void deleteStoredArtifactBySHA1Hash() {
|
||||||
final ArtifactFilesystem artifact = storeRandomArtifact(randomBytes());
|
final ArtifactFilesystem artifact = storeRandomArtifact(randomBytes());
|
||||||
|
|
||||||
artifactFilesystemRepository.deleteBySha1(artifact.getHashes().getSha1());
|
artifactFilesystemRepository.deleteBySha1(TENANT, artifact.getHashes().getSha1());
|
||||||
|
|
||||||
assertThat(artifactFilesystemRepository.getArtifactBySha1(artifact.getHashes().getSha1())).isNull();
|
assertThat(artifactFilesystemRepository.getArtifactBySha1(TENANT, artifact.getHashes().getSha1())).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verfies that all artifacts of a tenant can be deleted in the file-system repository")
|
||||||
|
public void deleteStoredArtifactOfTenant() {
|
||||||
|
final ArtifactFilesystem artifact = storeRandomArtifact(randomBytes());
|
||||||
|
|
||||||
|
artifactFilesystemRepository.deleteByTenant(TENANT);
|
||||||
|
|
||||||
|
assertThat(artifactFilesystemRepository.getArtifactBySha1(TENANT, artifact.getHashes().getSha1())).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Description("Verfies that an artifact which does not exists is deleted quietly in the file-system repository")
|
@Description("Verfies that an artifact which does not exists is deleted quietly in the file-system repository")
|
||||||
public void deleteArtifactWhichDoesNotExistsBySHA1HashWithoutException() {
|
public void deleteArtifactWhichDoesNotExistsBySHA1HashWithoutException() {
|
||||||
try {
|
try {
|
||||||
artifactFilesystemRepository.deleteBySha1("sha1HashWhichDoesNotExists");
|
artifactFilesystemRepository.deleteBySha1(TENANT, "sha1HashWhichDoesNotExists");
|
||||||
|
} catch (final Exception e) {
|
||||||
|
Assertions.fail("did not expect an exception while deleting a file which does not exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
final ArtifactFilesystem artifact = storeRandomArtifact(randomBytes());
|
||||||
|
try {
|
||||||
|
artifactFilesystemRepository.deleteBySha1("tenantWhichDoesNotExist", artifact.getHashes().getSha1());
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Assertions.fail("did not expect an exception while deleting a file which does not exists");
|
Assertions.fail("did not expect an exception while deleting a file which does not exists");
|
||||||
}
|
}
|
||||||
@@ -79,7 +97,8 @@ public class ArtifactFilesystemRepositoryTest {
|
|||||||
private ArtifactFilesystem storeRandomArtifact(final byte[] fileContent) {
|
private ArtifactFilesystem storeRandomArtifact(final byte[] fileContent) {
|
||||||
final String fileName = "filename.tmp";
|
final String fileName = "filename.tmp";
|
||||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(fileContent);
|
final ByteArrayInputStream inputStream = new ByteArrayInputStream(fileContent);
|
||||||
final ArtifactFilesystem store = artifactFilesystemRepository.store(inputStream, fileName, "application/txt");
|
final ArtifactFilesystem store = artifactFilesystemRepository.store(TENANT, inputStream, fileName,
|
||||||
|
"application/txt");
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,7 +313,8 @@ public class SecurityManagedConfiguration {
|
|||||||
|
|
||||||
http.regexMatcher(HttpDownloadAuthenticationFilter.REQUEST_ID_REGEX_PATTERN)
|
http.regexMatcher(HttpDownloadAuthenticationFilter.REQUEST_ID_REGEX_PATTERN)
|
||||||
.addFilterBefore(downloadIdAuthenticationFilter, FilterSecurityInterceptor.class);
|
.addFilterBefore(downloadIdAuthenticationFilter, FilterSecurityInterceptor.class);
|
||||||
http.authorizeRequests().anyRequest().authenticated();
|
http.authorizeRequests().anyRequest().authenticated().and().sessionManagement()
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -401,6 +402,7 @@ public class SecurityManagedConfiguration {
|
|||||||
|
|
||||||
httpSec.httpBasic().and().exceptionHandling().authenticationEntryPoint(basicAuthEntryPoint);
|
httpSec.httpBasic().and().exceptionHandling().authenticationEntryPoint(basicAuthEntryPoint);
|
||||||
httpSec.anonymous().disable();
|
httpSec.anonymous().disable();
|
||||||
|
httpSec.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@
|
|||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.validation</groupId>
|
||||||
|
<artifactId>validation-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate</groupId>
|
||||||
|
<artifactId>hibernate-validator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Test -->
|
<!-- Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ package org.eclipse.hawkbit.artifact.repository;
|
|||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ArtifactRepository service interface.
|
* ArtifactRepository service interface.
|
||||||
@@ -39,11 +42,14 @@ public interface ArtifactRepository {
|
|||||||
* @throws ArtifactStoreException
|
* @throws ArtifactStoreException
|
||||||
* in case storing of the artifact was not successful
|
* in case storing of the artifact was not successful
|
||||||
*/
|
*/
|
||||||
DbArtifact store(final InputStream content, final String filename, final String contentType);
|
DbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
|
||||||
|
String contentType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores an artifact into the repository.
|
* Stores an artifact into the repository.
|
||||||
*
|
*
|
||||||
|
* @param tenant
|
||||||
|
* the tenant to store the artifact
|
||||||
* @param content
|
* @param content
|
||||||
* the content to store
|
* the content to store
|
||||||
* @param filename
|
* @param filename
|
||||||
@@ -63,23 +69,27 @@ public interface ArtifactRepository {
|
|||||||
* in case {@code hash} is provided and not matching to the
|
* in case {@code hash} is provided and not matching to the
|
||||||
* calculated hashes during storing
|
* calculated hashes during storing
|
||||||
*/
|
*/
|
||||||
DbArtifact store(final InputStream content, final String filename, final String contentType, DbArtifactHash hash);
|
DbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
|
||||||
|
String contentType, DbArtifactHash hash);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an artifact by its SHA1 hash.
|
* Deletes an artifact by its SHA1 hash.
|
||||||
*
|
*
|
||||||
*
|
* @param tenant
|
||||||
|
* the tenant to store the artifact
|
||||||
* @param sha1Hash
|
* @param sha1Hash
|
||||||
* the sha1-hash of the artifact to delete
|
* the sha1-hash of the artifact to delete
|
||||||
*
|
*
|
||||||
* @throws MethodNotSupportedException
|
* @throws MethodNotSupportedException
|
||||||
* if implementation does not support the operation
|
* if implementation does not support the operation
|
||||||
*/
|
*/
|
||||||
void deleteBySha1(final String sha1Hash);
|
void deleteBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a {@link DbArtifact} from the store by it's SHA1 hash.
|
* Retrieves a {@link DbArtifact} from the store by it's SHA1 hash.
|
||||||
*
|
*
|
||||||
|
* @param tenant
|
||||||
|
* the tenant to store the artifact
|
||||||
* @param sha1Hash
|
* @param sha1Hash
|
||||||
* the sha1-hash of the file to lookup.
|
* the sha1-hash of the file to lookup.
|
||||||
* @return The artifact file object or {@code null} if no file exists.
|
* @return The artifact file object or {@code null} if no file exists.
|
||||||
@@ -87,5 +97,13 @@ public interface ArtifactRepository {
|
|||||||
* @throws MethodNotSupportedException
|
* @throws MethodNotSupportedException
|
||||||
* if implementation does not support the operation
|
* if implementation does not support the operation
|
||||||
*/
|
*/
|
||||||
DbArtifact getArtifactBySha1(String sha1Hash);
|
DbArtifact getArtifactBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all artifacts of given tenant.
|
||||||
|
*
|
||||||
|
* @param tenant
|
||||||
|
* to erase
|
||||||
|
*/
|
||||||
|
void deleteByTenant(@NotEmpty String tenant);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.eclipse.hawkbit.cache.DownloadIdCache;
|
|||||||
import org.eclipse.hawkbit.cache.DownloadType;
|
import org.eclipse.hawkbit.cache.DownloadType;
|
||||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDownloadRestApi;
|
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDownloadRestApi;
|
||||||
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
|
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
|
||||||
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -37,9 +38,6 @@ import com.google.common.net.HttpHeaders;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A resource for download artifacts.
|
* A resource for download artifacts.
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
|
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
|
||||||
@@ -56,6 +54,9 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RequestResponseContextHolder requestResponseContextHolder;
|
private RequestResponseContextHolder requestResponseContextHolder;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TenantAware tenantAware;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the GET request for downloading an artifact.
|
* Handles the GET request for downloading an artifact.
|
||||||
*
|
*
|
||||||
@@ -77,7 +78,7 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi {
|
|||||||
DbArtifact artifact = null;
|
DbArtifact artifact = null;
|
||||||
|
|
||||||
if (DownloadType.BY_SHA1.equals(artifactCache.getDownloadType())) {
|
if (DownloadType.BY_SHA1.equals(artifactCache.getDownloadType())) {
|
||||||
artifact = artifactRepository.getArtifactBySha1(artifactCache.getId());
|
artifact = artifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifactCache.getId());
|
||||||
} else {
|
} else {
|
||||||
LOGGER.warn("Download Type {} not supported", artifactCache.getDownloadType());
|
LOGGER.warn("Download Type {} not supported", artifactCache.getDownloadType());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
|||||||
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
|
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
|
||||||
import org.eclipse.hawkbit.repository.model.Action;
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||||
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
|
||||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||||
import org.eclipse.hawkbit.repository.model.Target;
|
import org.eclipse.hawkbit.repository.model.Target;
|
||||||
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
|
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
|
||||||
|
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
||||||
import org.hibernate.validator.constraints.NotEmpty;
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -119,12 +119,14 @@ public interface ControllerManagement {
|
|||||||
* Retrieves oldest {@link Action} that is active and assigned to a
|
* Retrieves oldest {@link Action} that is active and assigned to a
|
||||||
* {@link Target}.
|
* {@link Target}.
|
||||||
*
|
*
|
||||||
|
* For performance reasons this method does not throw
|
||||||
|
* {@link EntityNotFoundException} in case target with given controlelrId
|
||||||
|
* does not exist but will return an {@link Optional#empty()} instead.
|
||||||
|
*
|
||||||
* @param controllerId
|
* @param controllerId
|
||||||
* identifies the target to retrieve the actions from
|
* identifies the target to retrieve the actions from
|
||||||
* @return a list of actions assigned to given target which are active
|
* @return a list of actions assigned to given target which are active
|
||||||
*
|
*
|
||||||
* @throws EntityNotFoundException
|
|
||||||
* if target with given ID does not exist
|
|
||||||
*/
|
*/
|
||||||
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||||
Optional<Action> findOldestActiveActionByTarget(@NotNull String controllerId);
|
Optional<Action> findOldestActiveActionByTarget(@NotNull String controllerId);
|
||||||
|
|||||||
@@ -116,6 +116,17 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
|
|||||||
@EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD)
|
@EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD)
|
||||||
Optional<Action> findFirstByTargetControllerIdAndActive(final Sort sort, final String controllerId, boolean active);
|
Optional<Action> findFirstByTargetControllerIdAndActive(final Sort sort, final String controllerId, boolean active);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an active action exists for given
|
||||||
|
* {@link Target#getControllerId()}.
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* of target to check
|
||||||
|
* @return <code>true</code> if an active action for the target exists.
|
||||||
|
*/
|
||||||
|
@Query("SELECT CASE WHEN COUNT(a)>0 THEN 'true' ELSE 'false' END FROM JpaAction a JOIN a.target t WHERE t.controllerId=:controllerId AND a.active=1")
|
||||||
|
boolean activeActionExistsForControllerId(@Param("controllerId") String controllerId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves latest {@link Action} for given target and
|
* Retrieves latest {@link Action} for given target and
|
||||||
* {@link SoftwareModule}.
|
* {@link SoftwareModule}.
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaArtifact;
|
|||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
|
||||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||||
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.dao.ConcurrencyFailureException;
|
import org.springframework.dao.ConcurrencyFailureException;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -49,14 +49,22 @@ public class JpaArtifactManagement implements ArtifactManagement {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(JpaArtifactManagement.class);
|
private static final Logger LOG = LoggerFactory.getLogger(JpaArtifactManagement.class);
|
||||||
|
|
||||||
@Autowired
|
private final LocalArtifactRepository localArtifactRepository;
|
||||||
private LocalArtifactRepository localArtifactRepository;
|
|
||||||
|
|
||||||
@Autowired
|
private final SoftwareModuleRepository softwareModuleRepository;
|
||||||
private SoftwareModuleRepository softwareModuleRepository;
|
|
||||||
|
|
||||||
@Autowired
|
private final ArtifactRepository artifactRepository;
|
||||||
private ArtifactRepository artifactRepository;
|
|
||||||
|
private final TenantAware tenantAware;
|
||||||
|
|
||||||
|
JpaArtifactManagement(final LocalArtifactRepository localArtifactRepository,
|
||||||
|
final SoftwareModuleRepository softwareModuleRepository, final ArtifactRepository artifactRepository,
|
||||||
|
final TenantAware tenantAware) {
|
||||||
|
this.localArtifactRepository = localArtifactRepository;
|
||||||
|
this.softwareModuleRepository = softwareModuleRepository;
|
||||||
|
this.artifactRepository = artifactRepository;
|
||||||
|
this.tenantAware = tenantAware;
|
||||||
|
}
|
||||||
|
|
||||||
private static Artifact checkForExistingArtifact(final String filename, final boolean overrideExisting,
|
private static Artifact checkForExistingArtifact(final String filename, final boolean overrideExisting,
|
||||||
final SoftwareModule softwareModule) {
|
final SoftwareModule softwareModule) {
|
||||||
@@ -87,7 +95,7 @@ public class JpaArtifactManagement implements ArtifactManagement {
|
|||||||
final Artifact existing = checkForExistingArtifact(filename, overrideExisting, softwareModule);
|
final Artifact existing = checkForExistingArtifact(filename, overrideExisting, softwareModule);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = artifactRepository.store(stream, filename, contentType,
|
result = artifactRepository.store(tenantAware.getCurrentTenant(), stream, filename, contentType,
|
||||||
new DbArtifactHash(providedSha1Sum, providedMd5Sum));
|
new DbArtifactHash(providedSha1Sum, providedMd5Sum));
|
||||||
} catch (final ArtifactStoreException e) {
|
} catch (final ArtifactStoreException e) {
|
||||||
throw new ArtifactUploadFailedException(e);
|
throw new ArtifactUploadFailedException(e);
|
||||||
@@ -118,7 +126,7 @@ public class JpaArtifactManagement implements ArtifactManagement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
LOG.debug("deleting artifact from repository {}", sha1Hash);
|
LOG.debug("deleting artifact from repository {}", sha1Hash);
|
||||||
artifactRepository.deleteBySha1(sha1Hash);
|
artifactRepository.deleteBySha1(tenantAware.getCurrentTenant(), sha1Hash);
|
||||||
return true;
|
return true;
|
||||||
} catch (final ArtifactStoreException e) {
|
} catch (final ArtifactStoreException e) {
|
||||||
throw new ArtifactDeleteFailedException(e);
|
throw new ArtifactDeleteFailedException(e);
|
||||||
@@ -177,7 +185,7 @@ public class JpaArtifactManagement implements ArtifactManagement {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<DbArtifact> loadArtifactBinary(final String sha1Hash) {
|
public Optional<DbArtifact> loadArtifactBinary(final String sha1Hash) {
|
||||||
return Optional.ofNullable(artifactRepository.getArtifactBySha1(sha1Hash));
|
return Optional.ofNullable(artifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), sha1Hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Artifact storeArtifactMetadata(final SoftwareModule softwareModule, final String providedFilename,
|
private Artifact storeArtifactMetadata(final SoftwareModule softwareModule, final String providedFilename,
|
||||||
|
|||||||
@@ -188,7 +188,9 @@ public class JpaControllerManagement implements ControllerManagement {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Action> findOldestActiveActionByTarget(final String controllerId) {
|
public Optional<Action> findOldestActiveActionByTarget(final String controllerId) {
|
||||||
throwExceptionIfTargetDoesNotExist(controllerId);
|
if (!actionRepository.activeActionExistsForControllerId(controllerId)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
// used in favorite to findFirstByTargetAndActiveOrderByIdAsc due to
|
// used in favorite to findFirstByTargetAndActiveOrderByIdAsc due to
|
||||||
// DATAJPA-841 issue.
|
// DATAJPA-841 issue.
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JPA implementation of {@link SoftwareModuleManagement}.
|
* JPA implementation of {@link SoftwareModuleManagement}.
|
||||||
@@ -591,7 +590,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement {
|
|||||||
@Retryable(include = {
|
@Retryable(include = {
|
||||||
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||||
public void deleteSoftwareModule(final Long moduleId) {
|
public void deleteSoftwareModule(final Long moduleId) {
|
||||||
deleteSoftwareModules(Sets.newHashSet(moduleId));
|
deleteSoftwareModules(Arrays.asList(moduleId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
||||||
import org.eclipse.hawkbit.cache.TenancyCacheManager;
|
import org.eclipse.hawkbit.cache.TenancyCacheManager;
|
||||||
import org.eclipse.hawkbit.repository.RolloutStatusCache;
|
import org.eclipse.hawkbit.repository.RolloutStatusCache;
|
||||||
import org.eclipse.hawkbit.repository.SystemManagement;
|
import org.eclipse.hawkbit.repository.SystemManagement;
|
||||||
@@ -116,6 +117,9 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RolloutStatusCache rolloutStatusCache;
|
private RolloutStatusCache rolloutStatusCache;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ArtifactRepository artifactRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SystemUsageReport getSystemUsageStatistics() {
|
public SystemUsageReport getSystemUsageStatistics() {
|
||||||
|
|
||||||
@@ -236,6 +240,7 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst
|
|||||||
distributionSetRepository.deleteByTenant(tenant);
|
distributionSetRepository.deleteByTenant(tenant);
|
||||||
distributionSetTypeRepository.deleteByTenant(tenant);
|
distributionSetTypeRepository.deleteByTenant(tenant);
|
||||||
softwareModuleRepository.deleteByTenant(tenant);
|
softwareModuleRepository.deleteByTenant(tenant);
|
||||||
|
artifactRepository.deleteByTenant(tenant);
|
||||||
softwareModuleTypeRepository.deleteByTenant(tenant);
|
softwareModuleTypeRepository.deleteByTenant(tenant);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import java.util.Map;
|
|||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
||||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||||
@@ -502,16 +503,13 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
|||||||
return new JpaControllerManagement();
|
return new JpaControllerManagement();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link JpaArtifactManagement} bean.
|
|
||||||
*
|
|
||||||
* @return a new {@link ArtifactManagement}
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
ArtifactManagement artifactManagement() {
|
ArtifactManagement artifactManagement(final LocalArtifactRepository localArtifactRepository,
|
||||||
return new JpaArtifactManagement();
|
final SoftwareModuleRepository softwareModuleRepository, final ArtifactRepository artifactRepository,
|
||||||
|
final TenantAware tenantAware) {
|
||||||
|
return new JpaArtifactManagement(localArtifactRepository, softwareModuleRepository, artifactRepository,
|
||||||
|
tenantAware);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -175,16 +175,21 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
assertThat(result2.getId()).isNotNull();
|
assertThat(result2.getId()).isNotNull();
|
||||||
assertThat(((JpaArtifact) result).getSha1Hash()).isNotEqualTo(((JpaArtifact) result2).getSha1Hash());
|
assertThat(((JpaArtifact) result).getSha1Hash()).isNotEqualTo(((JpaArtifact) result2).getSha1Hash());
|
||||||
|
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNotNull();
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash()))
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result2).getSha1Hash())).isNotNull();
|
.isNotNull();
|
||||||
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash()))
|
||||||
|
.isNotNull();
|
||||||
|
|
||||||
artifactManagement.deleteArtifact(result.getId());
|
artifactManagement.deleteArtifact(result.getId());
|
||||||
|
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNull();
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash()))
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result2).getSha1Hash())).isNotNull();
|
.isNull();
|
||||||
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash()))
|
||||||
|
.isNotNull();
|
||||||
|
|
||||||
artifactManagement.deleteArtifact(result2.getId());
|
artifactManagement.deleteArtifact(result2.getId());
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result2).getSha1Hash())).isNull();
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash()))
|
||||||
|
.isNull();
|
||||||
|
|
||||||
assertThat(artifactRepository.findAll()).hasSize(0);
|
assertThat(artifactRepository.findAll()).hasSize(0);
|
||||||
}
|
}
|
||||||
@@ -211,12 +216,15 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
assertThat(result2.getId()).isNotNull();
|
assertThat(result2.getId()).isNotNull();
|
||||||
assertThat(((JpaArtifact) result).getSha1Hash()).isEqualTo(((JpaArtifact) result2).getSha1Hash());
|
assertThat(((JpaArtifact) result).getSha1Hash()).isEqualTo(((JpaArtifact) result2).getSha1Hash());
|
||||||
|
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNotNull();
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash()))
|
||||||
|
.isNotNull();
|
||||||
artifactManagement.deleteArtifact(result.getId());
|
artifactManagement.deleteArtifact(result.getId());
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNotNull();
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash()))
|
||||||
|
.isNotNull();
|
||||||
|
|
||||||
artifactManagement.deleteArtifact(result2.getId());
|
artifactManagement.deleteArtifact(result2.getId());
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNull();
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash()))
|
||||||
|
.isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
assertThat(controllerManagement.getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(),
|
assertThat(controllerManagement.getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(),
|
||||||
module.getId())).isNotPresent();
|
module.getId())).isNotPresent();
|
||||||
|
|
||||||
|
assertThat(controllerManagement.findOldestActiveActionByTarget(NOT_EXIST_ID)).isNotPresent();
|
||||||
|
|
||||||
assertThat(controllerManagement.hasTargetArtifactAssigned(target.getControllerId(), "XXX")).isFalse();
|
assertThat(controllerManagement.hasTargetArtifactAssigned(target.getControllerId(), "XXX")).isFalse();
|
||||||
assertThat(controllerManagement.hasTargetArtifactAssigned(target.getId(), "XXX")).isFalse();
|
assertThat(controllerManagement.hasTargetArtifactAssigned(target.getId(), "XXX")).isFalse();
|
||||||
}
|
}
|
||||||
@@ -100,8 +102,6 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
verifyThrownExceptionBy(() -> controllerManagement.addUpdateActionStatus(
|
verifyThrownExceptionBy(() -> controllerManagement.addUpdateActionStatus(
|
||||||
entityFactory.actionStatus().create(NOT_EXIST_IDL).status(Action.Status.FINISHED)), "Action");
|
entityFactory.actionStatus().create(NOT_EXIST_IDL).status(Action.Status.FINISHED)), "Action");
|
||||||
|
|
||||||
verifyThrownExceptionBy(() -> controllerManagement.findOldestActiveActionByTarget(NOT_EXIST_ID), "Target");
|
|
||||||
|
|
||||||
verifyThrownExceptionBy(() -> controllerManagement
|
verifyThrownExceptionBy(() -> controllerManagement
|
||||||
.getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), NOT_EXIST_IDL),
|
.getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), NOT_EXIST_IDL),
|
||||||
"SoftwareModule");
|
"SoftwareModule");
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import java.util.List;
|
|||||||
import org.apache.commons.lang3.RandomUtils;
|
import org.apache.commons.lang3.RandomUtils;
|
||||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||||
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaArtifact;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleMetadata;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleMetadata;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||||
@@ -490,13 +489,15 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
assertThat(artifactRepository.findAll()).hasSize(results.length);
|
assertThat(artifactRepository.findAll()).hasSize(results.length);
|
||||||
for (final Artifact result : results) {
|
for (final Artifact result : results) {
|
||||||
assertThat(result.getId()).isNotNull();
|
assertThat(result.getId()).isNotNull();
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNotNull();
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash()))
|
||||||
|
.isNotNull();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertArtfiactNull(final Artifact... results) {
|
private void assertArtfiactNull(final Artifact... results) {
|
||||||
for (final Artifact result : results) {
|
for (final Artifact result : results) {
|
||||||
assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNull();
|
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash()))
|
||||||
|
.isNull();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
# Test utility properties for easier fault investigation - START
|
# Test utility properties for easier fault investigation - START
|
||||||
## Logging - START
|
## Logging - START
|
||||||
logging.level.=INFO
|
logging.level.=INFO
|
||||||
logging.level.org.eclipse.persistence=ERROR
|
|
||||||
logging.level.org.eclipse.hawkbit.repository.test.matcher.EventVerifier=ERROR
|
logging.level.org.eclipse.hawkbit.repository.test.matcher.EventVerifier=ERROR
|
||||||
|
logging.level.org.eclipse.persistence=ERROR
|
||||||
|
spring.datasource.eclipselink.logging.logger=JavaLogger
|
||||||
spring.jpa.properties.eclipselink.logging.level=FINE
|
spring.jpa.properties.eclipselink.logging.level=FINE
|
||||||
spring.jpa.properties.eclipselink.logging.level.sql=FINE
|
spring.jpa.properties.eclipselink.logging.level.sql=FINE
|
||||||
spring.jpa.properties.eclipselink.logging.parameters=true
|
spring.jpa.properties.eclipselink.logging.parameters=true
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ public class DosFilter extends OncePerRequestFilter {
|
|||||||
private final Pattern ipAdressBlacklist;
|
private final Pattern ipAdressBlacklist;
|
||||||
|
|
||||||
private final Cache<String, AtomicInteger> readCountCache = Caffeine.newBuilder()
|
private final Cache<String, AtomicInteger> readCountCache = Caffeine.newBuilder()
|
||||||
.expireAfterAccess(1, TimeUnit.SECONDS).build();
|
.expireAfterWrite(1, TimeUnit.SECONDS).build();
|
||||||
|
|
||||||
private final Cache<String, AtomicInteger> writeCountCache = Caffeine.newBuilder()
|
private final Cache<String, AtomicInteger> writeCountCache = Caffeine.newBuilder()
|
||||||
.expireAfterAccess(1, TimeUnit.SECONDS).build();
|
.expireAfterWrite(1, TimeUnit.SECONDS).build();
|
||||||
|
|
||||||
private final int maxRead;
|
private final int maxRead;
|
||||||
private final int maxWrite;
|
private final int maxWrite;
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ public class CreateUpdateDistributionTagLayoutWindow extends AbstractCreateUpdat
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void populateTagNameCombo() {
|
protected void populateTagNameCombo() {
|
||||||
|
if (tagNameComboBox == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tagNameComboBox.removeAllItems();
|
tagNameComboBox.removeAllItems();
|
||||||
final List<DistributionSetTag> distTagNameList = tagManagement
|
final List<DistributionSetTag> distTagNameList = tagManagement
|
||||||
.findAllDistributionSetTags(new PageRequest(0, MAX_TAGS)).getContent();
|
.findAllDistributionSetTags(new PageRequest(0, MAX_TAGS)).getContent();
|
||||||
|
|||||||
Reference in New Issue
Block a user