You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by be...@apache.org on 2018/11/13 12:45:32 UTC
[ambari] branch trunk updated: AMBARI-24881 Add Service Request
JSON (benyoka) (#2594)
This is an automated email from the ASF dual-hosted git repository.
benyoka pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git
The following commit(s) were added to refs/heads/trunk by this push:
new 664d14d AMBARI-24881 Add Service Request JSON (benyoka) (#2594)
664d14d is described below
commit 664d14dae54655a092a304400944bae7e24d1275
Author: benyoka <be...@users.noreply.github.com>
AuthorDate: Tue Nov 13 13:45:28 2018 +0100
AMBARI-24881 Add Service Request JSON (benyoka) (#2594)
* AMBARI-24881 Add Service Request JSON (benyoka)
* AMBARI-24881 review comments (benyoka)
---
.../server/controller/AddServiceRequest.java | 250 ++++++++++++++++++++
.../ambari/server/topology/Configurable.java | 147 ++++++++++++
.../server/controller/AddServiceRequestTest.java | 263 +++++++++++++++++++++
.../ambari/server/topology/ConfigurableTest.java | 104 ++++++++
.../resources/add_service_api/configurable.json | 16 ++
.../resources/add_service_api/configurable2.json | 8 +
.../test/resources/add_service_api/request1.json | 39 +++
.../test/resources/add_service_api/request2.json | 18 ++
.../test/resources/add_service_api/request3.json | 6 +
.../test/resources/add_service_api/request4.json | 13 +
.../add_service_api/request_invalid_1.json | 23 ++
.../add_service_api/request_invalid_2.json | 40 ++++
12 files changed, 927 insertions(+)
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java
new file mode 100644
index 0000000..83a66d8
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.server.controller;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Collections.emptySet;
+import static org.apache.ambari.server.controller.internal.BaseClusterRequest.PROVISION_ACTION_PROPERTY;
+import static org.apache.ambari.server.controller.internal.ProvisionClusterRequest.CONFIG_RECOMMENDATION_STRATEGY;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.ambari.annotations.ApiIgnore;
+import org.apache.ambari.server.controller.internal.ProvisionAction;
+import org.apache.ambari.server.topology.ConfigRecommendationStrategy;
+import org.apache.ambari.server.topology.Configurable;
+import org.apache.ambari.server.topology.Configuration;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * Data object representing an add service request.
+ */
+@ApiModel
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public final class AddServiceRequest {
+
+ static final String OPERATION_TYPE = "operation_type";
+ static final String STACK_NAME = "stack_name";
+ static final String STACK_VERSION = "stack_version";
+ static final String SERVICES = "services";
+ static final String COMPONENTS = "components";
+
+ private final OperationType operationType;
+ private final ConfigRecommendationStrategy recommendationStrategy;
+ private final ProvisionAction provisionAction;
+ private final String stackName;
+ private final String stackVersion;
+ private final Set<Service> services;
+ private final Set<Component> components;
+ private final Configuration configuration;
+
+ @JsonCreator
+ public AddServiceRequest(@JsonProperty(OPERATION_TYPE) OperationType operationType,
+ @JsonProperty(CONFIG_RECOMMENDATION_STRATEGY) ConfigRecommendationStrategy recommendationStrategy,
+ @JsonProperty(PROVISION_ACTION_PROPERTY)ProvisionAction provisionAction,
+ @JsonProperty(STACK_NAME) String stackName,
+ @JsonProperty(STACK_VERSION) String stackVersion,
+ @JsonProperty(SERVICES) Set<Service> services,
+ @JsonProperty(COMPONENTS)Set<Component> components,
+ @JsonProperty(Configurable.CONFIGURATIONS) Collection<? extends Map<String, ?>> configs) {
+ this(operationType, recommendationStrategy, provisionAction, stackName, stackVersion, services, components,
+ Configurable.parseConfigs(configs));
+ }
+
+
+ private AddServiceRequest(OperationType operationType,
+ ConfigRecommendationStrategy recommendationStrategy,
+ ProvisionAction provisionAction,
+ String stackName,
+ String stackVersion,
+ Set<Service> services,
+ Set<Component> components,
+ Configuration configuration) {
+ this.operationType = null != operationType ? operationType : OperationType.ADD_SERVICE;
+ this.recommendationStrategy = null != recommendationStrategy ? recommendationStrategy : ConfigRecommendationStrategy.NEVER_APPLY;
+ this.provisionAction = null != provisionAction ? provisionAction : ProvisionAction.INSTALL_AND_START;
+ this.stackName = stackName;
+ this.stackVersion = stackVersion;
+ this.services = null != services ? services : emptySet();
+ this.components = null != components ? components : emptySet();
+ this.configuration = null != configuration ? configuration : new Configuration(new HashMap<>(), new HashMap<>());
+
+ checkArgument(!this.services.isEmpty() || !this.components.isEmpty(), "Either services or components must be specified");
+ }
+
+
+ @JsonProperty(OPERATION_TYPE)
+ @ApiModelProperty(name = OPERATION_TYPE)
+ public OperationType getOperationType() {
+ return operationType;
+ }
+
+ @JsonProperty(CONFIG_RECOMMENDATION_STRATEGY)
+ @ApiModelProperty(name = CONFIG_RECOMMENDATION_STRATEGY)
+ public ConfigRecommendationStrategy getRecommendationStrategy() {
+ return recommendationStrategy;
+ }
+
+ @JsonProperty(PROVISION_ACTION_PROPERTY)
+ @ApiModelProperty(name = PROVISION_ACTION_PROPERTY)
+ public ProvisionAction getProvisionAction() {
+ return provisionAction;
+ }
+
+ @JsonProperty(STACK_NAME)
+ @ApiModelProperty(name = STACK_NAME)
+ public String getStackName() {
+ return stackName;
+ }
+
+ @JsonProperty(STACK_VERSION)
+ @ApiModelProperty(name = STACK_VERSION)
+ public String getStackVersion() {
+ return stackVersion;
+ }
+
+ @JsonProperty(SERVICES)
+ @ApiModelProperty(name = SERVICES)
+ public Set<Service> getServices() {
+ return services;
+ }
+
+ @JsonProperty(COMPONENTS)
+ @ApiModelProperty(name = COMPONENTS)
+ public Set<Component> getComponents() {
+ return components;
+ }
+
+ @JsonIgnore
+ @ApiIgnore
+ public Configuration getConfiguration() {
+ return configuration;
+ }
+
+ @JsonProperty(Configurable.CONFIGURATIONS)
+ @ApiModelProperty(name = Configurable.CONFIGURATIONS)
+ public Collection<Map<String, Map<String, ?>>> getConfigurationContents() {
+ return Configurable.convertConfigToMap(configuration);
+ }
+
+// ------- inner classes -------
+
+ public enum OperationType {
+ ADD_SERVICE, DELETE_SERVICE, MOVE_SERVICE
+ }
+
+ public static class Component {
+ static final String COMPONENT_NAME = "component_name";
+ static final String FQDN = "fqdn";
+
+ private String name;
+ private String fqdn;
+
+ public static final Component of(String name, String fqdn) {
+ Component component = new Component();
+ component.setName(name);
+ component.setFqdn(fqdn);
+ return component;
+ }
+
+ @JsonProperty(COMPONENT_NAME)
+ @ApiModelProperty(name = COMPONENT_NAME)
+ public String getName() {
+ return name;
+ }
+
+ @JsonProperty(COMPONENT_NAME)
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @JsonProperty(FQDN)
+ @ApiModelProperty(name = FQDN)
+ public String getFqdn() {
+ return fqdn;
+ }
+
+ @JsonProperty(FQDN)
+ public void setFqdn(String fqdn) {
+ this.fqdn = fqdn;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Component component = (Component) o;
+ return Objects.equals(name, component.name) &&
+ Objects.equals(fqdn, component.fqdn);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, fqdn);
+ }
+ }
+
+ @ApiModel
+ public static class Service {
+ static final String NAME = "name";
+
+ private String name;
+
+ public static final Service of(String name) {
+ Service service = new Service();
+ service.setName(name);
+ return service;
+ }
+
+ @JsonProperty(NAME)
+ @ApiModelProperty(name = NAME)
+ public String getName() {
+ return name;
+ }
+
+ @JsonProperty(NAME)
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Service service = (Service) o;
+ return Objects.equals(name, service.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+ }
+}
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/topology/Configurable.java b/ambari-server/src/main/java/org/apache/ambari/server/topology/Configurable.java
new file mode 100644
index 0000000..e8f780d
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/topology/Configurable.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.server.topology;
+
+import static org.apache.ambari.server.controller.internal.BlueprintResourceProvider.PROPERTIES_ATTRIBUTES_PROPERTY_ID;
+import static org.apache.ambari.server.controller.internal.BlueprintResourceProvider.PROPERTIES_PROPERTY_ID;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.apache.ambari.annotations.ApiIgnore;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * Provides support for JSON serializaion of {@link Configuration} objects. Can handle both plain JSON and Ambari style
+ * flattened JSON such as {@code "hdfs-site/properties/dfs.replication": "3"}. Objects may implement this interface or
+ * call its static utility methods.
+ */
+public interface Configurable {
+
+ public static final String CONFIGURATIONS = "configurations";
+
+ @JsonIgnore
+ @ApiIgnore
+ void setConfiguration(Configuration configuration);
+
+ @JsonIgnore
+ @ApiIgnore
+ Configuration getConfiguration();
+
+ @JsonProperty(CONFIGURATIONS)
+ @ApiModelProperty(name = CONFIGURATIONS)
+ default void setConfigs(Collection<? extends Map<String, ?>> configs) {
+ setConfiguration(parseConfigs(configs));
+ }
+
+ @JsonProperty(CONFIGURATIONS)
+ @ApiModelProperty(name = CONFIGURATIONS)
+ default Collection<Map<String, Map<String, ?>>> getConfigs() {
+ return convertConfigToMap(getConfiguration());
+ }
+
+ /**
+ * Parses configuration maps The configs can be in fully structured JSON, e.g.
+ * <code>
+ * [{"hdfs-site":
+ * "properties": {
+ * ""dfs.replication": "3",
+ * ...
+ * },
+ * properties_attributes: {}
+ * }]
+ * </code>
+ * or flattened like
+ * <code>
+ * [{
+ * "hdfs-site/properties/dfs.replication": "3",
+ * ...
+ * }]
+ * </code>
+ * In the latter case it calls {@link ConfigurationFactory#getConfiguration(Collection)}
+ * @param configs
+ * @return
+ */
+ static Configuration parseConfigs(@Nullable Collection<? extends Map<String, ?>> configs) {
+ Configuration configuration;
+
+ if (null == configs) {
+ configuration = new Configuration(new HashMap<>(), new HashMap<>());
+ }
+ else if (!configs.isEmpty() && configs.iterator().next().keySet().iterator().next().contains("/")) {
+ // Configuration has keys with slashes like "zk.cfg/properties/dataDir" means it is coming through
+ // the resource framework and must be parsed with configuration factories
+ configuration = new ConfigurationFactory().getConfiguration((Collection<Map<String, String>>)configs);
+ }
+ else {
+ // If the configuration does not have keys with slashes it means it is coming from plain JSON and needs to be
+ // parsed accordingly.
+ Map<String, Map<String, String>> allProperties = new HashMap<>();
+ Map<String, Map<String, Map<String, String>>> allAttributes = new HashMap<>();
+ configs.forEach( item -> {
+ String configName = item.keySet().iterator().next();
+ Map<String, Object> configData = (Map<String, Object>) item.get(configName);
+ if (configData.containsKey(PROPERTIES_PROPERTY_ID)) {
+ Map<String, String> properties = (Map<String, String>)configData.get(PROPERTIES_PROPERTY_ID);
+ allProperties.put(configName, properties);
+ }
+ if (configData.containsKey(PROPERTIES_ATTRIBUTES_PROPERTY_ID)) {
+ Map<String, Map<String, String>> attributes =
+ (Map<String, Map<String, String>>)configData.get(PROPERTIES_ATTRIBUTES_PROPERTY_ID);
+ allAttributes.put(configName, attributes);
+ }
+ });
+ configuration = new Configuration(allProperties, allAttributes);
+ }
+ return configuration;
+ }
+
+ /**
+ * Converts {@link Configuration} objects to a collection easily serializable to Json
+ * @param configuration the configuration to convert
+ * @return the resulting collection
+ */
+ static Collection<Map<String, Map<String, ?>>> convertConfigToMap(Configuration configuration) {
+ Collection<Map<String, Map<String, ?>>> configurations = new ArrayList<>();
+ Set<String> allConfigTypes = Sets.union(configuration.getProperties().keySet(), configuration.getAttributes().keySet());
+ for (String configType: allConfigTypes) {
+ Map<String, Map<String, ? extends Object>> configData = new HashMap<>();
+ if (configuration.getProperties().containsKey(configType)) {
+ configData.put(PROPERTIES_PROPERTY_ID, configuration.getProperties().get(configType));
+ }
+ if (configuration.getAttributes().containsKey(configType)) {
+ configData.put(PROPERTIES_ATTRIBUTES_PROPERTY_ID, configuration.getAttributes().get(configType));
+ }
+ configurations.add(ImmutableMap.of(configType, configData));
+ }
+ return configurations;
+ }
+
+}
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java
new file mode 100644
index 0000000..2ed98b7
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.server.controller;
+
+import static org.apache.ambari.server.controller.AddServiceRequest.COMPONENTS;
+import static org.apache.ambari.server.controller.AddServiceRequest.Component;
+import static org.apache.ambari.server.controller.AddServiceRequest.OperationType.ADD_SERVICE;
+import static org.apache.ambari.server.controller.AddServiceRequest.SERVICES;
+import static org.apache.ambari.server.controller.AddServiceRequest.STACK_NAME;
+import static org.apache.ambari.server.controller.AddServiceRequest.STACK_VERSION;
+import static org.apache.ambari.server.controller.AddServiceRequest.Service;
+import static org.apache.ambari.server.controller.internal.BaseClusterRequest.PROVISION_ACTION_PROPERTY;
+import static org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_AND_START;
+import static org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_ONLY;
+import static org.apache.ambari.server.controller.internal.ProvisionClusterRequest.CONFIG_RECOMMENDATION_STRATEGY;
+import static org.apache.ambari.server.serveraction.kerberos.KerberosServerAction.OPERATION_TYPE;
+import static org.apache.ambari.server.topology.ConfigRecommendationStrategy.ALWAYS_APPLY;
+import static org.apache.ambari.server.topology.ConfigRecommendationStrategy.NEVER_APPLY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ambari.server.controller.internal.ProvisionAction;
+import org.apache.ambari.server.topology.ConfigRecommendationStrategy;
+import org.apache.ambari.server.topology.Configurable;
+import org.apache.ambari.server.topology.Configuration;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.Resources;
+
+/**
+ * Tests for {@link AddServiceRequest} serialization / deserialization / syntax validation
+ */
+public class AddServiceRequestTest {
+
+ private static String REQUEST_ALL_FIELDS_SET;
+ private static String REQUEST_MINIMAL_SERVICES_AND_COMPONENTS;
+ private static String REQUEST_MINIMAL_SERVICES_ONLY;
+ private static String REQUEST_MINIMAL_COMPONENTS_ONLY;
+ private static String REQUEST_INVALID_NO_SERVICES_AND_COMPONENTS;
+ private static String REQUEST_INVALID_INVALID_FIELD;
+
+
+ private ObjectMapper mapper = new ObjectMapper();
+
+ @BeforeClass
+ public static void setUpClass() {
+ REQUEST_ALL_FIELDS_SET = read("add_service_api/request1.json");
+ REQUEST_MINIMAL_SERVICES_AND_COMPONENTS = read("add_service_api/request2.json");
+ REQUEST_MINIMAL_SERVICES_ONLY = read("add_service_api/request3.json");
+ REQUEST_MINIMAL_COMPONENTS_ONLY = read("add_service_api/request4.json");
+ REQUEST_INVALID_NO_SERVICES_AND_COMPONENTS = read("add_service_api/request_invalid_1.json");
+ REQUEST_INVALID_INVALID_FIELD = read("add_service_api/request_invalid_2.json");
+ }
+
+ @Test
+ public void testDeserialize_basic() throws Exception {
+ AddServiceRequest request = mapper.readValue(REQUEST_ALL_FIELDS_SET, AddServiceRequest.class);
+
+ assertEquals(ADD_SERVICE, request.getOperationType());
+ assertEquals(ALWAYS_APPLY, request.getRecommendationStrategy());
+ assertEquals(INSTALL_ONLY, request.getProvisionAction());
+ assertEquals("HDP", request.getStackName());
+ assertEquals("3.0", request.getStackVersion());
+
+ Configuration configuration = request.getConfiguration();
+ assertEquals(
+ ImmutableMap.of("storm-site", ImmutableMap.of("final", ImmutableMap.of("fs.defaultFS", "true"))),
+ configuration.getAttributes());
+ assertEquals(
+ ImmutableMap.of("storm-site", ImmutableMap.of("ipc.client.connect.max.retries", "50")),
+ configuration.getProperties());
+
+ assertEquals(
+ ImmutableSet.of(Component.of("NIMBUS", "c7401.ambari.apache.org"), Component.of("BEACON_SERVER", "c7402.ambari.apache.org")),
+ request.getComponents());
+
+ assertEquals(
+ ImmutableSet.of(Service.of("STORM"), Service.of("BEACON")),
+ request.getServices());
+
+ }
+
+ @Test
+ public void testDeserialize_defaultAndEmptyValues() throws Exception {
+ AddServiceRequest request = mapper.readValue(REQUEST_MINIMAL_SERVICES_AND_COMPONENTS, AddServiceRequest.class);
+
+ // filled-out values
+ assertEquals(
+ ImmutableSet.of(Component.of("NIMBUS", "c7401.ambari.apache.org"), Component.of("BEACON_SERVER", "c7402.ambari.apache.org")),
+ request.getComponents());
+
+ assertEquals(
+ ImmutableSet.of(Service.of("STORM"), Service.of("BEACON")),
+ request.getServices());
+
+ // default / empty values
+ assertEquals(ADD_SERVICE, request.getOperationType());
+ assertEquals(NEVER_APPLY, request.getRecommendationStrategy());
+ assertEquals(INSTALL_AND_START, request.getProvisionAction());
+ assertNull(request.getStackName());
+ assertNull(request.getStackVersion());
+
+ Configuration configuration = request.getConfiguration();
+ assertTrue(configuration.getFullAttributes().isEmpty());
+ assertTrue(configuration.getFullProperties().isEmpty());
+ }
+
+ @Test
+ public void testDeserialize_onlyServices() throws Exception {
+ AddServiceRequest request = mapper.readValue(REQUEST_MINIMAL_SERVICES_ONLY, AddServiceRequest.class);
+
+ // filled-out values
+ assertEquals(
+ ImmutableSet.of(Service.of("STORM"), Service.of("BEACON")),
+ request.getServices());
+
+ // default / empty values
+ assertEquals(ADD_SERVICE, request.getOperationType());
+ assertEquals(NEVER_APPLY, request.getRecommendationStrategy());
+ assertEquals(INSTALL_AND_START, request.getProvisionAction());
+ assertNull(request.getStackName());
+ assertNull(request.getStackVersion());
+
+ Configuration configuration = request.getConfiguration();
+ assertTrue(configuration.getFullAttributes().isEmpty());
+ assertTrue(configuration.getFullProperties().isEmpty());
+
+ assertTrue(request.getComponents().isEmpty());
+ }
+
+ @Test
+ public void testDeserialize_onlyComponents() throws Exception {
+ AddServiceRequest request = mapper.readValue(REQUEST_MINIMAL_COMPONENTS_ONLY, AddServiceRequest.class);
+
+ // filled-out values
+ assertEquals(
+ ImmutableSet.of(Component.of("NIMBUS", "c7401.ambari.apache.org"), Component.of("BEACON_SERVER", "c7402.ambari.apache.org")),
+ request.getComponents());
+
+ // default / empty values
+ assertEquals(ADD_SERVICE, request.getOperationType());
+ assertEquals(NEVER_APPLY, request.getRecommendationStrategy());
+ assertEquals(INSTALL_AND_START, request.getProvisionAction());
+ assertNull(request.getStackName());
+ assertNull(request.getStackVersion());
+
+ Configuration configuration = request.getConfiguration();
+ assertTrue(configuration.getFullAttributes().isEmpty());
+ assertTrue(configuration.getFullProperties().isEmpty());
+
+ assertTrue(request.getServices().isEmpty());
+ }
+
+ @Test(expected = JsonProcessingException.class)
+ public void testDeserialize_invalid_noServicesAndComponents() throws Exception {
+ mapper.readValue(REQUEST_INVALID_NO_SERVICES_AND_COMPONENTS, AddServiceRequest.class);
+ }
+
+ @Test(expected = JsonProcessingException.class)
+ public void testDeserialize_invalid_invalidField() throws Exception {
+ mapper.readValue(REQUEST_INVALID_INVALID_FIELD, AddServiceRequest.class);
+ }
+
+ @Test
+ public void testSerialize_basic() throws Exception {
+ AddServiceRequest request = mapper.readValue(REQUEST_ALL_FIELDS_SET, AddServiceRequest.class);
+
+ Map<String, ?> serialized = serialize(request);
+
+ assertEquals(AddServiceRequest.OperationType.ADD_SERVICE.name(), serialized.get(OPERATION_TYPE));
+ assertEquals(ConfigRecommendationStrategy.ALWAYS_APPLY.name(), serialized.get(CONFIG_RECOMMENDATION_STRATEGY));
+ assertEquals(ProvisionAction.INSTALL_ONLY.name(), serialized.get(PROVISION_ACTION_PROPERTY));
+ assertEquals("HDP", serialized.get(STACK_NAME));
+ assertEquals("3.0", serialized.get(STACK_VERSION));
+
+ assertEquals(
+ ImmutableSet.of(ImmutableMap.of(Service.NAME, "BEACON"), ImmutableMap.of(Service.NAME, "STORM")),
+ ImmutableSet.copyOf((List<String>) serialized.get(SERVICES)) );
+
+ assertEquals(
+ ImmutableSet.of(
+ ImmutableMap.of(Component.COMPONENT_NAME, "NIMBUS", Component.FQDN, "c7401.ambari.apache.org"),
+ ImmutableMap.of(Component.COMPONENT_NAME, "BEACON_SERVER", Component.FQDN, "c7402.ambari.apache.org")),
+ ImmutableSet.copyOf((List<String>) serialized.get(COMPONENTS)) );
+
+ assertEquals(
+ ImmutableList.of(
+ ImmutableMap.of(
+ "storm-site",
+ ImmutableMap.of(
+ "properties", ImmutableMap.of("ipc.client.connect.max.retries", "50"),
+ "properties_attributes", ImmutableMap.of("final", ImmutableMap.of("fs.defaultFS", "true"))
+ )
+ )
+ ),
+ serialized.get(Configurable.CONFIGURATIONS)
+ );
+ }
+
+ @Test
+ public void testSerialize_EmptyOmitted() throws Exception {
+ AddServiceRequest request = mapper.readValue(REQUEST_MINIMAL_SERVICES_ONLY, AddServiceRequest.class);
+ Map<String, ?> serialized = serialize(request);
+
+ assertEquals(AddServiceRequest.OperationType.ADD_SERVICE.name(), serialized.get(OPERATION_TYPE));
+ assertEquals(ProvisionAction.INSTALL_AND_START.name(), serialized.get(PROVISION_ACTION_PROPERTY));
+ assertEquals(
+ ImmutableSet.of(ImmutableMap.of(Service.NAME, "BEACON"), ImmutableMap.of(Service.NAME, "STORM")),
+ ImmutableSet.copyOf((List<String>) serialized.get(SERVICES)) );
+
+ assertFalse(serialized.containsKey(STACK_NAME));
+ assertFalse(serialized.containsKey(STACK_VERSION));
+ assertFalse(serialized.containsKey(Configurable.CONFIGURATIONS));
+ assertFalse(serialized.containsKey(COMPONENTS));
+
+ }
+
+ private Map<String, ?> serialize(AddServiceRequest request) throws IOException {
+ String serialized = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
+ return mapper.readValue(serialized, new TypeReference<Map<String, ?>>() {});
+ }
+
+ private static String read(String resourceName) {
+ try {
+ return Resources.toString(Resources.getResource(resourceName), StandardCharsets.UTF_8);
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/topology/ConfigurableTest.java b/ambari-server/src/test/java/org/apache/ambari/server/topology/ConfigurableTest.java
new file mode 100644
index 0000000..69aec1f
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/topology/ConfigurableTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.server.topology;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.URL;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Resources;
+
+public class ConfigurableTest {
+ public static final String JSON_LOCATION = "add_service_api/configurable.json";
+ public static final String JSON_LOCATION2 = "add_service_api/configurable2.json";
+
+ private TestConfigurable configurable;
+ private ObjectMapper mapper;
+
+ @Before
+ public void setUp() throws Exception {
+ mapper = new ObjectMapper();
+ URL url = Resources.getResource(JSON_LOCATION);
+ configurable = new ObjectMapper().readValue(url, TestConfigurable.class);
+ }
+
+ /**
+ * Parse normal JSON configuration
+ */
+ @Test
+ public void testParseConfigurable() throws Exception {
+ assertEquals(ImmutableMap.of("zoo.cfg", ImmutableMap.of("dataDir", "/zookeeper1")),
+ configurable.getConfiguration().getProperties());
+ assertEquals(
+ ImmutableMap.of("zoo.cfg",
+ ImmutableMap.of("final",
+ ImmutableMap.of("someProp", "true"))),
+ configurable.getConfiguration().getAttributes());
+ }
+
+ /**
+ * Deserialize normal JSON configuration
+ */
+ @Test
+ public void testSerializaDeserialize() throws Exception {
+ String persisted = mapper.writeValueAsString(configurable);
+ Configurable restored = mapper.readValue(persisted, TestConfigurable.class);
+ assertEquals(configurable.getConfiguration().getProperties(), restored.getConfiguration().getProperties());
+ assertEquals(configurable.getConfiguration().getAttributes(), restored.getConfiguration().getAttributes());
+ }
+
+ /**
+ * Parse flattened configuration
+ */
+ @Test
+ public void testParseConfigurableFromResoueceManager() throws Exception{
+ mapper = new ObjectMapper();
+ URL url = Resources.getResource(JSON_LOCATION2);
+ configurable = new ObjectMapper().readValue(url, TestConfigurable.class);
+
+ assertEquals(ImmutableMap.of("zoo.cfg", ImmutableMap.of("dataDir", "/zookeeper1")),
+ configurable.getConfiguration().getProperties());
+ assertEquals(
+ ImmutableMap.of("zoo.cfg",
+ ImmutableMap.of("final",
+ ImmutableMap.of("someProp", "true"))),
+ configurable.getConfiguration().getAttributes());
+ }
+
+}
+
+class TestConfigurable implements Configurable {
+ Configuration configuration;
+
+ @Override
+ public Configuration getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public void setConfiguration(Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/configurable.json b/ambari-server/src/test/resources/add_service_api/configurable.json
new file mode 100644
index 0000000..cefa23b
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/configurable.json
@@ -0,0 +1,16 @@
+{
+ "configurations" : [
+ {
+ "zoo.cfg" : {
+ "properties" : {
+ "dataDir" : "/zookeeper1"
+ },
+ "properties_attributes": {
+ "final": {
+ "someProp": "true"
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/configurable2.json b/ambari-server/src/test/resources/add_service_api/configurable2.json
new file mode 100644
index 0000000..c27ecf4
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/configurable2.json
@@ -0,0 +1,8 @@
+{
+ "configurations" : [
+ {
+ "zoo.cfg/properties/dataDir" : "/zookeeper1",
+ "zoo.cfg/properties_attributes/final/someProp": "true"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request1.json b/ambari-server/src/test/resources/add_service_api/request1.json
new file mode 100644
index 0000000..cf4ce62
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request1.json
@@ -0,0 +1,39 @@
+{
+ "operation_type" : "ADD_SERVICE",
+ "config_recommendation_strategy" : "ALWAYS_APPLY",
+ "provision_action" : "INSTALL_ONLY",
+ "stack_name" : "HDP",
+ "stack_version" : "3.0",
+
+ "services": [
+ { "name" : "STORM" },
+ { "name" : "BEACON" }
+ ],
+
+ "components" : [
+ {
+ "component_name" : "NIMBUS",
+ "fqdn" : "c7401.ambari.apache.org"
+ },
+ {
+ "component_name" : "BEACON_SERVER",
+ "fqdn" : "c7402.ambari.apache.org"
+ }
+ ],
+
+ "configurations" : [
+ {
+ "storm-site" : {
+ "properties_attributes" : {
+ "final" : {
+ "fs.defaultFS" : "true"
+ }
+ },
+ "properties" : {
+ "ipc.client.connect.max.retries" : "50"
+ }
+ }
+ }
+ ]
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request2.json b/ambari-server/src/test/resources/add_service_api/request2.json
new file mode 100644
index 0000000..f0e540f
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request2.json
@@ -0,0 +1,18 @@
+{
+ "services": [
+ { "name" : "STORM" },
+ { "name" : "BEACON" }
+ ],
+
+ "components" : [
+ {
+ "component_name" : "NIMBUS",
+ "fqdn" : "c7401.ambari.apache.org"
+ },
+ {
+ "component_name" : "BEACON_SERVER",
+ "fqdn" : "c7402.ambari.apache.org"
+ }
+ ]
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request3.json b/ambari-server/src/test/resources/add_service_api/request3.json
new file mode 100644
index 0000000..4e715c1
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request3.json
@@ -0,0 +1,6 @@
+{
+ "services": [
+ { "name" : "STORM" },
+ { "name" : "BEACON" }
+ ]
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request4.json b/ambari-server/src/test/resources/add_service_api/request4.json
new file mode 100644
index 0000000..b6fd0ea
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request4.json
@@ -0,0 +1,13 @@
+{
+ "components" : [
+ {
+ "component_name" : "NIMBUS",
+ "fqdn" : "c7401.ambari.apache.org"
+ },
+ {
+ "component_name" : "BEACON_SERVER",
+ "fqdn" : "c7402.ambari.apache.org"
+ }
+ ]
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request_invalid_1.json b/ambari-server/src/test/resources/add_service_api/request_invalid_1.json
new file mode 100644
index 0000000..432970f
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request_invalid_1.json
@@ -0,0 +1,23 @@
+{
+ "operation_type" : "ADD_SERVICE",
+ "config_recommendation_strategy" : "ALWAYS_APPLY",
+ "provision_action" : "INSTALL_ONLY",
+ "stack_name" : "HDP",
+ "stack_version" : "3.0",
+
+ "configurations" : [
+ {
+ "storm-site" : {
+ "properties_attributes" : {
+ "final" : {
+ "fs.defaultFS" : "true"
+ }
+ },
+ "properties" : {
+ "ipc.client.connect.max.retries" : "50"
+ }
+ }
+ }
+ ]
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request_invalid_2.json b/ambari-server/src/test/resources/add_service_api/request_invalid_2.json
new file mode 100644
index 0000000..a017c0b
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request_invalid_2.json
@@ -0,0 +1,40 @@
+{
+ "mpack" : "HDPCORE-2.0",
+ "operation_type" : "ADD_SERVICE",
+ "config_recommendation_strategy" : "ALWAYS_APPLY",
+ "provision_action" : "INSTALL_ONLY",
+ "stack_name" : "HDP",
+ "stack_version" : "3.0",
+
+ "services": [
+ { "name" : "STORM" },
+ { "name" : "BEACON" }
+ ],
+
+ "components" : [
+ {
+ "component_name" : "NIMBUS",
+ "fqdn" : "c7401.ambari.apache.org"
+ },
+ {
+ "component_name" : "BEACON_SERVER",
+ "fqdn" : "c7402.ambari.apache.org"
+ }
+ ],
+
+ "configurations" : [
+ {
+ "storm-site" : {
+ "properties_attributes" : {
+ "final" : {
+ "fs.defaultFS" : "true"
+ }
+ },
+ "properties" : {
+ "ipc.client.connect.max.retries" : "50"
+ }
+ }
+ }
+ ]
+
+}
\ No newline at end of file