From 0cf4f8e8b9e579c32834ad85bb5be2e1449b6abc Mon Sep 17 00:00:00 2001 From: Bondar Bogdan <36962546+bogdan-bondar@users.noreply.github.com> Date: Mon, 29 Oct 2018 11:28:34 +0100 Subject: [PATCH] Feature target metadata (#757) * Defined the model for target matadata and the corresponding repository layer/management * Added target metadata quotas incl enforcement * Extended Target Mgmt REST API to allow for metadata CRUD operations * Added migration scripts for each database * Added back reference to target metadata in JpaTarget * Added tests for target management, Mgmt REST API, target metadata RSQL, and REST documentation * Updated asciidocs for target rest documentation * Fix Allure imports and annotations * Fix review findings Signed-off-by: Bogdan Bondar Signed-off-by: Stefan Behl --- .../repository/TargetMetadataFields.java | 36 +++ .../hawkbit/repository/EntityFactory.java | 18 +- .../hawkbit/repository/QuotaManagement.java | 5 + .../hawkbit/repository/TargetManagement.java | 115 +++++++ .../exception/EntityNotFoundException.java | 22 +- .../repository/model/TargetMetadata.java | 26 ++ .../repository/PropertiesQuotaManagement.java | 5 + .../jpa/JpaDistributionSetManagement.java | 7 +- .../repository/jpa/JpaEntityFactory.java | 8 +- .../repository/jpa/JpaTargetManagement.java | 158 ++++++++- .../RepositoryApplicationConfiguration.java | 11 +- .../jpa/TargetMetadataRepository.java | 36 +++ .../repository/jpa/model/JpaTarget.java | 13 + .../jpa/model/JpaTargetMetadata.java | 109 +++++++ .../jpa/model/TargetMetadataCompositeKey.java | 90 ++++++ .../V1_12_9__add_target_metadata___DB2.sql | 9 + .../H2/V1_12_9__add_target_metadata___H2.sql | 12 + .../V1_12_9__add_target_metadata___MYSQL.sql | 12 + ...12_9__add_target_metadata___SQL_SERVER.sql | 9 + .../jpa/DistributionSetManagementTest.java | 8 +- .../repository/jpa/TargetManagementTest.java | 174 +++++++++- .../rsql/RSQLDistributionSetFieldTest.java | 4 +- ...RSQLDistributionSetMetadataFieldsTest.java | 4 +- .../rsql/RSQLTargetMetadataFieldsTest.java | 80 +++++ .../mgmt/rest/api/MgmtTargetRestApi.java | 169 +++++++--- .../resource/MgmtDistributionSetMapper.java | 2 +- .../resource/MgmtDistributionSetResource.java | 2 +- .../mgmt/rest/resource/MgmtTargetMapper.java | 28 ++ .../rest/resource/MgmtTargetResource.java | 144 ++++++--- .../MgmtDistributionSetResourceTest.java | 12 +- .../rest/resource/MgmtTargetResourceTest.java | 206 +++++++++++- .../src/main/asciidoc/targets-api-guide.adoc | 235 ++++++++++++++ .../AbstractApiRestDocumentation.java | 4 +- .../DistributionSetsDocumentationTest.java | 10 +- .../TargetResourceDocumentationTest.java | 302 ++++++++++++++---- .../security/HawkbitSecurityProperties.java | 13 + .../dstable/DsMetadataPopupLayout.java | 4 +- 37 files changed, 1886 insertions(+), 216 deletions(-) create mode 100644 hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetMetadataFields.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetMetadata.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetMetadataRepository.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetMetadata.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/TargetMetadataCompositeKey.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_9__add_target_metadata___DB2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_9__add_target_metadata___H2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_9__add_target_metadata___MYSQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_9__add_target_metadata___SQL_SERVER.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetMetadataFieldsTest.java diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetMetadataFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetMetadataFields.java new file mode 100644 index 000000000..c0a51a828 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetMetadataFields.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Sort fields for TargetMetadata. + * + */ +public enum TargetMetadataFields implements FieldNameProvider { + + /** + * The value field. + */ + VALUE("value"), + /** + * The key field. + */ + KEY("key"); + + private final String fieldName; + + TargetMetadataFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/EntityFactory.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/EntityFactory.java index 4205c1081..65fe29c69 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/EntityFactory.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/EntityFactory.java @@ -43,7 +43,8 @@ public interface EntityFactory { DistributionSetBuilder distributionSet(); /** - * Generates an {@link MetaData} element without persisting it. + * Generates an {@link MetaData} element for distribution set without + * persisting it. * * @param key * {@link MetaData#getKey()} @@ -52,7 +53,20 @@ public interface EntityFactory { * * @return {@link MetaData} object */ - MetaData generateMetadata(@Size(min = 1, max = MetaData.KEY_MAX_SIZE) @NotNull String key, + MetaData generateDsMetadata(@Size(min = 1, max = MetaData.KEY_MAX_SIZE) @NotNull String key, + @Size(max = MetaData.VALUE_MAX_SIZE) String value); + + /** + * Generates an {@link MetaData} element for target without persisting it. + * + * @param key + * {@link MetaData#getKey()} + * @param value + * {@link MetaData#getValue()} + * + * @return {@link MetaData} object + */ + MetaData generateTargetMetadata(@Size(min = 1, max = MetaData.KEY_MAX_SIZE) @NotNull String key, @Size(max = MetaData.VALUE_MAX_SIZE) String value); /** diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java index 1d3ee5eec..c769f4d73 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java @@ -53,6 +53,11 @@ public interface QuotaManagement { */ int getMaxMetaDataEntriesPerDistributionSet(); + /** + * @return maximum number of meta data entries per target + */ + int getMaxMetaDataEntriesPerTarget(); + /** * @return maximum number of software modules per distribution set */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java index da5655046..73c5e121c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java @@ -23,13 +23,16 @@ import org.eclipse.hawkbit.repository.builder.TargetCreate; import org.eclipse.hawkbit.repository.builder.TargetUpdate; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.Tag; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; @@ -650,4 +653,116 @@ public interface TargetManagement { * {@code false}: update of controller attributes not requested. */ boolean isControllerAttributesRequested(@NotEmpty String controllerId); + + /** + * Creates a list of target meta data entries. + * + * @param controllerId + * {@link Target} controller id the metadata has to be created + * for + * @param metadata + * the meta data entries to create or update + * @return the updated or created target meta data entries + * + * @throws EntityNotFoundException + * if given target does not exist + * + * @throws EntityAlreadyExistsException + * in case one of the meta data entry already exists for the + * specific key + * + * @throws QuotaExceededException + * if the maximum number of {@link MetaData} entries is exceeded + * for the addressed {@link Target} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + List createMetaData(@NotEmpty String controllerId, @NotEmpty Collection metadata); + + /** + * Deletes a target meta data entry. + * + * @param controllerId + * where meta data has to be deleted + * @param key + * of the meta data element + * + * @throws EntityNotFoundException + * if given target does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + void deleteMetaData(@NotEmpty String controllerId, @NotEmpty String key); + + /** + * Finds all meta data by the given target id. + * + * @param pageable + * the page request to page the result + * @param controllerId + * the controller id to retrieve the meta data from + * + * @return a paged result of all meta data entries for a given target id + * + * @throws EntityNotFoundException + * if target with given ID does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + Page findMetaDataByControllerId(@NotNull Pageable pageable, @NotEmpty String controllerId); + + /** + * Finds all meta data by the given target id and query. + * + * @param pageable + * the page request to page the result + * @param controllerId + * the controller id to retrieve the meta data from + * @param rsqlParam + * rsql query string + * + * @return a paged result of all meta data entries for a given target id + * + * @throws RSQLParameterUnsupportedFieldException + * if a field in the RSQL string is used but not provided by the + * given {@code fieldNameProvider} + * + * @throws RSQLParameterSyntaxException + * if the RSQL syntax is wrong + * + * @throws EntityNotFoundException + * if target with given ID does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + Page findMetaDataByControllerIdAndRsql(@NotNull Pageable pageable, @NotEmpty String controllerId, + @NotNull String rsqlParam); + + /** + * Finds a single target meta data by its id. + * + * @param controllerId + * of the {@link Target} + * @param key + * of the meta data element + * @return the found TargetMetadata + * + * @throws EntityNotFoundException + * if target with given ID does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + Optional getMetaDataByControllerId(@NotEmpty String controllerId, @NotEmpty String key); + + /** + * Updates a target meta data value if corresponding entry exists. + * + * @param controllerId + * {@link Target} controller id of the meta data entry to be + * updated + * @param metadata + * meta data entry to be updated + * @return the updated meta data entry + * + * @throws EntityNotFoundException + * in case the meta data entry does not exists and cannot be + * updated + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + TargetMetadata updateMetaData(@NotEmpty String controllerId, @NotNull MetaData metadata); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java index d1d9f836f..6c9bdbbb8 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java @@ -70,11 +70,25 @@ public class EntityNotFoundException extends AbstractServerRtException { * @param type * of the entity that was not found * - * @param enityId + * @param entityId * of the {@link BaseEntity} */ - public EntityNotFoundException(final Class type, final Object enityId) { - this(type.getSimpleName() + " with given identifier {" + enityId + "} does not exist."); + public EntityNotFoundException(final Class type, final Object entityId) { + this(type.getSimpleName() + " with given identifier {" + entityId + "} does not exist."); + } + + /** + * Parameterized constructor for {@link MetaData} not found. + * + * @param type + * of the entity that was not found + * @param entityId + * of the {@link BaseEntity} the {@link MetaData} was for + * @param key + * for the {@link MetaData} entry + */ + public EntityNotFoundException(final Class type, final Long entityId, final String key) { + this(type.getSimpleName() + " for given entity {" + entityId + "} and with key {" + key + "} does not exist."); } /** @@ -87,7 +101,7 @@ public class EntityNotFoundException extends AbstractServerRtException { * @param key * for the {@link MetaData} entry */ - public EntityNotFoundException(final Class type, final Long enityId, final String key) { + public EntityNotFoundException(final Class type, final String enityId, final String key) { this(type.getSimpleName() + " for given entity {" + enityId + "} and with key {" + key + "} does not exist."); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetMetadata.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetMetadata.java new file mode 100644 index 000000000..06a9ab14b --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetMetadata.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +/** + * {@link MetaData} of a {@link Target}. + * + */ +public interface TargetMetadata extends MetaData { + + /** + * @return {@link Target} of this {@link MetaData} entry. + */ + Target getTarget(); + + @Override + default Long getEntityId() { + return getTarget().getId(); + } +} diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java index 40f6f1927..001f5f9dc 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java @@ -58,6 +58,11 @@ public class PropertiesQuotaManagement implements QuotaManagement { return securityProperties.getDos().getMaxMetaDataEntriesPerDistributionSet(); } + @Override + public int getMaxMetaDataEntriesPerTarget() { + return securityProperties.getDos().getMaxMetaDataEntriesPerTarget(); + } + @Override public int getMaxSoftwareModulesPerDistributionSet() { return securityProperties.getDos().getMaxSoftwareModulesPerDistributionSet(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java index 7eb9f8e8c..c7852f80e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java @@ -457,7 +457,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public List createMetaData(final long dsId, final Collection md) { - md.forEach(meta -> checkAndThrowAlreadyIfDistributionSetMetadataExists( + md.forEach(meta -> checkAndThrowIfDistributionSetMetadataAlreadyExists( new DsMetadataCompositeKey(dsId, meta.getKey()))); assertMetaDataQuota(dsId, md.size()); @@ -478,8 +478,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { private void assertSoftwareModuleQuota(final Long id, final int requested) { QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxSoftwareModulesPerDistributionSet(), - SoftwareModule.class, DistributionSet.class, - softwareModuleRepository::countByAssignedToId); + SoftwareModule.class, DistributionSet.class, softwareModuleRepository::countByAssignedToId); } @Override @@ -676,7 +675,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { return distributionSetRepository.findAll(SpecificationsBuilder.combineWithAnd(specList), pageable); } - private void checkAndThrowAlreadyIfDistributionSetMetadataExists(final DsMetadataCompositeKey metadataId) { + private void checkAndThrowIfDistributionSetMetadataAlreadyExists(final DsMetadataCompositeKey metadataId) { if (distributionSetMetadataRepository.exists(metadataId)) { throw new EntityAlreadyExistsException( "Metadata entry with key '" + metadataId.getKey() + "' already exists"); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaEntityFactory.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaEntityFactory.java index 63da67bff..b620571e8 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaEntityFactory.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaEntityFactory.java @@ -26,6 +26,7 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaSoftwareModuleTypeBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaTagBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetBuilder; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetMetadata; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata; import org.eclipse.hawkbit.repository.model.MetaData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; @@ -57,10 +58,15 @@ public class JpaEntityFactory implements EntityFactory { private SoftwareModuleMetadataBuilder softwareModuleMetadataBuilder; @Override - public MetaData generateMetadata(final String key, final String value) { + public MetaData generateDsMetadata(final String key, final String value) { return new JpaDistributionSetMetadata(key, StringUtils.trimWhitespace(value)); } + @Override + public MetaData generateTargetMetadata(final String key, final String value) { + return new JpaTargetMetadata(key, StringUtils.trimWhitespace(value)); + } + @Override public DistributionSetTypeBuilder distributionSetType() { return distributionSetTypeBuilder; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java index 218457dab..1d16bf3c8 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java @@ -26,13 +26,16 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import org.eclipse.hawkbit.repository.FilterParams; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.TargetMetadataFields; import org.eclipse.hawkbit.repository.TimestampCalculator; import org.eclipse.hawkbit.repository.builder.TargetCreate; import org.eclipse.hawkbit.repository.builder.TargetUpdate; -import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; +import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetCreate; import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetUpdate; @@ -40,15 +43,21 @@ import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata_; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; +import org.eclipse.hawkbit.repository.jpa.model.TargetMetadataCompositeKey; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; @@ -83,8 +92,12 @@ public class JpaTargetManagement implements TargetManagement { private final EntityManager entityManager; + private final QuotaManagement quotaManagement; + private final TargetRepository targetRepository; + private final TargetMetadataRepository targetMetadataRepository; + private final RolloutGroupRepository rolloutGroupRepository; private final DistributionSetRepository distributionSetRepository; @@ -107,7 +120,8 @@ public class JpaTargetManagement implements TargetManagement { private final Database database; - JpaTargetManagement(final EntityManager entityManager, final TargetRepository targetRepository, + JpaTargetManagement(final EntityManager entityManager, final QuotaManagement quotaManagement, + final TargetRepository targetRepository, final TargetMetadataRepository targetMetadataRepository, final RolloutGroupRepository rolloutGroupRepository, final DistributionSetRepository distributionSetRepository, final TargetFilterQueryRepository targetFilterQueryRepository, @@ -116,7 +130,9 @@ public class JpaTargetManagement implements TargetManagement { final TenantAware tenantAware, final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) { this.entityManager = entityManager; + this.quotaManagement = quotaManagement; this.targetRepository = targetRepository; + this.targetMetadataRepository = targetMetadataRepository; this.rolloutGroupRepository = rolloutGroupRepository; this.distributionSetRepository = distributionSetRepository; this.targetFilterQueryRepository = targetFilterQueryRepository; @@ -135,6 +151,11 @@ public class JpaTargetManagement implements TargetManagement { return targetRepository.findByControllerId(controllerId); } + private JpaTarget getByControllerIdAndThrowIfNotFound(final String controllerId) { + return targetRepository.findByControllerId(controllerId).map(JpaTarget.class::cast) + .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + } + @Override public List getByControllerID(final Collection controllerIDs) { return Collections.unmodifiableList( @@ -146,6 +167,121 @@ public class JpaTargetManagement implements TargetManagement { return targetRepository.count(); } + @Override + @Transactional + @Retryable(include = { + ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public List createMetaData(final String controllerId, final Collection md) { + + final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerId); + + md.forEach(meta -> checkAndThrowIfTargetMetadataAlreadyExists( + new TargetMetadataCompositeKey(target.getId(), meta.getKey()))); + + assertMetaDataQuota(target.getId(), md.size()); + + final JpaTarget updatedTarget = touch(target); + + return Collections.unmodifiableList(md.stream() + .map(meta -> targetMetadataRepository + .save(new JpaTargetMetadata(meta.getKey(), meta.getValue(), updatedTarget))) + .collect(Collectors.toList())); + } + + private void checkAndThrowIfTargetMetadataAlreadyExists(final TargetMetadataCompositeKey metadataId) { + if (targetMetadataRepository.exists(metadataId)) { + throw new EntityAlreadyExistsException( + "Metadata entry with key '" + metadataId.getKey() + "' already exists"); + } + } + + private void assertMetaDataQuota(final Long targetId, final int requested) { + QuotaHelper.assertAssignmentQuota(targetId, requested, quotaManagement.getMaxMetaDataEntriesPerTarget(), + TargetMetadata.class, Target.class, targetMetadataRepository::countByTargetId); + } + + private JpaTarget touch(final JpaTarget target) { + + // merge base target so optLockRevision gets updated and audit + // log written because modifying metadata is modifying the base + // target itself for auditing purposes. + final JpaTarget result = entityManager.merge(target); + result.setLastModifiedAt(0L); + + return targetRepository.save(result); + } + + private JpaTarget touch(final String controllerId) { + return touch(getByControllerIdAndThrowIfNotFound(controllerId)); + } + + @Override + @Transactional + @Retryable(include = { + ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public TargetMetadata updateMetaData(final String controllerId, final MetaData md) { + + // check if exists otherwise throw entity not found exception + final JpaTargetMetadata toUpdate = (JpaTargetMetadata) getMetaDataByControllerId(controllerId, md.getKey()) + .orElseThrow(() -> new EntityNotFoundException(TargetMetadata.class, controllerId, md.getKey())); + toUpdate.setValue(md.getValue()); + // touch it to update the lock revision because we are modifying the + // target indirectly + touch(controllerId); + return targetMetadataRepository.save(toUpdate); + } + + @Override + @Transactional + @Retryable(include = { + ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public void deleteMetaData(final String controllerId, final String key) { + final JpaTargetMetadata metadata = (JpaTargetMetadata) getMetaDataByControllerId(controllerId, key) + .orElseThrow(() -> new EntityNotFoundException(TargetMetadata.class, controllerId, key)); + + touch(controllerId); + targetMetadataRepository.delete(metadata.getId()); + } + + @Override + public Page findMetaDataByControllerId(final Pageable pageable, final String controllerId) { + final Long targetId = getByControllerIdAndThrowIfNotFound(controllerId).getId(); + + return convertMdPage( + targetMetadataRepository + .findAll( + (Specification) (root, query, cb) -> cb + .equal(root.get(JpaTargetMetadata_.target).get(JpaTarget_.id), targetId), + pageable), + pageable); + } + + private static Page convertMdPage(final Page findAll, final Pageable pageable) { + return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, findAll.getTotalElements()); + } + + @Override + public Page findMetaDataByControllerIdAndRsql(final Pageable pageable, final String controllerId, + final String rsqlParam) { + + final Long targetId = getByControllerIdAndThrowIfNotFound(controllerId).getId(); + + final Specification spec = RSQLUtility.parse(rsqlParam, TargetMetadataFields.class, + virtualPropertyReplacer, database); + + return convertMdPage(targetMetadataRepository.findAll((Specification) (root, query, cb) -> cb + .and(cb.equal(root.get(JpaTargetMetadata_.target).get(JpaTarget_.id), targetId), + spec.toPredicate(root, query, cb)), + pageable), pageable); + } + + @Override + public Optional getMetaDataByControllerId(final String controllerId, final String key) { + final Long targetId = getByControllerIdAndThrowIfNotFound(controllerId).getId(); + + return Optional.ofNullable(targetMetadataRepository.findOne(new TargetMetadataCompositeKey(targetId, key))); + } + @Override public Slice findAll(final Pageable pageable) { return convertPage(criteriaNoCountDao.findAll(pageable, JpaTarget.class), pageable); @@ -178,8 +314,7 @@ public class JpaTargetManagement implements TargetManagement { public Target update(final TargetUpdate u) { final JpaTargetUpdate update = (JpaTargetUpdate) u; - final JpaTarget target = (JpaTarget) targetRepository.findByControllerId(update.getControllerId()) - .orElseThrow(() -> new EntityNotFoundException(Target.class, update.getControllerId())); + final JpaTarget target = getByControllerIdAndThrowIfNotFound(update.getControllerId()); update.getName().ifPresent(target::setName); update.getDescription().ifPresent(target::setDescription); @@ -214,8 +349,7 @@ public class JpaTargetManagement implements TargetManagement { @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public void deleteByControllerID(final String controllerID) { - final Target target = targetRepository.findByControllerId(controllerID) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerID)); + final Target target = getByControllerIdAndThrowIfNotFound(controllerID); targetRepository.delete(target.getId()); } @@ -418,8 +552,7 @@ public class JpaTargetManagement implements TargetManagement { @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public Target unAssignTag(final String controllerID, final long targetTagId) { - final JpaTarget target = (JpaTarget) targetRepository.findByControllerId(controllerID) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerID)); + final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerID); final TargetTag tag = targetTagRepository.findById(targetTagId) .orElseThrow(() -> new EntityNotFoundException(TargetTag.class, targetTagId)); @@ -635,16 +768,14 @@ public class JpaTargetManagement implements TargetManagement { @Override public Map getControllerAttributes(final String controllerId) { - final JpaTarget target = (JpaTarget) getByControllerID(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerId); return target.getControllerAttributes(); } @Override public void requestControllerAttributes(final String controllerId) { - final JpaTarget target = (JpaTarget) getByControllerID(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerId); target.setRequestControllerAttributes(true); @@ -655,8 +786,7 @@ public class JpaTargetManagement implements TargetManagement { @Override public boolean isControllerAttributesRequested(final String controllerId) { - final JpaTarget target = (JpaTarget) getByControllerID(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerId); return target.isRequestControllerAttributes(); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index 57c6657e7..4d22a3fb7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -449,7 +449,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { */ @Bean @ConditionalOnMissingBean - TargetManagement targetManagement(final EntityManager entityManager, final TargetRepository targetRepository, + TargetManagement targetManagement(final EntityManager entityManager, final QuotaManagement quotaManagement, + final TargetRepository targetRepository, final TargetMetadataRepository targetMetadataRepository, final RolloutGroupRepository rolloutGroupRepository, final DistributionSetRepository distributionSetRepository, final TargetFilterQueryRepository targetFilterQueryRepository, @@ -457,10 +458,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final ApplicationEventPublisher eventPublisher, final ApplicationContext applicationContext, final TenantAware tenantAware, final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, final JpaProperties properties) { - return new JpaTargetManagement(entityManager, targetRepository, rolloutGroupRepository, - distributionSetRepository, targetFilterQueryRepository, targetTagRepository, criteriaNoCountDao, - eventPublisher, applicationContext, tenantAware, afterCommit, virtualPropertyReplacer, - properties.getDatabase()); + return new JpaTargetManagement(entityManager, quotaManagement, targetRepository, targetMetadataRepository, + rolloutGroupRepository, distributionSetRepository, targetFilterQueryRepository, targetTagRepository, + criteriaNoCountDao, eventPublisher, applicationContext, tenantAware, afterCommit, + virtualPropertyReplacer, properties.getDatabase()); } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetMetadataRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetMetadataRepository.java new file mode 100644 index 000000000..baa8d22b0 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetMetadataRepository.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa; + +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata; +import org.eclipse.hawkbit.repository.jpa.model.TargetMetadataCompositeKey; +import org.eclipse.hawkbit.repository.model.TargetMetadata; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link TargetMetadata} repository. + */ +@Transactional(readOnly = true) +public interface TargetMetadataRepository + extends PagingAndSortingRepository, + JpaSpecificationExecutor { + + /** + * Counts the meta data entries that match the given target ID. + * + * @param id + * of the target. + * + * @return The number of matching meta data entries. + */ + long countByTargetId(@Param("id") Long id); +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java index d4939c3f9..01fc2390b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java @@ -54,6 +54,7 @@ import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.PollStatus; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; @@ -167,6 +168,10 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw @Column(name = "request_controller_attributes", nullable = false) private boolean requestControllerAttributes = true; + @CascadeOnDelete + @OneToMany(mappedBy = "target", fetch = FetchType.LAZY, targetEntity = JpaTargetMetadata.class) + private List metadata; + /** * Constructor. * @@ -351,6 +356,14 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return requestControllerAttributes; } + public List getMetadata() { + if (metadata == null) { + return Collections.emptyList(); + } + + return Collections.unmodifiableList(metadata); + } + @Override public String toString() { return "JpaTarget [controllerId=" + controllerId + ", revision=" + getOptLockRevision() + ", id=" + getId() diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetMetadata.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetMetadata.java new file mode 100644 index 000000000..6deaf9f52 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetMetadata.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.model; + +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetMetadata; + +/** + * Meta data for {@link Target}. + * + */ +@IdClass(TargetMetadataCompositeKey.class) +@Entity +@Table(name = "sp_target_metadata") +public class JpaTargetMetadata extends AbstractJpaMetaData implements TargetMetadata { + private static final long serialVersionUID = 1L; + + @Id + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "target_id", nullable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_metadata_target")) + private JpaTarget target; + + public JpaTargetMetadata() { + // default public constructor for JPA + } + + /** + * Creates a single metadata entry with the given key and value. + * + * @param key + * of the meta data entry + * @param value + * of the meta data entry + */ + public JpaTargetMetadata(final String key, final String value) { + super(key, value); + } + + /** + * Creates a single metadata entry with the given key and value for the + * given {@link Target}. + * + * @param key + * of the meta data entry + * @param value + * of the meta data entry + * @param target + * the meta data entry is associated with + */ + public JpaTargetMetadata(final String key, final String value, final Target target) { + super(key, value); + this.target = (JpaTarget) target; + } + + public TargetMetadataCompositeKey getId() { + return new TargetMetadataCompositeKey(target.getId(), getKey()); + } + + public void setTarget(final Target target) { + this.target = (JpaTarget) target; + } + + @Override + public Target getTarget() { + return target; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((target == null) ? 0 : target.hashCode()); + return result; + } + + @Override + // exception squid:S2259 - obj is checked for null in super + @SuppressWarnings("squid:S2259") + public boolean equals(final Object obj) { + if (!super.equals(obj)) { + return false; + } + final JpaTargetMetadata other = (JpaTargetMetadata) obj; + if (target == null) { + if (other.target != null) { + return false; + } + } else if (!target.equals(other.target)) { + return false; + } + return true; + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/TargetMetadataCompositeKey.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/TargetMetadataCompositeKey.java new file mode 100644 index 000000000..d65f941bf --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/TargetMetadataCompositeKey.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.model; + +import java.io.Serializable; +import java.util.Objects; + +/** + * The Target Metadata composite key which contains the meta data key and the ID + * of the Target itself. + * + */ +public final class TargetMetadataCompositeKey implements Serializable { + private static final long serialVersionUID = 1L; + + private String key; + + private Long target; + + public TargetMetadataCompositeKey() { + // Default constructor for JPA. + } + + /** + * @param target + * the target Id for this meta data + * @param key + * the key of the meta data + */ + public TargetMetadataCompositeKey(final Long target, final String key) { + this.target = target; + this.key = key; + } + + public String getKey() { + return key; + } + + public void setKey(final String key) { + this.key = key; + } + + public Long getTargetId() { + return target; + } + + public void setTargetId(final Long target) { + this.target = target; + } + + @Override + public int hashCode() { + return Objects.hash(target, key); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TargetMetadataCompositeKey other = (TargetMetadataCompositeKey) obj; + if (target == null) { + if (other.target != null) { + return false; + } + } else if (!target.equals(other.target)) { + return false; + } + if (key == null) { + if (other.key != null) { + return false; + } + } else if (!key.equals(other.key)) { + return false; + } + return true; + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_9__add_target_metadata___DB2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_9__add_target_metadata___DB2.sql new file mode 100644 index 000000000..027af4dd6 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_9__add_target_metadata___DB2.sql @@ -0,0 +1,9 @@ +CREATE TABLE sp_target_metadata +( + meta_key VARCHAR(128) NOT NULL, + meta_value VARCHAR(4000), + target_id BIGINT NOT NULL, + PRIMARY KEY (meta_key, target_id) +); + +ALTER TABLE sp_target_metadata ADD CONSTRAINT fk_metadata_target FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_9__add_target_metadata___H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_9__add_target_metadata___H2.sql new file mode 100644 index 000000000..de70b7519 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_9__add_target_metadata___H2.sql @@ -0,0 +1,12 @@ +create table sp_target_metadata ( + meta_key varchar(128) not null, + meta_value varchar(4000), + target_id bigint not null, + primary key (target_id, meta_key) +); + +alter table sp_target_metadata + add constraint fk_metadata_target + foreign key (target_id) + references sp_target + on delete cascade; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_9__add_target_metadata___MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_9__add_target_metadata___MYSQL.sql new file mode 100644 index 000000000..3b1be5e4b --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_9__add_target_metadata___MYSQL.sql @@ -0,0 +1,12 @@ +create table sp_target_metadata ( + meta_key varchar(128) not null, + meta_value varchar(4000), + target_id bigint not null, + primary key (target_id, meta_key) +); + +alter table sp_target_metadata + add constraint fk_metadata_target + foreign key (target_id) + references sp_target (id) + on delete cascade; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_9__add_target_metadata___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_9__add_target_metadata___SQL_SERVER.sql new file mode 100644 index 000000000..baa8cf292 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_9__add_target_metadata___SQL_SERVER.sql @@ -0,0 +1,9 @@ +CREATE TABLE sp_target_metadata +( + meta_key VARCHAR(128) NOT NULL, + meta_value VARCHAR(4000) NULL, + target_id NUMERIC(19) NOT NULL, + PRIMARY KEY (meta_key, target_id) +); + +ALTER TABLE sp_target_metadata ADD CONSTRAINT fk_metadata_target FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java index 2f51a3aa6..4c6cf957e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java @@ -136,7 +136,7 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { "DistributionSetType"); verifyThrownExceptionBy(() -> distributionSetManagement.createMetaData(NOT_EXIST_IDL, - Arrays.asList(entityFactory.generateMetadata("123", "123"))), "DistributionSet"); + Arrays.asList(entityFactory.generateDsMetadata("123", "123"))), "DistributionSet"); verifyThrownExceptionBy(() -> distributionSetManagement.delete(Arrays.asList(NOT_EXIST_IDL)), "DistributionSet"); @@ -167,10 +167,10 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { "DistributionSet"); verifyThrownExceptionBy(() -> distributionSetManagement.updateMetaData(NOT_EXIST_IDL, - entityFactory.generateMetadata("xxx", "xxx")), "DistributionSet"); + entityFactory.generateDsMetadata("xxx", "xxx")), "DistributionSet"); verifyThrownExceptionBy(() -> distributionSetManagement.updateMetaData(set.getId(), - entityFactory.generateMetadata(NOT_EXIST_ID, "xxx")), "DistributionSetMetadata"); + entityFactory.generateDsMetadata(NOT_EXIST_ID, "xxx")), "DistributionSetMetadata"); } @Test @@ -553,7 +553,7 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { // update the DS metadata final JpaDistributionSetMetadata updated = (JpaDistributionSetMetadata) distributionSetManagement - .updateMetaData(ds.getId(), entityFactory.generateMetadata(knownKey, knownUpdateValue)); + .updateMetaData(ds.getId(), entityFactory.generateDsMetadata(knownKey, knownUpdateValue)); // we are updating the sw meta data so also modifying the base software // module so opt lock // revision must be three diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java index f5c086d71..c25e044ce 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java @@ -41,13 +41,17 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetTagCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.InvalidTargetAddressException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.TenantNotExistException; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.Tag; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; @@ -73,10 +77,12 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Verifies that management get access react as specified on calls for non existing entities by means " + "of Optional not present.") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1) }) public void nonExistingEntityAccessReturnsNotPresent() { + final Target target = testdataFactory.createTarget(); assertThat(targetManagement.getByControllerID(NOT_EXIST_ID)).isNotPresent(); assertThat(targetManagement.get(NOT_EXIST_IDL)).isNotPresent(); + assertThat(targetManagement.getMetaDataByControllerId(target.getControllerId(), NOT_EXIST_ID)).isNotPresent(); } @Test @@ -89,8 +95,10 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { final Target target = testdataFactory.createTarget(); verifyThrownExceptionBy( - () -> targetManagement.assignTag(Collections.singletonList(target.getControllerId()), NOT_EXIST_IDL), "TargetTag"); - verifyThrownExceptionBy(() -> targetManagement.assignTag(Collections.singletonList(NOT_EXIST_ID), tag.getId()), "Target"); + () -> targetManagement.assignTag(Collections.singletonList(target.getControllerId()), NOT_EXIST_IDL), + "TargetTag"); + verifyThrownExceptionBy(() -> targetManagement.assignTag(Collections.singletonList(NOT_EXIST_ID), tag.getId()), + "Target"); verifyThrownExceptionBy(() -> targetManagement.findByTag(PAGE, NOT_EXIST_IDL), "TargetTag"); verifyThrownExceptionBy(() -> targetManagement.findByRsqlAndTag(PAGE, "name==*", NOT_EXIST_IDL), "TargetTag"); @@ -123,16 +131,31 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { () -> targetManagement.findByInstalledDistributionSetAndRsql(PAGE, NOT_EXIST_IDL, "name==*"), "DistributionSet"); + verifyThrownExceptionBy(() -> targetManagement + .toggleTagAssignment(Collections.singletonList(target.getControllerId()), NOT_EXIST_ID), "TargetTag"); verifyThrownExceptionBy( - () -> targetManagement.toggleTagAssignment(Collections.singletonList(target.getControllerId()), NOT_EXIST_ID), - "TargetTag"); - verifyThrownExceptionBy(() -> targetManagement.toggleTagAssignment(Collections.singletonList(NOT_EXIST_ID), tag.getName()), + () -> targetManagement.toggleTagAssignment(Collections.singletonList(NOT_EXIST_ID), tag.getName()), "Target"); verifyThrownExceptionBy(() -> targetManagement.unAssignTag(NOT_EXIST_ID, tag.getId()), "Target"); verifyThrownExceptionBy(() -> targetManagement.unAssignTag(target.getControllerId(), NOT_EXIST_IDL), "TargetTag"); verifyThrownExceptionBy(() -> targetManagement.update(entityFactory.target().update(NOT_EXIST_ID)), "Target"); + + verifyThrownExceptionBy(() -> targetManagement.createMetaData(NOT_EXIST_ID, + Arrays.asList(entityFactory.generateTargetMetadata("123", "123"))), "Target"); + verifyThrownExceptionBy(() -> targetManagement.deleteMetaData(NOT_EXIST_ID, "xxx"), "Target"); + verifyThrownExceptionBy(() -> targetManagement.deleteMetaData(target.getControllerId(), NOT_EXIST_ID), + "TargetMetadata"); + verifyThrownExceptionBy(() -> targetManagement.getMetaDataByControllerId(NOT_EXIST_ID, "xxx"), "Target"); + verifyThrownExceptionBy(() -> targetManagement.findMetaDataByControllerId(PAGE, NOT_EXIST_ID), "Target"); + verifyThrownExceptionBy(() -> targetManagement.findMetaDataByControllerIdAndRsql(PAGE, NOT_EXIST_ID, "name==*"), + "Target"); + verifyThrownExceptionBy( + () -> targetManagement.updateMetaData(NOT_EXIST_ID, entityFactory.generateTargetMetadata("xxx", "xxx")), + "Target"); + verifyThrownExceptionBy(() -> targetManagement.updateMetaData(target.getControllerId(), + entityFactory.generateTargetMetadata(NOT_EXIST_ID, "xxx")), "TargetMetadata"); } @Test @@ -143,8 +166,9 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { .create(entityFactory.target().create().controllerId("targetWithSecurityToken").securityToken("token")); // retrieve security token only with READ_TARGET_SEC_TOKEN permission - final String securityTokenWithReadPermission = securityRule.runAs(WithSpringAuthorityRule - .withUser("OnlyTargetReadPermission", false, SpPermission.READ_TARGET_SEC_TOKEN), createdTarget::getSecurityToken); + final String securityTokenWithReadPermission = securityRule.runAs( + WithSpringAuthorityRule.withUser("OnlyTargetReadPermission", false, SpPermission.READ_TARGET_SEC_TOKEN), + createdTarget::getSecurityToken); // retrieve security token as system code execution final String securityTokenAsSystemCode = systemSecurityContext.runAsSystem(createdTarget::getSecurityToken); @@ -889,4 +913,138 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { assertThat(targetManagement.isControllerAttributesRequested(knownTargetId)).isTrue(); } + + @Test + @Description("Checks that metadata for a target can be created.") + public void createTargetMetadata() { + final String knownKey = "targetMetaKnownKey"; + final String knownValue = "targetMetaKnownValue"; + + final Target target = testdataFactory.createTarget("targetIdWithMetadata"); + final JpaTargetMetadata createdMetadata = insertTargetMetadata(knownKey, target, knownValue); + + assertThat(createdMetadata).isNotNull(); + assertThat(createdMetadata.getId().getKey()).isEqualTo(knownKey); + assertThat(createdMetadata.getTarget().getControllerId()).isEqualTo(target.getControllerId()); + assertThat(createdMetadata.getTarget().getId()).isEqualTo(target.getId()); + assertThat(createdMetadata.getValue()).isEqualTo(knownValue); + } + + private JpaTargetMetadata insertTargetMetadata(final String knownKey, final Target target, + final String knownValue) { + final JpaTargetMetadata metadata = new JpaTargetMetadata(knownKey, knownValue, target); + return (JpaTargetMetadata) targetManagement + .createMetaData(target.getControllerId(), Collections.singletonList(metadata)).get(0); + } + + @Test + @Description("Verifies the enforcement of the metadata quota per target.") + public void createTargetMetadataUntilQuotaIsExceeded() { + + // add meta data one by one + final Target target1 = testdataFactory.createTarget("target1"); + final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerTarget(); + for (int i = 0; i < maxMetaData; ++i) { + assertThat(insertTargetMetadata("k" + i, target1, "v" + i)).isNotNull(); + } + + // quota exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> insertTargetMetadata("k" + maxMetaData, target1, "v" + maxMetaData)); + + // add multiple meta data entries at once + final Target target2 = testdataFactory.createTarget("target2"); + final List metaData2 = new ArrayList<>(); + for (int i = 0; i < maxMetaData + 1; ++i) { + metaData2.add(new JpaTargetMetadata("k" + i, "v" + i, target2)); + } + // verify quota is exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> targetManagement.createMetaData(target2.getControllerId(), metaData2)); + + // add some meta data entries + final Target target3 = testdataFactory.createTarget("target3"); + final int firstHalf = Math.round(maxMetaData / 2); + for (int i = 0; i < firstHalf; ++i) { + insertTargetMetadata("k" + i, target3, "v" + i); + } + // add too many data entries + final int secondHalf = maxMetaData - firstHalf; + final List metaData3 = new ArrayList<>(); + for (int i = 0; i < secondHalf + 1; ++i) { + metaData3.add(new JpaTargetMetadata("kk" + i, "vv" + i, target3)); + } + // verify quota is exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> targetManagement.createMetaData(target3.getControllerId(), metaData3)); + + } + + @Test + @WithUser(allSpPermissions = true) + @Description("Checks that metadata for a target can be updated.") + public void updateTargetMetadata() throws InterruptedException { + final String knownKey = "myKnownKey"; + final String knownValue = "myKnownValue"; + final String knownUpdateValue = "myNewUpdatedValue"; + + // create a target + final Target target = testdataFactory.createTarget("target1"); + // initial opt lock revision must be zero + assertThat(target.getOptLockRevision()).isEqualTo(1); + + // create target meta data entry + insertTargetMetadata(knownKey, target, knownValue); + + Target changedLockRevisionTarget = targetManagement.get(target.getId()).get(); + assertThat(changedLockRevisionTarget.getOptLockRevision()).isEqualTo(2); + + Thread.sleep(100); + + // update the target metadata + final JpaTargetMetadata updated = (JpaTargetMetadata) targetManagement.updateMetaData(target.getControllerId(), + entityFactory.generateTargetMetadata(knownKey, knownUpdateValue)); + // we are updating the target meta data so also modifying the base + // software + // module so opt lock + // revision must be three + changedLockRevisionTarget = targetManagement.get(target.getId()).get(); + assertThat(changedLockRevisionTarget.getOptLockRevision()).isEqualTo(3); + assertThat(changedLockRevisionTarget.getLastModifiedAt()).isGreaterThan(0L); + + // verify updated meta data contains the updated value + assertThat(updated).isNotNull(); + assertThat(updated.getValue()).isEqualTo(knownUpdateValue); + assertThat(updated.getId().getKey()).isEqualTo(knownKey); + assertThat(updated.getTarget().getControllerId()).isEqualTo(target.getControllerId()); + assertThat(updated.getTarget().getId()).isEqualTo(target.getId()); + } + + @Test + @Description("Queries and loads the metadata related to a given target.") + public void findAllTargetMetadataByControllerId() { + // create targets + final Target target1 = testdataFactory.createTarget("target1"); + final Target target2 = testdataFactory.createTarget("target2"); + + for (int index = 0; index < 10; index++) { + insertTargetMetadata("key" + index, target1, "value" + index); + } + + for (int index = 0; index < 8; index++) { + insertTargetMetadata("key" + index, target2, "value" + index); + } + + final Page metadataOfTarget1 = targetManagement + .findMetaDataByControllerId(new PageRequest(0, 100), target1.getControllerId()); + + final Page metadataOfTarget2 = targetManagement + .findMetaDataByControllerId(new PageRequest(0, 100), target2.getControllerId()); + + assertThat(metadataOfTarget1.getNumberOfElements()).isEqualTo(10); + assertThat(metadataOfTarget1.getTotalElements()).isEqualTo(10); + + assertThat(metadataOfTarget2.getNumberOfElements()).isEqualTo(8); + assertThat(metadataOfTarget2.getTotalElements()).isEqualTo(8); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java index b3904af8d..55028413f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java @@ -37,12 +37,12 @@ public class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest { DistributionSet ds = testdataFactory.createDistributionSet("DS"); ds = distributionSetManagement.update(entityFactory.distributionSet().update(ds.getId()).description("DS")); - createDistributionSetMetadata(ds.getId(), entityFactory.generateMetadata("metaKey", "metaValue")); + createDistributionSetMetadata(ds.getId(), entityFactory.generateDsMetadata("metaKey", "metaValue")); DistributionSet ds2 = testdataFactory.createDistributionSets("NewDS", 3).get(0); ds2 = distributionSetManagement.update(entityFactory.distributionSet().update(ds2.getId()).description("DS%")); - createDistributionSetMetadata(ds2.getId(), entityFactory.generateMetadata("metaKey", "value")); + createDistributionSetMetadata(ds2.getId(), entityFactory.generateDsMetadata("metaKey", "value")); final DistributionSetTag targetTag = distributionSetTagManagement .create(entityFactory.tag().create().name("Tag1")); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetMetadataFieldsTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetMetadataFieldsTest.java index 1db2d60b3..65fcf7937 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetMetadataFieldsTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetMetadataFieldsTest.java @@ -41,13 +41,13 @@ public class RSQLDistributionSetMetadataFieldsTest extends AbstractJpaIntegratio final List metadata = new ArrayList<>(5); for (int i = 0; i < 5; i++) { - metadata.add(entityFactory.generateMetadata("" + i, "" + i)); + metadata.add(entityFactory.generateDsMetadata("" + i, "" + i)); } distributionSetManagement.createMetaData(distributionSetId, metadata); distributionSetManagement.createMetaData(distributionSetId, - Arrays.asList(entityFactory.generateMetadata("emptyValueTest", null))); + Arrays.asList(entityFactory.generateDsMetadata("emptyValueTest", null))); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetMetadataFieldsTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetMetadataFieldsTest.java new file mode 100644 index 000000000..cefba21bf --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetMetadataFieldsTest.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.rsql; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.hawkbit.repository.TargetMetadataFields; +import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.model.MetaData; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetMetadata; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; + +@Feature("Component Tests - Repository") +@Story("RSQL filter target metadata") +public class RSQLTargetMetadataFieldsTest extends AbstractJpaIntegrationTest { + private String controllerId; + + @Before + public void setupBeforeTest() { + final Target target = testdataFactory.createTarget("target"); + controllerId = target.getControllerId(); + + final List metadata = new ArrayList<>(5); + for (int i = 0; i < 5; i++) { + metadata.add(entityFactory.generateTargetMetadata("" + i, "" + i)); + } + + targetManagement.createMetaData(controllerId, metadata); + + targetManagement.createMetaData(controllerId, + Arrays.asList(entityFactory.generateTargetMetadata("emptyValueTest", null))); + } + + @Test + @Description("Test filter target metadata by key") + public void testFilterByParameterKey() { + assertRSQLQuery(TargetMetadataFields.KEY.name() + "==1", 1); + assertRSQLQuery(TargetMetadataFields.KEY.name() + "!=1", 5); + assertRSQLQuery(TargetMetadataFields.KEY.name() + "=in=(1,2)", 2); + assertRSQLQuery(TargetMetadataFields.KEY.name() + "=out=(1,2)", 4); + } + + @Test + @Description("Test filter target metadata by value") + public void testFilterByParameterValue() { + assertRSQLQuery(TargetMetadataFields.VALUE.name() + "==''", 1); + assertRSQLQuery(TargetMetadataFields.VALUE.name() + "!=''", 5); + assertRSQLQuery(TargetMetadataFields.VALUE.name() + "==1", 1); + assertRSQLQuery(TargetMetadataFields.VALUE.name() + "!=1", 4); + assertRSQLQuery(TargetMetadataFields.VALUE.name() + "=in=(1,2)", 2); + assertRSQLQuery(TargetMetadataFields.VALUE.name() + "=out=(1,2)", 3); + } + + private void assertRSQLQuery(final String rsqlParam, final long expectedEntities) { + + final Page findEnitity = targetManagement + .findMetaDataByControllerIdAndRsql(new PageRequest(0, 100), controllerId, rsqlParam); + final long countAllEntities = findEnitity.getTotalElements(); + assertThat(findEnitity).isNotNull(); + assertThat(countAllEntities).isEqualTo(expectedEntities); + } +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java index 922e4c87b..b6c710f2c 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java @@ -10,6 +10,8 @@ package org.eclipse.hawkbit.mgmt.rest.api; import java.util.List; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut; @@ -37,13 +39,13 @@ public interface MgmtTargetRestApi { /** * Handles the GET request of retrieving a single target. * - * @param controllerId + * @param targetId * the ID of the target to retrieve * @return a single target with status OK. */ - @RequestMapping(method = RequestMethod.GET, value = "/{controllerId}", produces = { MediaTypes.HAL_JSON_VALUE, + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity getTarget(@PathVariable("controllerId") String controllerId); + ResponseEntity getTarget(@PathVariable("targetId") String targetId); /** * Handles the GET request of retrieving all targets. @@ -94,7 +96,7 @@ public interface MgmtTargetRestApi { * path of the request. A given ID in the request body is ignored. It's not * possible to set fields to {@code null} values. * - * @param controllerId + * @param targetId * the path parameter which contains the ID of the target * @param targetRest * the request body which contains the fields which should be @@ -103,40 +105,40 @@ public interface MgmtTargetRestApi { * @return the updated target response which contains all fields also fields * which have not updated */ - @RequestMapping(method = RequestMethod.PUT, value = "/{controllerId}", consumes = { MediaTypes.HAL_JSON_VALUE, + @RequestMapping(method = RequestMethod.PUT, value = "/{targetId}", consumes = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity updateTarget(@PathVariable("controllerId") String controllerId, + ResponseEntity updateTarget(@PathVariable("targetId") String targetId, MgmtTargetRequestBody targetRest); /** * Handles the DELETE request of deleting a target. * - * @param controllerId + * @param targetId * the ID of the target to be deleted - * @return If the given controllerId could exists and could be deleted Http - * OK. In any failure the JsonResponseExceptionHandler is handling - * the response. + * @return If the given targetId could exists and could be deleted Http OK. + * In any failure the JsonResponseExceptionHandler is handling the + * response. */ - @RequestMapping(method = RequestMethod.DELETE, value = "/{controllerId}") - ResponseEntity deleteTarget(@PathVariable("controllerId") String controllerId); + @RequestMapping(method = RequestMethod.DELETE, value = "/{targetId}") + ResponseEntity deleteTarget(@PathVariable("targetId") String targetId); /** * Handles the GET request of retrieving the attributes of a specific * target. * - * @param controllerId + * @param targetId * the ID of the target to retrieve the attributes. * @return the target attributes as map response with status OK */ - @RequestMapping(method = RequestMethod.GET, value = "/{controllerId}/attributes", produces = { + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/attributes", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity getAttributes(@PathVariable("controllerId") String controllerId); + ResponseEntity getAttributes(@PathVariable("targetId") String targetId); /** * Handles the GET request of retrieving the Actions of a specific target. * - * @param controllerId + * @param targetId * to load actions for * @param pagingOffsetParam * the offset of list of targets for pagination, might not be @@ -154,9 +156,9 @@ public interface MgmtTargetRestApi { * status OK. The response is always paged. In any failure the * JsonResponseExceptionHandler is handling the response. */ - @RequestMapping(method = RequestMethod.GET, value = "/{controllerId}/actions", produces = { + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/actions", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity> getActionHistory(@PathVariable("controllerId") String controllerId, + ResponseEntity> getActionHistory(@PathVariable("targetId") String targetId, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) int pagingOffsetParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) String sortParam, @@ -166,22 +168,22 @@ public interface MgmtTargetRestApi { * Handles the GET request of retrieving a specific Actions of a specific * Target. * - * @param controllerId + * @param targetId * to load the action for * @param actionId * to load * @return the action */ - @RequestMapping(method = RequestMethod.GET, value = "/{controllerId}/actions/{actionId}", produces = { + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/actions/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity getAction(@PathVariable("controllerId") String controllerId, + ResponseEntity getAction(@PathVariable("targetId") String targetId, @PathVariable("actionId") Long actionId); /** * Handles the DELETE request of canceling an specific Actions of a specific * Target. * - * @param controllerId + * @param targetId * the ID of the target in the URL path parameter * @param actionId * the ID of the action in the URL path parameter @@ -189,15 +191,15 @@ public interface MgmtTargetRestApi { * optional parameter, which indicates a force cancel * @return status no content in case cancellation was successful */ - @RequestMapping(method = RequestMethod.DELETE, value = "/{controllerId}/actions/{actionId}") - ResponseEntity cancelAction(@PathVariable("controllerId") String controllerId, + @RequestMapping(method = RequestMethod.DELETE, value = "/{targetId}/actions/{actionId}") + ResponseEntity cancelAction(@PathVariable("targetId") String targetId, @PathVariable("actionId") Long actionId, @RequestParam(value = "force", required = false, defaultValue = "false") boolean force); /** * Handles the PUT update request to switch an action from soft to forced. * - * @param controllerId + * @param targetId * the ID of the target in the URL path parameter * @param actionId * the ID of the action in the URL path parameter @@ -205,17 +207,17 @@ public interface MgmtTargetRestApi { * to update the action * @return status no content in case cancellation was successful */ - @RequestMapping(method = RequestMethod.PUT, value = "/{controllerId}/actions/{actionId}", consumes = { + @RequestMapping(method = RequestMethod.PUT, value = "/{targetId}/actions/{actionId}", consumes = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity updateAction(@PathVariable("controllerId") String controllerId, + ResponseEntity updateAction(@PathVariable("targetId") String targetId, @PathVariable("actionId") Long actionId, MgmtActionRequestBodyPut actionUpdate); /** * Handles the GET request of retrieving the ActionStatus of a specific * target and action. * - * @param controllerId + * @param targetId * of the the action * @param actionId * of the status we are intend to load @@ -232,9 +234,9 @@ public interface MgmtTargetRestApi { * with status OK. The response is always paged. In any failure the * JsonResponseExceptionHandler is handling the response. */ - @RequestMapping(method = RequestMethod.GET, value = "/{controllerId}/actions/{actionId}/status", produces = { + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/actions/{actionId}/status", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity> getActionStatusList(@PathVariable("controllerId") String controllerId, + ResponseEntity> getActionStatusList(@PathVariable("targetId") String targetId, @PathVariable("actionId") Long actionId, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) int pagingOffsetParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam, @@ -244,20 +246,20 @@ public interface MgmtTargetRestApi { * Handles the GET request of retrieving the assigned distribution set of an * specific target. * - * @param controllerId + * @param targetId * the ID of the target to retrieve the assigned distribution * * @return the assigned distribution set with status OK, if none is assigned * than {@code null} content (e.g. "{}") */ - @RequestMapping(method = RequestMethod.GET, value = "/{controllerId}/assignedDS", produces = { + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/assignedDS", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity getAssignedDistributionSet(@PathVariable("controllerId") String controllerId); + ResponseEntity getAssignedDistributionSet(@PathVariable("targetId") String targetId); /** * Changes the assigned distribution set of a target. * - * @param controllerId + * @param targetId * of the target to change * @param dsId * of the distributionset that is to be assigned @@ -269,24 +271,113 @@ public interface MgmtTargetRestApi { * complex return body which contains information about the assigned * targets and the already assigned targets counters */ - @RequestMapping(method = RequestMethod.POST, value = "/{controllerId}/assignedDS", consumes = { + @RequestMapping(method = RequestMethod.POST, value = "/{targetId}/assignedDS", consumes = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) ResponseEntity postAssignedDistributionSet( - @PathVariable("controllerId") String controllerId, MgmtDistributionSetAssignment dsId, + @PathVariable("targetId") String targetId, MgmtDistributionSetAssignment dsId, @RequestParam(value = "offline", required = false) boolean offline); /** * Handles the GET request of retrieving the installed distribution set of * an specific target. * - * @param controllerId + * @param targetId * the ID of the target to retrieve * @return the assigned installed set with status OK, if none is installed * than {@code null} content (e.g. "{}") */ - @RequestMapping(method = RequestMethod.GET, value = "/{controllerId}/installedDS", produces = { + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/installedDS", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) - ResponseEntity getInstalledDistributionSet(@PathVariable("controllerId") String controllerId); + ResponseEntity getInstalledDistributionSet(@PathVariable("targetId") String targetId); + + /** + * Gets a paged list of meta data for a target. + * + * @param targetId + * the ID of the target for the meta data + * @param pagingOffsetParam + * the offset of list of targets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=key==abc} + * @return status OK if get request is successful with the paged list of + * meta data + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/metadata", produces = { + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity> getMetadata(@PathVariable("targetId") String targetId, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) int pagingOffsetParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) String sortParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) String rsqlParam); + + /** + * Gets a single meta data value for a specific key of a target. + * + * @param targetId + * the ID of the target to get the meta data from + * @param metadataKey + * the key of the meta data entry to retrieve the value from + * @return status OK if get request is successful with the value of the meta + * data + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/metadata/{metadataKey}", produces = { + MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity getMetadataValue(@PathVariable("targetId") String targetId, + @PathVariable("metadataKey") String metadataKey); + + /** + * Updates a single meta data value of a target. + * + * @param targetId + * the ID of the target to update the meta data entry + * @param metadataKey + * the key of the meta data to update the value + * @param metadata + * update body + * @return status OK if the update request is successful and the updated + * meta data result + */ + @RequestMapping(method = RequestMethod.PUT, value = "/{targetId}/metadata/{metadataKey}", produces = { + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity updateMetadata(@PathVariable("targetId") String targetId, + @PathVariable("metadataKey") String metadataKey, MgmtMetadataBodyPut metadata); + + /** + * Deletes a single meta data entry from the target. + * + * @param targetId + * the ID of the target to delete the meta data entry + * @param metadataKey + * the key of the meta data to delete + * @return status OK if the delete request is successful + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{targetId}/metadata/{metadataKey}") + ResponseEntity deleteMetadata(@PathVariable("targetId") String targetId, + @PathVariable("metadataKey") String metadataKey); + + /** + * Creates a list of meta data for a specific target. + * + * @param targetId + * the ID of the targetId to create meta data for + * @param metadataRest + * the list of meta data entries to create + * @return status created if post request is successful with the value of + * the created meta data + */ + @RequestMapping(method = RequestMethod.POST, value = "/{targetId}/metadata", consumes = { + MediaType.APPLICATION_JSON_VALUE, + MediaTypes.HAL_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity> createMetadata(@PathVariable("targetId") String targetId, + List metadataRest); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java index 387088168..01ead2959 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetMapper.java @@ -93,7 +93,7 @@ public final class MgmtDistributionSetMapper { } return metadata.stream() - .map(metadataRest -> entityFactory.generateMetadata(metadataRest.getKey(), metadataRest.getValue())) + .map(metadataRest -> entityFactory.generateDsMetadata(metadataRest.getKey(), metadataRest.getValue())) .collect(Collectors.toList()); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java index 3cba1781f..683f2f010 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java @@ -321,7 +321,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { // check if distribution set exists otherwise throw exception // immediately final DistributionSetMetadata updated = distributionSetManagement.updateMetaData(distributionSetId, - entityFactory.generateMetadata(metadataKey, metadata.getValue())); + entityFactory.generateDsMetadata(metadataKey, metadata.getValue())); return ResponseEntity.ok(MgmtDistributionSetMapper.toResponseDsMetadata(updated)); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java index fc4e6fef9..e2e6985c0 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.stream.Collectors; import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindow; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; import org.eclipse.hawkbit.mgmt.json.model.MgmtPollStatus; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus; @@ -36,8 +37,10 @@ import org.eclipse.hawkbit.repository.builder.TargetCreate; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.PollStatus; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.rest.data.ResponseList; import org.eclipse.hawkbit.rest.data.SortDirection; import org.eclipse.hawkbit.util.IpUtil; @@ -71,6 +74,9 @@ public final class MgmtTargetMapper { MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, ActionFields.ID.getFieldName() + ":" + SortDirection.DESC, null)) .withRel(MgmtRestConstants.TARGET_V1_ACTIONS).expand()); + response.add(linkTo(methodOn(MgmtTargetRestApi.class).getMetadata(response.getControllerId(), + MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET_VALUE, + MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, null, null)).withRel("metadata")); } static void addPollStatus(final Target target, final MgmtTarget targetRest) { @@ -167,6 +173,17 @@ public final class MgmtTargetMapper { .address(targetRest.getAddress()); } + static List fromRequestTargetMetadata(final List metadata, + final EntityFactory entityFactory) { + if (metadata == null) { + return Collections.emptyList(); + } + + return metadata.stream().map( + metadataRest -> entityFactory.generateTargetMetadata(metadataRest.getKey(), metadataRest.getValue())) + .collect(Collectors.toList()); + } + static List toActionStatusRestResponse(final Collection actionStatus, final DeploymentManagement deploymentManagement) { if (actionStatus == null) { @@ -261,4 +278,15 @@ public final class MgmtTargetMapper { return result; } + static MgmtMetadata toResponseTargetMetadata(final TargetMetadata metadata) { + final MgmtMetadata metadataRest = new MgmtMetadata(); + metadataRest.setKey(metadata.getKey()); + metadataRest.setValue(metadata.getValue()); + return metadataRest; + } + + static List toResponseTargetMetadata(final List metadata) { + return metadata.stream().map(MgmtTargetMapper::toResponseTargetMetadata).collect(Collectors.toList()); + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java index 85055a0a5..ec75e3657 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java @@ -16,6 +16,8 @@ import java.util.Map; import javax.validation.ValidationException; import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut; @@ -38,6 +40,7 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,8 +73,8 @@ public class MgmtTargetResource implements MgmtTargetRestApi { private EntityFactory entityFactory; @Override - public ResponseEntity getTarget(@PathVariable("controllerId") final String controllerId) { - final Target findTarget = findTargetWithExceptionIfNotFound(controllerId); + public ResponseEntity getTarget(@PathVariable("targetId") final String targetId) { + final Target findTarget = findTargetWithExceptionIfNotFound(targetId); // to single response include poll status final MgmtTarget response = MgmtTargetMapper.toResponse(findTarget); MgmtTargetMapper.addPollStatus(findTarget, response); @@ -117,22 +120,21 @@ public class MgmtTargetResource implements MgmtTargetRestApi { } @Override - public ResponseEntity updateTarget(@PathVariable("controllerId") final String controllerId, + public ResponseEntity updateTarget(@PathVariable("targetId") final String targetId, @RequestBody final MgmtTargetRequestBody targetRest) { if (targetRest.isRequestAttributes() != null) { if (targetRest.isRequestAttributes()) { - targetManagement.requestControllerAttributes(controllerId); + targetManagement.requestControllerAttributes(targetId); } else { return ResponseEntity.badRequest().build(); } } - final Target updateTarget = this.targetManagement.update(entityFactory.target().update(controllerId) + final Target updateTarget = this.targetManagement.update(entityFactory.target().update(targetId) .name(targetRest.getName()).description(targetRest.getDescription()).address(targetRest.getAddress()) .securityToken(targetRest.getSecurityToken()).requestAttributes(targetRest.isRequestAttributes())); - final MgmtTarget response = MgmtTargetMapper.toResponse(updateTarget); MgmtTargetMapper.addPollStatus(updateTarget, response); MgmtTargetMapper.addTargetLinks(response); @@ -141,15 +143,15 @@ public class MgmtTargetResource implements MgmtTargetRestApi { } @Override - public ResponseEntity deleteTarget(@PathVariable("controllerId") final String controllerId) { - this.targetManagement.deleteByControllerID(controllerId); - LOG.debug("{} target deleted, return status {}", controllerId, HttpStatus.OK); + public ResponseEntity deleteTarget(@PathVariable("targetId") final String targetId) { + this.targetManagement.deleteByControllerID(targetId); + LOG.debug("{} target deleted, return status {}", targetId, HttpStatus.OK); return ResponseEntity.ok().build(); } @Override - public ResponseEntity getAttributes(@PathVariable("controllerId") final String controllerId) { - final Map controllerAttributes = targetManagement.getControllerAttributes(controllerId); + public ResponseEntity getAttributes(@PathVariable("targetId") final String targetId) { + final Map controllerAttributes = targetManagement.getControllerAttributes(targetId); if (controllerAttributes.isEmpty()) { return ResponseEntity.noContent().build(); } @@ -162,13 +164,13 @@ public class MgmtTargetResource implements MgmtTargetRestApi { @Override public ResponseEntity> getActionHistory( - @PathVariable("controllerId") final String controllerId, + @PathVariable("targetId") final String targetId, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { - findTargetWithExceptionIfNotFound(controllerId); + findTargetWithExceptionIfNotFound(targetId); final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); @@ -178,40 +180,41 @@ public class MgmtTargetResource implements MgmtTargetRestApi { final Slice activeActions; final Long totalActionCount; if (rsqlParam != null) { - activeActions = this.deploymentManagement.findActionsByTarget(rsqlParam, controllerId, pageable); - totalActionCount = this.deploymentManagement.countActionsByTarget(rsqlParam, controllerId); + activeActions = this.deploymentManagement.findActionsByTarget(rsqlParam, targetId, pageable); + totalActionCount = this.deploymentManagement.countActionsByTarget(rsqlParam, targetId); } else { - activeActions = this.deploymentManagement.findActionsByTarget(controllerId, pageable); - totalActionCount = this.deploymentManagement.countActionsByTarget(controllerId); + activeActions = this.deploymentManagement.findActionsByTarget(targetId, pageable); + totalActionCount = this.deploymentManagement.countActionsByTarget(targetId); } - return ResponseEntity.ok(new PagedList<>(MgmtTargetMapper.toResponse(controllerId, activeActions.getContent()), + return ResponseEntity.ok( + new PagedList<>(MgmtTargetMapper.toResponse(targetId, activeActions.getContent()), totalActionCount)); } @Override - public ResponseEntity getAction(@PathVariable("controllerId") final String controllerId, + public ResponseEntity getAction(@PathVariable("targetId") final String targetId, @PathVariable("actionId") final Long actionId) { final Action action = deploymentManagement.findAction(actionId) .orElseThrow(() -> new EntityNotFoundException(Action.class, actionId)); - if (!action.getTarget().getControllerId().equals(controllerId)) { - LOG.warn("given action ({}) is not assigned to given target ({}).", action.getId(), controllerId); + if (!action.getTarget().getControllerId().equals(targetId)) { + LOG.warn("given action ({}) is not assigned to given target ({}).", action.getId(), targetId); return ResponseEntity.notFound().build(); } - return ResponseEntity.ok(MgmtTargetMapper.toResponseWithLinks(controllerId, action)); + return ResponseEntity.ok(MgmtTargetMapper.toResponseWithLinks(targetId, action)); } @Override - public ResponseEntity cancelAction(@PathVariable("controllerId") final String controllerId, + public ResponseEntity cancelAction(@PathVariable("targetId") final String targetId, @PathVariable("actionId") final Long actionId, @RequestParam(value = "force", required = false, defaultValue = "false") final boolean force) { final Action action = deploymentManagement.findAction(actionId) .orElseThrow(() -> new EntityNotFoundException(Action.class, actionId)); - if (!action.getTarget().getControllerId().equals(controllerId)) { - LOG.warn("given action ({}) is not assigned to given target ({}).", actionId, controllerId); + if (!action.getTarget().getControllerId().equals(targetId)) { + LOG.warn("given action ({}) is not assigned to given target ({}).", actionId, targetId); return ResponseEntity.notFound().build(); } @@ -228,12 +231,12 @@ public class MgmtTargetResource implements MgmtTargetRestApi { @Override public ResponseEntity> getActionStatusList( - @PathVariable("controllerId") final String controllerId, @PathVariable("actionId") final Long actionId, + @PathVariable("targetId") final String targetId, @PathVariable("actionId") final Long actionId, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam) { - final Target target = findTargetWithExceptionIfNotFound(controllerId); + final Target target = findTargetWithExceptionIfNotFound(targetId); final Action action = deploymentManagement.findAction(actionId) .orElseThrow(() -> new EntityNotFoundException(Action.class, actionId)); @@ -258,8 +261,8 @@ public class MgmtTargetResource implements MgmtTargetRestApi { @Override public ResponseEntity getAssignedDistributionSet( - @PathVariable("controllerId") final String controllerId) { - final MgmtDistributionSet distributionSetRest = deploymentManagement.getAssignedDistributionSet(controllerId) + @PathVariable("targetId") final String targetId) { + final MgmtDistributionSet distributionSetRest = deploymentManagement.getAssignedDistributionSet(targetId) .map(ds -> { final MgmtDistributionSet response = MgmtDistributionSetMapper.toResponse(ds); MgmtDistributionSetMapper.addLinks(ds, response); @@ -275,21 +278,21 @@ public class MgmtTargetResource implements MgmtTargetRestApi { @Override public ResponseEntity postAssignedDistributionSet( - @PathVariable("controllerId") final String controllerId, + @PathVariable("targetId") final String targetId, @RequestBody final MgmtDistributionSetAssignment dsId, @RequestParam(value = "offline", required = false) final boolean offline) { if (offline) { return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse( - deploymentManagement.offlineAssignedDistributionSet(dsId.getId(), Arrays.asList(controllerId)))); + deploymentManagement.offlineAssignedDistributionSet(dsId.getId(), Arrays.asList(targetId)))); } - findTargetWithExceptionIfNotFound(controllerId); + findTargetWithExceptionIfNotFound(targetId); final MgmtMaintenanceWindowRequestBody maintenanceWindow = dsId.getMaintenanceWindow(); if (maintenanceWindow == null) { return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement - .assignDistributionSet(dsId.getId(), Arrays.asList(new TargetWithActionType(controllerId, + .assignDistributionSet(dsId.getId(), Arrays.asList(new TargetWithActionType(targetId, MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime()))))); } @@ -301,7 +304,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi { return ResponseEntity .ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement.assignDistributionSet(dsId.getId(), - Arrays.asList(new TargetWithActionType(controllerId, + Arrays.asList(new TargetWithActionType(targetId, MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime(), cronSchedule, duration, timezone))))); @@ -309,8 +312,8 @@ public class MgmtTargetResource implements MgmtTargetRestApi { @Override public ResponseEntity getInstalledDistributionSet( - @PathVariable("controllerId") final String controllerId) { - final MgmtDistributionSet distributionSetRest = deploymentManagement.getInstalledDistributionSet(controllerId) + @PathVariable("targetId") final String targetId) { + final MgmtDistributionSet distributionSetRest = deploymentManagement.getInstalledDistributionSet(targetId) .map(set -> { final MgmtDistributionSet response = MgmtDistributionSetMapper.toResponse(set); MgmtDistributionSetMapper.addLinks(set, response); @@ -325,19 +328,19 @@ public class MgmtTargetResource implements MgmtTargetRestApi { return ResponseEntity.ok(distributionSetRest); } - private Target findTargetWithExceptionIfNotFound(final String controllerId) { - return targetManagement.getByControllerID(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + private Target findTargetWithExceptionIfNotFound(final String targetId) { + return targetManagement.getByControllerID(targetId) + .orElseThrow(() -> new EntityNotFoundException(Target.class, targetId)); } @Override - public ResponseEntity updateAction(@PathVariable("controllerId") final String controllerId, + public ResponseEntity updateAction(@PathVariable("targetId") final String targetId, @PathVariable("actionId") final Long actionId, @RequestBody final MgmtActionRequestBodyPut actionUpdate) { Action action = deploymentManagement.findAction(actionId) .orElseThrow(() -> new EntityNotFoundException(Action.class, actionId)); - if (!action.getTarget().getControllerId().equals(controllerId)) { - LOG.warn("given action ({}) is not assigned to given target ({}).", action.getId(), controllerId); + if (!action.getTarget().getControllerId().equals(targetId)) { + LOG.warn("given action ({}) is not assigned to given target ({}).", action.getId(), targetId); return ResponseEntity.notFound().build(); } @@ -347,7 +350,62 @@ public class MgmtTargetResource implements MgmtTargetRestApi { action = deploymentManagement.forceTargetAction(actionId); - return ResponseEntity.ok(MgmtTargetMapper.toResponseWithLinks(controllerId, action)); + return ResponseEntity.ok(MgmtTargetMapper.toResponseWithLinks(targetId, action)); + } + + @Override + public ResponseEntity> getMetadata(@PathVariable("targetId") final String targetId, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeDistributionSetMetadataSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + final Page metaDataPage; + + if (rsqlParam != null) { + metaDataPage = targetManagement.findMetaDataByControllerIdAndRsql(pageable, targetId, rsqlParam); + } else { + metaDataPage = targetManagement.findMetaDataByControllerId(pageable, targetId); + } + + return ResponseEntity.ok(new PagedList<>(MgmtTargetMapper.toResponseTargetMetadata(metaDataPage.getContent()), + metaDataPage.getTotalElements())); + } + + @Override + public ResponseEntity getMetadataValue(@PathVariable("targetId") final String targetId, + @PathVariable("metadataKey") final String metadataKey) { + final TargetMetadata findOne = targetManagement.getMetaDataByControllerId(targetId, metadataKey) + .orElseThrow(() -> new EntityNotFoundException(TargetMetadata.class, targetId, metadataKey)); + return ResponseEntity.ok(MgmtTargetMapper.toResponseTargetMetadata(findOne)); + } + + @Override + public ResponseEntity updateMetadata(@PathVariable("targetId") final String targetId, + @PathVariable("metadataKey") final String metadataKey, @RequestBody final MgmtMetadataBodyPut metadata) { + final TargetMetadata updated = targetManagement.updateMetaData(targetId, + entityFactory.generateTargetMetadata(metadataKey, metadata.getValue())); + return ResponseEntity.ok(MgmtTargetMapper.toResponseTargetMetadata(updated)); + } + + @Override + public ResponseEntity deleteMetadata(@PathVariable("targetId") final String targetId, + @PathVariable("metadataKey") final String metadataKey) { + targetManagement.deleteMetaData(targetId, metadataKey); + return ResponseEntity.ok().build(); + } + + @Override + public ResponseEntity> createMetadata(@PathVariable("targetId") final String targetId, + @RequestBody final List metadataRest) { + final List created = targetManagement.createMetaData(targetId, + MgmtTargetMapper.fromRequestTargetMetadata(metadataRest, entityFactory)); + return new ResponseEntity<>(MgmtTargetMapper.toResponseTargetMetadata(created), HttpStatus.CREATED); } } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index 480b36b4c..b4a300225 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -1036,7 +1036,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final String updateValue = "valueForUpdate"; final DistributionSet testDS = testdataFactory.createDistributionSet("one"); - createDistributionSetMetadata(testDS.getId(), entityFactory.generateMetadata(knownKey, knownValue)); + createDistributionSetMetadata(testDS.getId(), entityFactory.generateDsMetadata(knownKey, knownValue)); final JSONObject jsonObject = new JSONObject().put("key", knownKey).put("value", updateValue); @@ -1060,7 +1060,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final String knownValue = "knownValue"; final DistributionSet testDS = testdataFactory.createDistributionSet("one"); - createDistributionSetMetadata(testDS.getId(), entityFactory.generateMetadata(knownKey, knownValue)); + createDistributionSetMetadata(testDS.getId(), entityFactory.generateDsMetadata(knownKey, knownValue)); mvc.perform(delete("/rest/v1/distributionsets/{dsId}/metadata/{key}", testDS.getId(), knownKey)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); @@ -1076,7 +1076,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final String knownValue = "knownValue"; final DistributionSet testDS = testdataFactory.createDistributionSet("one"); - createDistributionSetMetadata(testDS.getId(), entityFactory.generateMetadata(knownKey, knownValue)); + createDistributionSetMetadata(testDS.getId(), entityFactory.generateDsMetadata(knownKey, knownValue)); mvc.perform(delete("/rest/v1/distributionsets/{dsId}/metadata/XXX", testDS.getId(), knownKey)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); @@ -1094,7 +1094,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final String knownKey = "knownKey"; final String knownValue = "knownValue"; final DistributionSet testDS = testdataFactory.createDistributionSet("one"); - createDistributionSetMetadata(testDS.getId(), entityFactory.generateMetadata(knownKey, knownValue)); + createDistributionSetMetadata(testDS.getId(), entityFactory.generateDsMetadata(knownKey, knownValue)); mvc.perform(get("/rest/v1/distributionsets/{dsId}/metadata/{key}", testDS.getId(), knownKey)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) @@ -1113,7 +1113,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final DistributionSet testDS = testdataFactory.createDistributionSet("one"); for (int index = 0; index < totalMetadata; index++) { createDistributionSetMetadata(testDS.getId(), - entityFactory.generateMetadata(knownKeyPrefix + index, knownValuePrefix + index)); + entityFactory.generateDsMetadata(knownKeyPrefix + index, knownValuePrefix + index)); } mvc.perform(get("/rest/v1/distributionsets/{dsId}/metadata?offset=" + offsetParam + "&limit=" + limitParam, @@ -1189,7 +1189,7 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final DistributionSet testDS = testdataFactory.createDistributionSet("one"); for (int index = 0; index < totalMetadata; index++) { createDistributionSetMetadata(testDS.getId(), - entityFactory.generateMetadata(knownKeyPrefix + index, knownValuePrefix + index)); + entityFactory.generateDsMetadata(knownKeyPrefix + index, knownValuePrefix + index)); } final String rsqlSearchValue1 = "value==knownValue1"; diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index e88f29aa7..e469c4c89 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -26,8 +26,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.net.URISyntaxException; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -45,8 +47,10 @@ import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.exception.MessageNotReadableException; @@ -54,6 +58,7 @@ import org.eclipse.hawkbit.rest.json.model.ExceptionInfo; import org.eclipse.hawkbit.rest.util.JsonBuilder; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.eclipse.hawkbit.util.IpUtil; +import org.json.JSONArray; import org.json.JSONObject; import org.junit.Test; import org.springframework.data.domain.PageRequest; @@ -1564,8 +1569,8 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest final String body = new JSONObject().put("requestAttributes", true).toString(); mvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId).content(body) - .contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); assertThat(targetManagement.isControllerAttributesRequested(knownTargetId)).isTrue(); } @@ -1575,8 +1580,8 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest final String body = new JSONObject().put("description", "verify attribute can be missing").toString(); mvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId).content(body) - .contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); } @Step @@ -1584,8 +1589,8 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest final String body = new JSONObject().put("requestAttributes", false).toString(); mvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId).content(body) - .contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); assertThat(targetManagement.isControllerAttributesRequested(knownTargetId)).isTrue(); } @@ -1649,4 +1654,193 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest assertThat(actionsByTarget.getContent()).hasSize(1); return targetManagement.getByControllerID(tA.getControllerId()).get(); } + + @Test + @Description("Ensures that the metadata creation through API is reflected by the repository.") + public void createMetadata() throws Exception { + final String knownControllerId = "targetIdWithMetadata"; + testdataFactory.createTarget(knownControllerId); + + final String knownKey1 = "knownKey1"; + final String knownKey2 = "knownKey2"; + + final String knownValue1 = "knownValue1"; + final String knownValue2 = "knownValue2"; + + final JSONArray metaData1 = new JSONArray(); + metaData1.put(new JSONObject().put("key", knownKey1).put("value", knownValue1)); + metaData1.put(new JSONObject().put("key", knownKey2).put("value", knownValue2)); + + mvc.perform( + post("/rest/v1/targets/{targetId}/metadata", knownControllerId).accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON).content(metaData1.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("[0]key", equalTo(knownKey1))).andExpect(jsonPath("[0]value", equalTo(knownValue1))) + .andExpect(jsonPath("[1]key", equalTo(knownKey2))) + .andExpect(jsonPath("[1]value", equalTo(knownValue2))); + + final TargetMetadata metaKey1 = targetManagement.getMetaDataByControllerId(knownControllerId, knownKey1).get(); + final TargetMetadata metaKey2 = targetManagement.getMetaDataByControllerId(knownControllerId, knownKey2).get(); + + assertThat(metaKey1.getValue()).isEqualTo(knownValue1); + assertThat(metaKey2.getValue()).isEqualTo(knownValue2); + + // verify quota enforcement + final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerTarget(); + + final JSONArray metaData2 = new JSONArray(); + for (int i = 0; i < maxMetaData - metaData1.length() + 1; ++i) { + metaData2.put(new JSONObject().put("key", knownKey1 + i).put("value", knownValue1 + i)); + } + + mvc.perform( + post("/rest/v1/targets/{targetId}/metadata", knownControllerId).accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON).content(metaData2.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()); + + // verify that the number of meta data entries has not changed + // (we cannot use the PAGE constant here as it tries to sort by ID) + assertThat(targetManagement.findMetaDataByControllerId(new PageRequest(0, Integer.MAX_VALUE), knownControllerId) + .getTotalElements()).isEqualTo(metaData1.length()); + + } + + @Test + @Description("Ensures that a metadata update through API is reflected by the repository.") + public void updateMetadata() throws Exception { + final String knownControllerId = "targetIdWithMetadata"; + + // prepare and create metadata for update + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + final String updateValue = "valueForUpdate"; + + setupTargetWithMetadata(knownControllerId, knownKey, knownValue); + + final JSONObject jsonObject = new JSONObject().put("key", knownKey).put("value", updateValue); + + mvc.perform(put("/rest/v1/targets/{targetId}/metadata/{key}", knownControllerId, knownKey) + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON) + .content(jsonObject.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("key", equalTo(knownKey))).andExpect(jsonPath("value", equalTo(updateValue))); + + final TargetMetadata updatedTargetMetadata = targetManagement + .getMetaDataByControllerId(knownControllerId, knownKey).get(); + assertThat(updatedTargetMetadata.getValue()).isEqualTo(updateValue); + + } + + private void setupTargetWithMetadata(final String knownControllerId, final String knownKey, + final String knownValue) { + testdataFactory.createTarget(knownControllerId); + targetManagement.createMetaData(knownControllerId, + Collections.singletonList(entityFactory.generateTargetMetadata(knownKey, knownValue))); + } + + @Test + @Description("Ensures that a metadata entry deletion through API is reflected by the repository.") + public void deleteMetadata() throws Exception { + final String knownControllerId = "targetIdWithMetadata"; + + // prepare and create metadata for deletion + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + + setupTargetWithMetadata(knownControllerId, knownKey, knownValue); + + mvc.perform(delete("/rest/v1/targets/{targetId}/metadata/{key}", knownControllerId, knownKey)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + assertThat(targetManagement.getMetaDataByControllerId(knownControllerId, knownKey)).isNotPresent(); + } + + @Test + @Description("Ensures that target metadata deletion request to API on an entity that does not exist results in NOT_FOUND.") + public void deleteMetadataThatDoesNotExistLeadsToNotFound() throws Exception { + final String knownControllerId = "targetIdWithMetadata"; + + // prepare and create metadata for deletion + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + + setupTargetWithMetadata(knownControllerId, knownKey, knownValue); + + mvc.perform(delete("/rest/v1/targets/{targetId}/metadata/XXX", knownControllerId)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + mvc.perform(delete("/rest/v1/targets/1234/metadata/{key}", knownKey)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + assertThat(targetManagement.getMetaDataByControllerId(knownControllerId, knownKey)).isPresent(); + } + + @Test + @Description("Ensures that a metadata entry selection through API reflectes the repository content.") + public void getSingleMetadata() throws Exception { + final String knownControllerId = "targetIdWithMetadata"; + + // prepare and create metadata for deletion + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + + setupTargetWithMetadata(knownControllerId, knownKey, knownValue); + + mvc.perform(get("/rest/v1/targets/{targetId}/metadata/{key}", knownControllerId, knownKey)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("key", equalTo(knownKey))).andExpect(jsonPath("value", equalTo(knownValue))); + } + + @Test + @Description("Ensures that a metadata entry paged list selection through API reflectes the repository content.") + public void getPagedListOfMetadata() throws Exception { + final String knownControllerId = "targetIdWithMetadata"; + + final int totalMetadata = 10; + final int limitParam = 5; + final String offsetParam = "0"; + final String knownKeyPrefix = "knownKey"; + final String knownValuePrefix = "knownValue"; + + setupTargetWithMetadata(knownControllerId, knownKeyPrefix, knownValuePrefix, totalMetadata); + + mvc.perform(get("/rest/v1/targets/{targetId}/metadata?offset=" + offsetParam + "&limit=" + limitParam, + knownControllerId)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("size", equalTo(limitParam))).andExpect(jsonPath("total", equalTo(totalMetadata))) + .andExpect(jsonPath("content[0].key", equalTo("knownKey0"))) + .andExpect(jsonPath("content[0].value", equalTo("knownValue0"))); + + } + + private void setupTargetWithMetadata(final String knownControllerId, final String knownKeyPrefix, + final String knownValuePrefix, final int totalMetadata) { + testdataFactory.createTarget(knownControllerId); + + final List targetMetadataEntries = new LinkedList<>(); + for (int index = 0; index < totalMetadata; index++) { + targetMetadataEntries + .add(entityFactory.generateTargetMetadata(knownKeyPrefix + index, knownValuePrefix + index)); + } + targetManagement.createMetaData(knownControllerId, targetMetadataEntries); + } + + @Test + @Description("Ensures that a target metadata filtered query with value==knownValue1 parameter returns only the metadata entries with that value.") + public void searchDistributionSetMetadataRsql() throws Exception { + final String knownControllerId = "targetIdWithMetadata"; + + final int totalMetadata = 10; + final String knownKeyPrefix = "knownKey"; + final String knownValuePrefix = "knownValue"; + + setupTargetWithMetadata(knownControllerId, knownKeyPrefix, knownValuePrefix, totalMetadata); + + final String rsqlSearchValue1 = "value==knownValue1"; + + mvc.perform(get("/rest/v1/targets/{targetId}/metadata?q=" + rsqlSearchValue1, knownControllerId)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("total", equalTo(1))).andExpect(jsonPath("content[0].key", equalTo("knownKey1"))) + .andExpect(jsonPath("content[0].value", equalTo("knownValue1"))); + } } diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targets-api-guide.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targets-api-guide.adoc index ef3ffb199..54bda7f96 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targets-api-guide.adoc +++ b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targets-api-guide.adoc @@ -683,6 +683,241 @@ include::../errors/406.adoc[] include::../errors/429.adoc[] |=== +== GET /rest/v1/targets/{targetId}/metadata + +=== Implementation Notes + +Get a paged list of meta data for a target. Required permission: READ_REPOSITORY + +=== Get a paged list of meta data + +==== Curl + +include::{snippets}/targets/get-metadata/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targets/get-metadata/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targets/get-metadata/path-parameters.adoc[] + +==== Request query parameter + +include::{snippets}/targets/get-metadata-with-parameters/request-parameters.adoc[] + +==== Request parameter example + +include::{snippets}/targets/get-metadata-with-parameters/http-request.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/targets/get-metadata/response-fields.adoc[] + +==== Response example + +include::{snippets}/targets/get-metadata/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + +== POST /rest/v1/targets/{targetId}/metadata + +=== Implementation Notes + +Create a list of meta data entries Required permissions: READ_REPOSITORY and UPDATE_TARGET + +=== Create a list of meta data entries + + +==== CURL + +include::{snippets}/targets/create-metadata/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targets/create-metadata/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targets/create-metadata/path-parameters.adoc[] + +==== Request fields + +include::{snippets}/targets/create-metadata/request-fields.adoc[] + +=== Response (Status 200) + +==== Response example + +include::{snippets}/targets/create-metadata/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/404.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/409.adoc[] +include::../errors/415.adoc[] +include::../errors/429.adoc[] +|=== + + +== DELETE /rest/v1/targets/{targetId}/metadata/{metadataKey} + + +=== Implementation Notes + +Delete a single meta data. Required permission: UPDATE_REPOSITORY + +=== Delete a single meta data + +==== CURL + +include::{snippets}/targets/delete-metadata/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targets/delete-metadata/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targets/delete-metadata/path-parameters.adoc[] + +=== Response (Status 200) + +==== Response example + +include::{snippets}/targets/delete-metadata/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/404.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + + +== GET /rest/v1/targets/{targetId}/metadata/{metadataKey} + + +=== Implementation Notes + +Get a single meta data value for a meta data key. Required permission: READ_REPOSITORY + +=== Get a single meta data value + +==== Curl + +include::{snippets}/targets/get-metadata-value/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targets/get-metadata-value/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targets/get-metadata-value/path-parameters.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/targets/get-metadata-value/response-fields.adoc[] + +==== Response example + +include::{snippets}/targets/get-metadata-value/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + +== PUT /rest/v1/targets/{targetId}/metadata/{metadataKey} + + +=== Implementation Notes + +Update a single meta data value for speficic key. Required permission: UPDATE_REPOSITORY + +=== Update a single meta data value + +==== Curl + +include::{snippets}/targets/update-metadata/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targets/update-metadata/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targets/update-metadata/path-parameters.adoc[] + +==== Request fields + +include::{snippets}/targets/update-metadata/request-fields.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/targets/update-metadata/response-fields.adoc[] + +==== Response example + +include::{snippets}/targets/update-metadata/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/404.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/409.adoc[] +include::../errors/415.adoc[] +include::../errors/429.adoc[] +|=== == Additional content diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/AbstractApiRestDocumentation.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/AbstractApiRestDocumentation.java index b3d607aca..7cf3bf903 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/AbstractApiRestDocumentation.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/AbstractApiRestDocumentation.java @@ -248,7 +248,9 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati fieldWithPath(fieldArrayPrefix + "_links.attributes") .description(MgmtApiModelProperties.LINKS_ATTRIBUTES), fieldWithPath(fieldArrayPrefix + "_links.actions") - .description(MgmtApiModelProperties.LINKS_ACTIONS))); + .description(MgmtApiModelProperties.LINKS_ACTIONS), + fieldWithPath(fieldArrayPrefix + "_links.metadata").description(MgmtApiModelProperties.META_DATA))); + } fields.addAll(Arrays.asList(descriptors)); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java index eb0aeb6ca..716938133 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java @@ -504,7 +504,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat final DistributionSet testDS = testdataFactory.createDistributionSet("one"); for (int index = 0; index < totalMetadata; index++) { distributionSetManagement.createMetaData(testDS.getId(), Lists - .newArrayList(entityFactory.generateMetadata(knownKeyPrefix + index, knownValuePrefix + index))); + .newArrayList(entityFactory.generateDsMetadata(knownKeyPrefix + index, knownValuePrefix + index))); } mockMvc.perform(get(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/{distributionSetId}/metadata", @@ -532,7 +532,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat final DistributionSet testDS = testdataFactory.createDistributionSet("one"); for (int index = 0; index < totalMetadata; index++) { distributionSetManagement.createMetaData(testDS.getId(), Lists - .newArrayList(entityFactory.generateMetadata(knownKeyPrefix + index, knownValuePrefix + index))); + .newArrayList(entityFactory.generateDsMetadata(knownKeyPrefix + index, knownValuePrefix + index))); } mockMvc.perform(get(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/{dsId}/metadata", testDS.getId()) @@ -564,7 +564,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat final String knownValue = "knownValue"; final DistributionSet testDS = testdataFactory.createDistributionSet("one"); distributionSetManagement.createMetaData(testDS.getId(), - Arrays.asList(entityFactory.generateMetadata(knownKey, knownValue))); + Arrays.asList(entityFactory.generateDsMetadata(knownKey, knownValue))); mockMvc.perform(get( MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/{distributionSetId}/metadata/{metadatakey}", @@ -588,7 +588,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat final DistributionSet testDS = testdataFactory.createDistributionSet("one"); distributionSetManagement.createMetaData(testDS.getId(), - Arrays.asList(entityFactory.generateMetadata(knownKey, knownValue))); + Arrays.asList(entityFactory.generateDsMetadata(knownKey, knownValue))); final JSONObject jsonObject = new JSONObject().put("key", knownKey).put("value", updateValue); @@ -616,7 +616,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat final DistributionSet testDS = testdataFactory.createDistributionSet("one"); distributionSetManagement.createMetaData(testDS.getId(), - Arrays.asList(entityFactory.generateMetadata(knownKey, knownValue))); + Arrays.asList(entityFactory.generateDsMetadata(knownKey, knownValue))); mockMvc.perform( delete(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/{distributionSetId}/metadata/{key}", diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java index 171612001..c0b1be26e 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java @@ -20,6 +20,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.paramete import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -28,6 +29,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.ActionStatusFields; import org.eclipse.hawkbit.repository.model.Action; @@ -38,6 +40,7 @@ import org.eclipse.hawkbit.rest.documentation.AbstractApiRestDocumentation; import org.eclipse.hawkbit.rest.documentation.ApiModelPropertiesGeneric; import org.eclipse.hawkbit.rest.documentation.MgmtApiModelProperties; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.json.JSONArray; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; @@ -47,6 +50,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.Lists; import io.qameta.allure.Description; import io.qameta.allure.Feature; @@ -60,7 +64,7 @@ import io.qameta.allure.Story; @Story("Target Resource") public class TargetResourceDocumentationTest extends AbstractApiRestDocumentation { - private final String controllerId = "137"; + private final String targetId = "137"; @Override @Before @@ -72,7 +76,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving all targets within SP. Required Permission: READ_TARGET.") public void getTargets() throws Exception { - createTargetByGivenNameWithAttributes(controllerId, createDistributionSet()); + createTargetByGivenNameWithAttributes(targetId, createDistributionSet()); mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING)).andExpect(status().isOk()) .andDo(MockMvcResultPrinter.print()) @@ -90,7 +94,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .type("enum").attributes( key("value").value("['error', 'in_sync', 'pending', 'registered', 'unknown']")), fieldWithPath("content[].securityToken").description(MgmtApiModelProperties.SECURITY_TOKEN), - fieldWithPath("content[].requestAttributes").description(MgmtApiModelProperties.REQUEST_ATTRIBUTES), + fieldWithPath("content[].requestAttributes") + .description(MgmtApiModelProperties.REQUEST_ATTRIBUTES), fieldWithPath("content[].installedAt").description(MgmtApiModelProperties.INSTALLED_AT), fieldWithPath("content[].lastModifiedAt") .description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("Number"), @@ -142,67 +147,71 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio .attributes(key("value") .value("['error', 'in_sync', 'pending', 'registered', 'unknown']")), fieldWithPath("[]securityToken").description(MgmtApiModelProperties.SECURITY_TOKEN), - fieldWithPath("[]requestAttributes").description(MgmtApiModelProperties.REQUEST_ATTRIBUTES), + fieldWithPath("[]requestAttributes") + .description(MgmtApiModelProperties.REQUEST_ATTRIBUTES), fieldWithPath("[]_links.self").ignored()))); } @Test @Description("Handles the DELETE request of deleting a single target within SP. Required Permission: DELETE_TARGET.") public void deleteTarget() throws Exception { - final Target target = testdataFactory.createTarget(controllerId); + final Target target = testdataFactory.createTarget(targetId); mockMvc.perform( - delete(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}", target.getControllerId())) + delete(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}", target.getControllerId())) .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()).andDo(this.document.document( - pathParameters(parameterWithName("controllerId").description(ApiModelPropertiesGeneric.NAME)))); + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)))); } @Test @Description("Handles the GET request of retrieving a single target within SP. Required Permission: READ_TARGET.") public void getTarget() throws Exception { - final Target target = createTargetByGivenNameWithAttributes(controllerId, createDistributionSet()); + final Target target = createTargetByGivenNameWithAttributes(targetId, createDistributionSet()); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}", target.getControllerId())) + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}", target.getControllerId())) .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( pathParameters( - parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID)), + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), getResponseFieldTarget(false))); } @Test @Description("Handles the PUT request of updating a target within SP. Required Permission: UPDATE_TARGET.") public void putTarget() throws Exception { - final Target target = createTargetByGivenNameWithAttributes(controllerId, createDistributionSet()); - final String targetAsJson = createJsonTarget(controllerId, "newTargetName", "I've been updated"); + final Target target = createTargetByGivenNameWithAttributes(targetId, createDistributionSet()); + final String targetAsJson = createJsonTarget(targetId, "newTargetName", "I've been updated"); - mockMvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}", target.getControllerId()) + mockMvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}", target.getControllerId()) .contentType(MediaType.APPLICATION_JSON_UTF8).content(targetAsJson)).andExpect(status().isOk()) .andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( pathParameters( - parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID)), + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), requestFields(optionalRequestFieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), - optionalRequestFieldWithPath("description").description(ApiModelPropertiesGeneric.DESCRPTION), - optionalRequestFieldWithPath("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID), + optionalRequestFieldWithPath("description") + .description(ApiModelPropertiesGeneric.DESCRPTION), + optionalRequestFieldWithPath("controllerId") + .description(ApiModelPropertiesGeneric.ITEM_ID), optionalRequestFieldWithPath("address").description(MgmtApiModelProperties.ADDRESS), optionalRequestFieldWithPath("securityToken") .description(MgmtApiModelProperties.SECURITY_TOKEN), - optionalRequestFieldWithPath("requestAttributes").description(MgmtApiModelProperties.REQUEST_ATTRIBUTES)), + optionalRequestFieldWithPath("requestAttributes") + .description(MgmtApiModelProperties.REQUEST_ATTRIBUTES)), getResponseFieldTarget(false))); } @Test @Description("Handles the GET request of retrieving the full action history of a specific target. Required Permission: READ_TARGET.") public void getActionsFromTarget() throws Exception { - generateActionForTarget(controllerId); + generateActionForTarget(targetId); mockMvc.perform(get( - MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" + MgmtRestConstants.TARGET_V1_ACTIONS, - controllerId)).andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + MgmtRestConstants.TARGET_V1_ACTIONS, + targetId)).andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( pathParameters( - parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID)), + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), responseFields( fieldWithPath("size").type(JsonFieldType.NUMBER) .description(ApiModelPropertiesGeneric.SIZE), @@ -227,14 +236,14 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving the full action history of a specific target with maintenance window. Required Permission: READ_TARGET.") public void getActionsFromTargetWithMaintenanceWindow() throws Exception { - generateActionForTarget(controllerId, true, false, getTestSchedule(2), getTestDuration(1), getTestTimeZone()); + generateActionForTarget(targetId, true, false, getTestSchedule(2), getTestDuration(1), getTestTimeZone()); mockMvc.perform(get( - MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" + MgmtRestConstants.TARGET_V1_ACTIONS, - controllerId)).andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + MgmtRestConstants.TARGET_V1_ACTIONS, + targetId)).andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( pathParameters( - parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID)), + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), responseFields( fieldWithPath("size").type(JsonFieldType.NUMBER) .description(ApiModelPropertiesGeneric.SIZE), @@ -269,9 +278,9 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving all targets within SP based by parameter. Required Permission: READ_TARGET.") public void getActionsFromTargetWithParameters() throws Exception { - generateActionForTarget(controllerId); + generateActionForTarget(targetId); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + controllerId + "/" + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetId + "/" + MgmtRestConstants.TARGET_V1_ACTIONS + "?limit=10&sort=id:ASC&offset=0&q=status==pending")) .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document(requestParameters( @@ -285,22 +294,22 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Cancels an active action, only active actions can be deleted. Required Permission: UPDATE_TARGET.") public void deleteActionFromTarget() throws Exception { - final Action actions = generateActionForTarget(controllerId, false); + final Action actions = generateActionForTarget(targetId, false); - mockMvc.perform(delete(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" - + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", controllerId, actions.getId())) + mockMvc.perform(delete(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", targetId, actions.getId())) .andExpect(status().isNoContent()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( - pathParameters(parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID), + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID), parameterWithName("actionId").description(ApiModelPropertiesGeneric.ITEM_ID)))); } @Test @Description("Handles the GET request of retrieving all targets within SP based by parameter. Required Permission: READ_TARGET.") public void deleteActionsFromTargetWithParameters() throws Exception { - generateActionForTarget(controllerId); + generateActionForTarget(targetId); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + controllerId + "/" + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetId + "/" + MgmtRestConstants.TARGET_V1_ACTIONS + "?force=true")).andExpect(status().isOk()) .andDo(MockMvcResultPrinter.print()).andDo(this.document.document( requestParameters(parameterWithName("force").description(MgmtApiModelProperties.FORCE)))); @@ -309,15 +318,15 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving a specific action on a specific target. Required Permission: READ_TARGET.") public void getActionFromTarget() throws Exception { - final Action action = generateActionForTarget(controllerId, true, true); + final Action action = generateActionForTarget(targetId, true, true); assertThat(deploymentManagement.findAction(action.getId()).get().getActionType()) .isEqualTo(ActionType.TIMEFORCED); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" - + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", controllerId, action.getId())) + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", targetId, action.getId())) .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( - pathParameters(parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID), + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID), parameterWithName("actionId").description(ApiModelPropertiesGeneric.ITEM_ID)), responseFields(fieldWithPath("createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath("createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), @@ -343,14 +352,14 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving a specific action on a specific target. Required Permission: READ_TARGET.") public void getActionFromTargetWithMaintenanceWindow() throws Exception { - final Action action = generateActionForTarget(controllerId, true, true, getTestSchedule(2), getTestDuration(1), + final Action action = generateActionForTarget(targetId, true, true, getTestSchedule(2), getTestDuration(1), getTestTimeZone()); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" - + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", controllerId, action.getId())) + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", targetId, action.getId())) .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( - pathParameters(parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID), + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID), parameterWithName("actionId").description(ApiModelPropertiesGeneric.ITEM_ID)), responseFields(fieldWithPath("createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath("createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), @@ -386,7 +395,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the PUT request to switch an action from soft to forced. Required Permission: UPDATE_TARGET.") public void switchActionToForced() throws Exception { - final Target target = testdataFactory.createTarget(controllerId); + final Target target = testdataFactory.createTarget(targetId); final DistributionSet set = testdataFactory.createDistributionSet(); final Long actionId = deploymentManagement .assignDistributionSet(set.getId(), ActionType.SOFT, 0, Arrays.asList(target.getControllerId())) @@ -396,13 +405,13 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio final Map body = new HashMap<>(); body.put("forceType", "forced"); - mockMvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" - + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", controllerId, actionId) + mockMvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", targetId, actionId) .content(this.objectMapper.writeValueAsString(body)) .contentType(MediaType.APPLICATION_JSON_UTF8)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andDo(this.document.document( - pathParameters(parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID), + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID), parameterWithName("actionId").description(ApiModelPropertiesGeneric.ITEM_ID)), requestFields( requestFieldWithPath("forceType").description(MgmtApiModelProperties.ACTION_FORCED)), @@ -428,13 +437,13 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving a specific action on a specific target. Required Permission: READ_TARGET.") public void getStatusFromAction() throws Exception { - final Action action = generateActionForTarget(controllerId); + final Action action = generateActionForTarget(targetId); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}/" + MgmtRestConstants.TARGET_V1_ACTION_STATUS, - controllerId, action.getId())).andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + targetId, action.getId())).andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( - pathParameters(parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID), + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID), parameterWithName("actionId").description(ApiModelPropertiesGeneric.ITEM_ID)), responseFields( fieldWithPath("size").type(JsonFieldType.NUMBER) @@ -454,9 +463,9 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving all targets within SP based by parameter. Required Permission: READ_TARGET.") public void getStatusFromActionWithParameters() throws Exception { - final Action action = generateActionForTarget(controllerId); + final Action action = generateActionForTarget(targetId); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + controllerId + "/" + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetId + "/" + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + action.getId() + "/" + MgmtRestConstants.TARGET_V1_ACTION_STATUS + "?limit=10&sort=id:ASC&offset=0")) .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) @@ -469,21 +478,21 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio @Test @Description("Handles the GET request of retrieving the assigned distribution set of an specific target. Required Permission: READ_TARGET.") public void getAssignedDistributionSetFromAction() throws Exception { - generateActionForTarget(controllerId); + generateActionForTarget(targetId); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" - + MgmtRestConstants.TARGET_V1_ASSIGNED_DISTRIBUTION_SET, controllerId)).andExpect(status().isOk()) + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + + MgmtRestConstants.TARGET_V1_ASSIGNED_DISTRIBUTION_SET, targetId)).andExpect(status().isOk()) .andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( pathParameters( - parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID)), + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), getResponseFieldsDistributionSet(false))); } @Test @Description("Handles the POST request for assigning a distribution set to a specific target. Required Permission: READ_REPOSITORY and UPDATE_TARGET.") public void postAssignDistributionSetToTarget() throws Exception { - testdataFactory.createTarget(controllerId); + testdataFactory.createTarget(targetId); final DistributionSet set = testdataFactory.createDistributionSet("one"); final long forceTime = System.currentTimeMillis(); @@ -492,13 +501,13 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio getMaintenanceWindow(getTestSchedule(10), getTestDuration(10), getTestTimeZone())) .toString(); - mockMvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" - + MgmtRestConstants.TARGET_V1_ASSIGNED_DISTRIBUTION_SET, controllerId).content(body) + mockMvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + + MgmtRestConstants.TARGET_V1_ASSIGNED_DISTRIBUTION_SET, targetId).content(body) .contentType(MediaType.APPLICATION_JSON_UTF8)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andDo(this.document.document( pathParameters( - parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID)), + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), requestParameters(parameterWithName("offline") .description(MgmtApiModelProperties.OFFLINE_UPDATE).optional()), requestFields(requestFieldWithPath("forcetime").description(MgmtApiModelProperties.FORCETIME), @@ -528,32 +537,193 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio final Map knownControllerAttrs = new HashMap<>(); knownControllerAttrs.put("a", "1"); knownControllerAttrs.put("b", "2"); - final Target target = testdataFactory.createTarget(controllerId); - controllerManagement.updateControllerAttributes(controllerId, knownControllerAttrs, null); + final Target target = testdataFactory.createTarget(targetId); + controllerManagement.updateControllerAttributes(targetId, knownControllerAttrs, null); // test query target over rest resource mockMvc.perform( - get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/attributes", target.getName())) + get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/attributes", target.getName())) .andDo(MockMvcResultPrinter.print()).andExpect(status().is2xxSuccessful()) .andExpect(jsonPath("$.a", equalTo("1"))).andExpect(jsonPath("$.b", equalTo("2"))) .andDo(this.document.document(pathParameters( - parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID)))); + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)))); } @Test @Description("Handles the GET request of retrieving the installed distribution set of an specific target. Required Permission: READ_TARGET.") public void getInstalledDistributionSetFromTarget() throws Exception { - final Target target = createTargetByGivenNameWithAttributes(controllerId, createDistributionSet()); + final Target target = createTargetByGivenNameWithAttributes(targetId, createDistributionSet()); - mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{controllerId}/" + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + MgmtRestConstants.TARGET_V1_INSTALLED_DISTRIBUTION_SET, target.getName())).andExpect(status().isOk()) .andDo(MockMvcResultPrinter.print()) .andDo(this.document.document( pathParameters( - parameterWithName("controllerId").description(ApiModelPropertiesGeneric.ITEM_ID)), + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), getResponseFieldsDistributionSet(false))); } + @Test + @Description("Get a paged list of meta data for a target with standard page size." + " Required Permission: " + + SpPermission.READ_REPOSITORY) + public void getMetadata() throws Exception { + final int totalMetadata = 4; + final String knownKeyPrefix = "knownKey"; + final String knownValuePrefix = "knownValue"; + final Target testTarget = testdataFactory.createTarget(targetId); + for (int index = 0; index < totalMetadata; index++) { + targetManagement.createMetaData(testTarget.getControllerId(), Lists.newArrayList( + entityFactory.generateTargetMetadata(knownKeyPrefix + index, knownValuePrefix + index))); + } + + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/metadata", + testTarget.getControllerId())).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON_HAL_UTF)) + .andDo(this.document.document( + pathParameters( + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), + responseFields(fieldWithPath("total").description(ApiModelPropertiesGeneric.TOTAL_ELEMENTS), + fieldWithPath("size").type(JsonFieldType.NUMBER) + .description(ApiModelPropertiesGeneric.SIZE), + fieldWithPath("content").description(MgmtApiModelProperties.META_DATA), + fieldWithPath("content[].key").description(MgmtApiModelProperties.META_DATA_KEY), + fieldWithPath("content[].value").description(MgmtApiModelProperties.META_DATA_VALUE)))); + } + + @Test + @Description("Get a paged list of meta data for a target with defined page size and sorting by name descending and key starting with 'known'." + + " Required Permission: " + SpPermission.READ_REPOSITORY) + public void getMetadataWithParameters() throws Exception { + final int totalMetadata = 4; + + final String knownKeyPrefix = "knownKey"; + final String knownValuePrefix = "knownValue"; + final Target testTarget = testdataFactory.createTarget(targetId); + for (int index = 0; index < totalMetadata; index++) { + targetManagement.createMetaData(testTarget.getControllerId(), Lists.newArrayList( + entityFactory.generateTargetMetadata(knownKeyPrefix + index, knownValuePrefix + index))); + } + + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/metadata", + testTarget.getControllerId()).param("offset", "1").param("limit", "2").param("sort", "key:DESC") + .param("q", "key==known*")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON_HAL_UTF)) + .andDo(this.document.document( + requestParameters( + parameterWithName("limit").attributes(key("type").value("query")) + .description(ApiModelPropertiesGeneric.LIMIT), + parameterWithName("sort").description(ApiModelPropertiesGeneric.SORT), + parameterWithName("offset").description(ApiModelPropertiesGeneric.OFFSET), + parameterWithName("q").description(ApiModelPropertiesGeneric.FIQL)), + responseFields(fieldWithPath("total").description(ApiModelPropertiesGeneric.TOTAL_ELEMENTS), + fieldWithPath("size").type(JsonFieldType.NUMBER) + .description(ApiModelPropertiesGeneric.SIZE), + fieldWithPath("content").description(MgmtApiModelProperties.META_DATA), + fieldWithPath("content[].key").description(MgmtApiModelProperties.META_DATA_KEY), + fieldWithPath("content[].value").description(MgmtApiModelProperties.META_DATA_VALUE)))); + } + + @Test + @Description("Get a single meta data value for a meta data key." + " Required Permission: " + + SpPermission.READ_REPOSITORY) + public void getMetadataValue() throws Exception { + + // prepare and create metadata + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + final Target testTarget = testdataFactory.createTarget(targetId); + targetManagement.createMetaData(testTarget.getControllerId(), + Arrays.asList(entityFactory.generateTargetMetadata(knownKey, knownValue))); + + mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/metadata/{metadatakey}", + testTarget.getControllerId(), knownKey)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document( + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID), + parameterWithName("metadatakey").description(ApiModelPropertiesGeneric.ITEM_ID)), + responseFields(fieldWithPath("key").description(MgmtApiModelProperties.META_DATA_KEY), + fieldWithPath("value").description(MgmtApiModelProperties.META_DATA_VALUE)))); + } + + @Test + @Description("Update a single meta data value for specific key." + " Required Permission: " + + SpPermission.UPDATE_REPOSITORY) + public void updateMetadata() throws Exception { + // prepare and create metadata for update + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + final String updateValue = "valueForUpdate"; + + final Target testTarget = testdataFactory.createTarget(targetId); + targetManagement.createMetaData(testTarget.getControllerId(), + Arrays.asList(entityFactory.generateTargetMetadata(knownKey, knownValue))); + + final JSONObject jsonObject = new JSONObject().put("key", knownKey).put("value", updateValue); + + mockMvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/metadata/{metadatakey}", + testTarget.getControllerId(), knownKey) + .contentType(MediaType.APPLICATION_JSON_UTF8).content(jsonObject.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document( + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID), + parameterWithName("metadatakey").description(ApiModelPropertiesGeneric.ITEM_ID)), + requestFields(requestFieldWithPath("key").description(MgmtApiModelProperties.META_DATA_KEY), + requestFieldWithPath("value").description(MgmtApiModelProperties.META_DATA_VALUE)), + responseFields(fieldWithPath("key").description(MgmtApiModelProperties.META_DATA_KEY), + fieldWithPath("value").description(MgmtApiModelProperties.META_DATA_VALUE)))); + + } + + @Test + @Description("Delete a single meta data." + " Required Permission: " + SpPermission.UPDATE_REPOSITORY) + public void deleteMetadata() throws Exception { + // prepare and create metadata for deletion + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + + final Target testTarget = testdataFactory.createTarget(targetId); + targetManagement.createMetaData(testTarget.getControllerId(), + Arrays.asList(entityFactory.generateTargetMetadata(knownKey, knownValue))); + + mockMvc.perform(delete(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/metadata/{key}", + testTarget.getControllerId(), knownKey)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document( + pathParameters(parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID), + parameterWithName("key").description(ApiModelPropertiesGeneric.ITEM_ID)))); + + } + + @Test + @Description("Create a list of meta data entries" + " Required Permission: " + SpPermission.READ_REPOSITORY + + " and " + SpPermission.UPDATE_TARGET) + public void createMetadata() throws Exception { + + final Target testTarget = testdataFactory.createTarget(targetId); + + final String knownKey1 = "knownKey1"; + final String knownKey2 = "knownKey2"; + + final String knownValue1 = "knownValue1"; + final String knownValue2 = "knownValue2"; + + final JSONArray jsonArray = new JSONArray(); + jsonArray.put(new JSONObject().put("key", knownKey1).put("value", knownValue1)); + jsonArray.put(new JSONObject().put("key", knownKey2).put("value", knownValue2)); + + mockMvc.perform( + post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/metadata", + testTarget.getControllerId()).contentType(MediaType.APPLICATION_JSON_UTF8) + .content(jsonArray.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(APPLICATION_JSON_HAL_UTF)) + .andDo(this.document.document( + pathParameters( + parameterWithName("targetId").description(ApiModelPropertiesGeneric.ITEM_ID)), + requestFields(requestFieldWithPath("[]key").description(MgmtApiModelProperties.META_DATA_KEY), + optionalRequestFieldWithPath("[]value") + .description(MgmtApiModelProperties.META_DATA_VALUE)))); + } + private String createTargetJsonForPostRequest(final String controllerId, final String name, final String description) throws JsonProcessingException { final Map target = new HashMap<>(); diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java index d1094caf3..ee921e39d 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java @@ -124,6 +124,11 @@ public class HawkbitSecurityProperties { */ private int maxMetaDataEntriesPerDistributionSet = 100; + /** + * Maximum number of meta data entries per target + */ + private int maxMetaDataEntriesPerTarget = 100; + /** * Maximum number of software modules per distribution set */ @@ -230,6 +235,14 @@ public class HawkbitSecurityProperties { this.maxMetaDataEntriesPerDistributionSet = maxMetaDataEntriesPerDistributionSet; } + public int getMaxMetaDataEntriesPerTarget() { + return maxMetaDataEntriesPerTarget; + } + + public void setMaxMetaDataEntriesPerTarget(final int maxMetaDataEntriesPerTarget) { + this.maxMetaDataEntriesPerTarget = maxMetaDataEntriesPerTarget; + } + public int getMaxSoftwareModulesPerDistributionSet() { return maxSoftwareModulesPerDistributionSet; } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DsMetadataPopupLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DsMetadataPopupLayout.java index 58e3cb5e7..05213fee7 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DsMetadataPopupLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DsMetadataPopupLayout.java @@ -52,7 +52,7 @@ public class DsMetadataPopupLayout extends AbstractMetadataPopupLayout