Browse Source

AMBARI-9142. Create new API endpoints for cluster and service kerberos descriptors

John Speidel 10 năm trước cách đây
mục cha
commit
d902509f75
23 tập tin đã thay đổi với 1616 bổ sung7 xóa
  1. 2 0
      ambari-server/src/main/java/org/apache/ambari/server/api/resources/ClusterResourceDefinition.java
  2. 4 0
      ambari-server/src/main/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImpl.java
  3. 3 0
      ambari-server/src/main/java/org/apache/ambari/server/api/resources/ServiceResourceDefinition.java
  4. 94 1
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java
  5. 91 1
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
  6. 695 0
      ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ArtifactResourceProvider.java
  7. 2 0
      ambari-server/src/main/java/org/apache/ambari/server/controller/internal/DefaultProviderModule.java
  8. 3 1
      ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java
  9. 109 0
      ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ArtifactDAO.java
  10. 137 0
      ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntity.java
  11. 98 0
      ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntityPK.java
  12. 10 0
      ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog200.java
  13. 6 0
      ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
  14. 6 0
      ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
  15. 7 0
      ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
  16. 3 1
      ambari-server/src/main/resources/META-INF/persistence.xml
  17. 2 1
      ambari-server/src/test/java/org/apache/ambari/server/api/query/render/DefaultRendererTest.java
  18. 2 1
      ambari-server/src/test/java/org/apache/ambari/server/api/query/render/MinimalRendererTest.java
  19. 2 1
      ambari-server/src/test/java/org/apache/ambari/server/api/resources/ClusterResourceDefinitionTest.java
  20. 10 0
      ambari-server/src/test/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImplTest.java
  21. 64 0
      ambari-server/src/test/java/org/apache/ambari/server/api/resources/ServiceResourceDefinitionTest.java
  22. 229 0
      ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ArtifactResourceProviderTest.java
  23. 37 0
      ambari-server/src/test/java/org/apache/ambari/server/upgrade/UpgradeCatalog200Test.java

+ 2 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/resources/ClusterResourceDefinition.java

@@ -70,6 +70,8 @@ public class ClusterResourceDefinition extends BaseStacksResourceDefinition {
     setChildren.add(new SubResourceDefinition(Resource.Type.AlertDefinition));
     setChildren.add(new SubResourceDefinition(Resource.Type.Alert));
     setChildren.add(new SubResourceDefinition(Resource.Type.ClusterStackVersion));
+    //todo: dynamic sub-resource definition
+    setChildren.add(new SubResourceDefinition(Resource.Type.Artifact));
 
     return setChildren;
   }

+ 4 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImpl.java

@@ -356,6 +356,10 @@ public class ResourceInstanceFactoryImpl implements ResourceInstanceFactory {
         };
         break;
 
+      case Artifact:
+        resourceDefinition = new SimpleResourceDefinition(Resource.Type.Artifact, "artifact", "artifacts");
+        break;
+
       default:
         throw new IllegalArgumentException("Unsupported resource type: " + type);
     }

+ 3 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/resources/ServiceResourceDefinition.java

@@ -50,6 +50,9 @@ public class ServiceResourceDefinition extends BaseResourceDefinition {
     Set<SubResourceDefinition> subs = new HashSet<SubResourceDefinition>();
     subs.add(new SubResourceDefinition(Resource.Type.Component));
     subs.add(new SubResourceDefinition(Resource.Type.Alert));
+    //todo: dynamic sub-resource definition
+    subs.add(new SubResourceDefinition(Resource.Type.Artifact));
+
     return subs;
   }
 }

+ 94 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java

@@ -19,6 +19,8 @@
 package org.apache.ambari.server.api.services;
 
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -169,6 +171,81 @@ public class ClusterService extends BaseService {
     return handleRequest(headers, null, ui, Request.Type.DELETE, createClusterResource(clusterName));
   }
 
+  /**
+   * Handles: GET /clusters/{clusterID}/artifacts
+   * Get all artifacts associated with the cluster.
+   *
+   * @param body         request body
+   * @param headers      http headers
+   * @param ui           uri info
+   * @param clusterName  cluster name
+   *
+   * @return artifact collection resource representation
+   */
+  @GET
+  @Path("{clusterName}/artifacts")
+  @Produces("text/plain")
+  public Response getClusterArtifacts(String body,
+                                      @Context HttpHeaders headers,
+                                      @Context UriInfo ui,
+                                      @PathParam("clusterName") String clusterName) {
+
+    hasPermission(Request.Type.GET, clusterName);
+    return handleRequest(headers, body, ui, Request.Type.GET,
+        createArtifactResource(clusterName, null));
+  }
+
+  /**
+   * Handles: GET /clusters/{clusterID}/artifacts/{artifactName}
+   * Get an artifact resource instance.
+   *
+   * @param body          request body
+   * @param headers       http headers
+   * @param ui            uri info
+   * @param clusterName   cluster name
+   * @param artifactName  artifact name
+   *
+   * @return  artifact instance resource representation
+   */
+  @GET
+  @Path("{clusterName}/artifacts/{artifactName}")
+  @Produces("text/plain")
+  public Response getClusterArtifact(String body,
+                                      @Context HttpHeaders headers,
+                                      @Context UriInfo ui,
+                                      @PathParam("clusterName") String clusterName,
+                                      @PathParam("artifactName") String artifactName) {
+
+    hasPermission(Request.Type.GET, clusterName);
+    return handleRequest(headers, body, ui, Request.Type.GET,
+        createArtifactResource(clusterName, artifactName));
+  }
+
+  /**
+   * Handles: POST /clusters/{clusterID}/artifacts/{artifactName}
+   * Create a cluster artifact.
+   *
+   * @param body          request body
+   * @param headers       http headers
+   * @param ui            uri info
+   * @param clusterName   cluster name
+   * @param artifactName  artifact name
+   * @return
+   */
+  @POST
+  @Path("{clusterName}/artifacts/{artifactName}")
+  @Produces("text/plain")
+  public Response createClusterArtifact(String body,
+                                      @Context HttpHeaders headers,
+                                      @Context UriInfo ui,
+                                      @PathParam("clusterName") String clusterName,
+                                      @PathParam("artifactName") String artifactName) {
+
+    hasPermission(Request.Type.POST, clusterName);
+    return handleRequest(headers, body, ui, Request.Type.POST,
+        createArtifactResource(clusterName, artifactName));
+  }
+
   /**
    * Get the hosts sub-resource
    *
@@ -434,7 +511,7 @@ public class ClusterService extends BaseService {
    * Gets the services for upgrades.
    *
    * @param request the request
-   * @param cluserName the cluster name
+   * @param clusterName the cluster name
    *
    * @return the upgrade services
    */
@@ -475,6 +552,22 @@ public class ClusterService extends BaseService {
         Collections.singletonMap(Resource.Type.Cluster, clusterName));
   }
 
+  /**
+   * Create an artifact resource instance.
+   *
+   * @param clusterName  cluster name
+   * @param artifactName artifact name
+   *
+   * @return an artifact resource instance
+   */
+  ResourceInstance createArtifactResource(String clusterName, String artifactName) {
+    Map<Resource.Type, String> mapIds = new HashMap<Resource.Type, String>();
+    mapIds.put(Resource.Type.Cluster, clusterName);
+    mapIds.put(Resource.Type.Artifact, artifactName);
+
+    return createResource(Resource.Type.Artifact, mapIds);
+  }
+
   /**
    * Determine whether or not the access specified by the given request type is
    * permitted for the current user on the cluster resource identified by the

+ 91 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java

@@ -199,6 +199,79 @@ public class ServiceService extends BaseService {
     return new AlertService(m_clusterName, serviceName, null);
   }
 
+  /**
+   * Handles: POST /clusters/{clusterId}/services/{serviceId}/artifacts/{artifactName}
+   * Create a service artifact instance.
+   *
+   * @param body          http body
+   * @param headers       http headers
+   * @param ui            uri info
+   * @param serviceName   service name
+   * @param artifactName  artifact name
+   *
+   * @return information regarding the created artifact
+   */
+  @POST
+  @Path("{serviceName}/artifacts/{artifactName}")
+  @Produces("text/plain")
+  public Response createArtifact(String body,
+                                 @Context HttpHeaders headers,
+                                 @Context UriInfo ui,
+                                 @PathParam("serviceName") String serviceName,
+                                 @PathParam("artifactName") String artifactName) {
+
+    return handleRequest(headers, body, ui, Request.Type.POST,
+        createArtifactResource(m_clusterName, serviceName, artifactName));
+  }
+
+  /**
+   * Handles: GET /clusters/{clusterId}/services/{serviceId}/artifacts
+   * Get all service artifacts.
+   *
+   * @param body          http body
+   * @param headers       http headers
+   * @param ui            uri info
+   * @param serviceName   service name
+   *
+   * @return artifact collection resource representation
+   */
+  @GET
+  @Path("{serviceName}/artifacts")
+  @Produces("text/plain")
+  public Response getArtifacts(String body,
+                              @Context HttpHeaders headers,
+                              @Context UriInfo ui,
+                              @PathParam("serviceName") String serviceName) {
+
+    return handleRequest(headers, body, ui, Request.Type.GET,
+        createArtifactResource(m_clusterName, serviceName, null));
+  }
+
+  /**
+   * Handles: GET /clusters/{clusterId}/services/{serviceId}/artifacts/{artifactName}
+   * Gat a service artifact instance.
+   *
+   * @param body          http body
+   * @param headers       http headers
+   * @param ui            uri info
+   * @param serviceName   service name
+   * @param artifactName  artifact name
+   *
+   * @return artifact instance resource representation
+   */
+  @GET
+  @Path("{serviceName}/artifacts/{artifactName}")
+  @Produces("text/plain")
+  public Response getArtifact(String body,
+                                 @Context HttpHeaders headers,
+                                 @Context UriInfo ui,
+                                 @PathParam("serviceName") String serviceName,
+                                 @PathParam("artifactName") String artifactName) {
+
+    return handleRequest(headers, body, ui, Request.Type.GET,
+        createArtifactResource(m_clusterName, serviceName, artifactName));
+  }
+
   /**
    * Gets the alert history service
    *
@@ -220,7 +293,6 @@ public class ServiceService extends BaseService {
   /**
    * Create a service resource instance.
    *
-   *
    * @param clusterName  cluster name
    * @param serviceName  service name
    *
@@ -233,4 +305,22 @@ public class ServiceService extends BaseService {
 
     return createResource(Resource.Type.Service, mapIds);
   }
+
+  /**
+   * Create an artifact resource instance.
+   *
+   * @param clusterName   cluster name
+   * @param serviceName   service name
+   * @param artifactName  artifact name
+   *
+   * @return an artifact resource instance
+   */
+  ResourceInstance createArtifactResource(String clusterName, String serviceName, String artifactName) {
+    Map<Resource.Type,String> mapIds = new HashMap<Resource.Type, String>();
+    mapIds.put(Resource.Type.Cluster, clusterName);
+    mapIds.put(Resource.Type.Service, serviceName);
+    mapIds.put(Resource.Type.Artifact, artifactName);
+
+    return createResource(Resource.Type.Artifact, mapIds);
+  }
 }

+ 695 - 0
ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ArtifactResourceProvider.java

@@ -0,0 +1,695 @@
+/**
+ * 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.internal;
+
+import com.google.inject.Inject;
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.DuplicateResourceException;
+import org.apache.ambari.server.ObjectNotFoundException;
+import org.apache.ambari.server.ParentObjectNotFoundException;
+import org.apache.ambari.server.StaticallyInject;
+import org.apache.ambari.server.controller.AmbariManagementController;
+import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
+import org.apache.ambari.server.controller.spi.NoSuchResourceException;
+import org.apache.ambari.server.controller.spi.Predicate;
+import org.apache.ambari.server.controller.spi.Request;
+import org.apache.ambari.server.controller.spi.RequestStatus;
+import org.apache.ambari.server.controller.spi.Resource;
+import org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException;
+import org.apache.ambari.server.controller.spi.SystemException;
+import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
+import org.apache.ambari.server.controller.utilities.PropertyHelper;
+import org.apache.ambari.server.orm.dao.ArtifactDAO;
+import org.apache.ambari.server.orm.entities.ArtifactEntity;
+import org.apache.ambari.server.state.Cluster;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Provider for cluster artifacts.
+ * Artifacts contain an artifact name as the PK and artifact data in the form of
+ * a map which is the content of the artifact.
+ * <p>
+ * An example of an artifact is a kerberos descriptor.
+ */
+//todo: implement ExtendedResourceProvider???
+@StaticallyInject
+public class ArtifactResourceProvider extends AbstractResourceProvider {
+  /**
+   * artifact name
+   */
+  public static final String ARTIFACT_NAME_PROPERTY =
+      PropertyHelper.getPropertyId("Artifacts", "artifact_name");
+
+  /**
+   * artifact data
+   */
+  public static final String ARTIFACT_DATA_PROPERTY = "artifact_data";
+
+  /**
+   * primary key fields
+   */
+  private static Set<String> pkPropertyIds = new HashSet<String>();
+
+  /**
+   * map of resource type to fk field
+   */
+  private static Map<Resource.Type, String> keyPropertyIds =
+      new HashMap<Resource.Type, String>();
+
+  /**
+   * resource properties
+   */
+  private static Set<String> propertyIds = new HashSet<String>();
+
+  /**
+   * map of resource type to type registration
+   */
+  private static final Map<Resource.Type, TypeRegistration> typeRegistrations =
+      new HashMap<Resource.Type, TypeRegistration>();
+
+  /**
+   * map of foreign key field to type registration
+   */
+  private static final Map<String, TypeRegistration> typeRegistrationsByFK =
+      new HashMap<String, TypeRegistration>();
+
+  /**
+   * map of short foreign key field to type registration
+   */
+  private static final Map<String, TypeRegistration> typeRegistrationsByShortFK =
+      new HashMap<String, TypeRegistration>();
+
+  /**
+   * artifact data access object
+   */
+  @Inject
+  private static ArtifactDAO artifactDAO;
+
+
+  /**
+   * set resource properties, pk and fk's
+   */
+  static {
+    // resource properties
+    propertyIds.add(ARTIFACT_NAME_PROPERTY);
+    propertyIds.add(ARTIFACT_DATA_PROPERTY);
+
+    // pk property
+    pkPropertyIds.add(ARTIFACT_NAME_PROPERTY);
+
+    // key properties
+    keyPropertyIds.put(Resource.Type.Artifact, ARTIFACT_NAME_PROPERTY);
+
+    //todo: external registration
+    // cluster registration
+    ClusterTypeRegistration clusterTypeRegistration = new ClusterTypeRegistration();
+    typeRegistrations.put(clusterTypeRegistration.getType(), clusterTypeRegistration);
+
+    //service registration
+    ServiceTypeRegistration serviceTypeRegistration = new ServiceTypeRegistration();
+    typeRegistrations.put(serviceTypeRegistration.getType(), serviceTypeRegistration);
+
+    //todo: detect resource type and fk name collisions during registration
+    for (TypeRegistration registration: typeRegistrations.values()) {
+      String fkProperty = registration.getFKPropertyName();
+      keyPropertyIds.put(registration.getType(), fkProperty);
+      propertyIds.add(fkProperty);
+
+      typeRegistrationsByFK.put(fkProperty, registration);
+      typeRegistrationsByShortFK.put(registration.getShortFKPropertyName(), registration);
+
+      for (Map.Entry<Resource.Type, String> ancestor : registration.getForeignKeyInfo().entrySet()) {
+        Resource.Type ancestorType = ancestor.getKey();
+        if (! keyPropertyIds.containsKey(ancestorType)) {
+          String ancestorFK = ancestor.getValue();
+          keyPropertyIds.put(ancestorType, ancestorFK);
+          propertyIds.add(ancestorFK);
+        }
+      }
+    }
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param controller  management controller
+   */
+  @Inject
+  protected ArtifactResourceProvider(AmbariManagementController controller) {
+    super(propertyIds, keyPropertyIds);
+
+    for (TypeRegistration typeRegistration : typeRegistrations.values()) {
+      typeRegistration.setManagementController(controller);
+    }
+  }
+
+  @Override
+  protected Set<String> getPKPropertyIds() {
+    return pkPropertyIds;
+  }
+
+  @Override
+  public RequestStatus createResources(Request request)
+      throws SystemException,
+             UnsupportedPropertyException,
+             ResourceAlreadyExistsException,
+             NoSuchParentResourceException {
+
+    for (Map<String, Object> properties : request.getProperties()) {
+      createResources(getCreateCommand(properties));
+    }
+    notifyCreate(Resource.Type.Artifact, request);
+
+    return getRequestStatus(null);
+  }
+
+  @Override
+  public Set<Resource> getResources(Request request, Predicate predicate)
+      throws SystemException,
+             UnsupportedPropertyException,
+             NoSuchResourceException,
+             NoSuchParentResourceException {
+
+    Set<Map<String, Object>> requestProps = getPropertyMaps(predicate);
+    Set<Resource> resources = new LinkedHashSet<Resource>();
+
+    for (Map<String, Object> props : requestProps) {
+      resources.addAll(getResources(getGetCommand(request, predicate, props)));
+    }
+
+    if (resources.isEmpty() && isInstanceRequest(requestProps)) {
+      throw new NoSuchResourceException(
+          "The requested resource doesn't exist: Artifact not found, " + predicate);
+    }
+    return resources;
+  }
+
+  @Override
+  public RequestStatus updateResources(Request request, Predicate predicate)
+      throws SystemException,
+             UnsupportedPropertyException,
+             NoSuchResourceException,
+             NoSuchParentResourceException {
+
+    throw new UnsupportedOperationException("Update not currently supported for Artifact resources");
+  }
+
+  @Override
+  public RequestStatus deleteResources(Predicate predicate)
+      throws SystemException,
+             UnsupportedPropertyException,
+             NoSuchResourceException,
+             NoSuchParentResourceException {
+
+    throw new UnsupportedOperationException("Delete not currently supported for Artifact resources");
+  }
+
+  /**
+   * Create a command to create the resource.
+   *
+   * @param properties  request properties
+   *
+   * @return a new create command
+   */
+  private Command<Void> getCreateCommand(final Map<String, Object> properties) {
+    return new Command<Void>() {
+      @Override
+      public Void invoke() throws AmbariException {
+        // ensure that parent exists
+        validateParent(properties);
+
+        String artifactName = String.valueOf(properties.get(ARTIFACT_NAME_PROPERTY));
+        TreeMap<String, String> foreignKeyMap = createForeignKeyMap(properties);
+
+        if (artifactDAO.findByNameAndForeignKeys(artifactName, foreignKeyMap) != null) {
+          throw new DuplicateResourceException(String.format(
+              "Attempted to create an artifact which already exists, artifact_name='%s', foreign_keys='%s'",
+              artifactName, getRequestForeignKeys(properties)));
+        }
+
+        LOG.debug("Creating Artifact Resource with name '{}'. Parent information: {}",
+            artifactName, getRequestForeignKeys(properties));
+
+        artifactDAO.create(toEntity(properties));
+
+        return null;
+      }
+    };
+  }
+
+  /**
+   * Create a command to get the requested resources.
+   *
+   * @param properties  request properties
+   *
+   * @return a new get command
+   */
+  private Command<Set<Resource>> getGetCommand(final Request request,
+                                               final Predicate predicate,
+                                               final Map<String, Object> properties) {
+    return new Command<Set<Resource>>() {
+      @Override
+      public Set<Resource> invoke() throws AmbariException {
+        String name = (String) properties.get(ARTIFACT_NAME_PROPERTY);
+        validateParent(properties);
+
+        Set<Resource> matchingResources = new HashSet<Resource>();
+        TreeMap<String, String> foreignKeys = createForeignKeyMap(properties);
+        Set<String> requestPropertyIds = getRequestPropertyIds(request, predicate);
+        if (name != null) {
+          // find instance using name and foreign keys
+          ArtifactEntity entity = artifactDAO.findByNameAndForeignKeys(name, foreignKeys);
+          if (entity != null) {
+            Resource instance = (toResource(entity, requestPropertyIds));
+            if (predicate.evaluate(instance)) {
+              matchingResources.add(instance);
+            }
+          }
+        } else {
+          // find collection using foreign keys only
+          List<ArtifactEntity> results = artifactDAO.findByForeignKeys(foreignKeys);
+          for (ArtifactEntity entity : results) {
+            Resource resource = toResource(entity, requestPropertyIds);
+            if (predicate.evaluate(resource)) {
+              matchingResources.add(resource);
+            }
+          }
+        }
+        return matchingResources;
+      }
+    };
+  }
+
+  /**
+   * Validate that parent resources exist.
+   *
+   * @param properties  request properties
+   *
+   * @throws ParentObjectNotFoundException  if the parent resource doesn't exist
+   * @throws AmbariException if an error occurred while attempting to validate the parent
+   */
+  private void validateParent(Map<String, Object> properties) throws AmbariException {
+    Resource.Type parentType = getRequestType(properties);
+    if (! typeRegistrations.get(parentType).instanceExists(keyPropertyIds, properties)) {
+      throw new ParentObjectNotFoundException(String.format(
+       "Parent resource doesn't exist: %s", getRequestForeignKeys(properties)));
+    }
+  }
+
+  /**
+   * Get the type of the parent resource from the request properties.
+   *
+   * @param properties  request properties
+   *
+   * @return the parent resource type based on the request properties
+   *
+   * @throws AmbariException  if unable to determine the parent resource type
+   */
+  private Resource.Type getRequestType(Map<String, Object> properties) throws AmbariException {
+    Set<String> requestFKs = getRequestForeignKeys(properties).keySet();
+    for (TypeRegistration registration : typeRegistrations.values()) {
+      Collection<String> typeFKs = new HashSet<String>(registration.getForeignKeyInfo().values());
+      typeFKs.add(registration.getFKPropertyName());
+      if (requestFKs.equals(typeFKs)) {
+        return registration.getType();
+      }
+    }
+    throw new AmbariException("Couldn't determine resource type based on request properties");
+  }
+
+  /**
+   * Get a map of foreign key to value for the given request properties.
+   * The foreign key map will only include the foreign key properties which
+   * are included in the request properties.  This is useful for reporting
+   * errors back to the user.
+   * .
+   * @param properties  request properties
+   *
+   * @return map of foreign key to value for the provided request properties
+   */
+  private Map<String, String> getRequestForeignKeys(Map<String, Object> properties) {
+    Map<String, String> requestFKs = new HashMap<String, String>();
+    for (String property : properties.keySet()) {
+      if (! property.equals(ARTIFACT_NAME_PROPERTY) && ! property.startsWith(ARTIFACT_DATA_PROPERTY)) {
+        requestFKs.put(property, String.valueOf(properties.get(property)));
+      }
+    }
+    return requestFKs;
+  }
+
+  /**
+   * Convert a map of properties to an artifact entity.
+   *
+   * @param properties  property map
+   *
+   * @return new artifact entity
+   */
+  private ArtifactEntity toEntity(Map<String, Object> properties)
+      throws AmbariException {
+
+    String name = (String) properties.get(ARTIFACT_NAME_PROPERTY);
+    if (name == null || name.isEmpty()) {
+      throw new IllegalArgumentException("Artifact name must be provided");
+    }
+
+    ArtifactEntity artifact = new ArtifactEntity();
+    artifact.setArtifactName(name);
+    Map<String, Object> dataMap = new HashMap<String, Object>();
+    for (Map.Entry<String, Object> entry : properties.entrySet()) {
+      String key = entry.getKey();
+      //todo: should we handle scalar value?
+      if (key.startsWith(ARTIFACT_DATA_PROPERTY)) {
+        dataMap.put(key.split("/")[1], entry.getValue());
+      }
+    }
+    artifact.setArtifactData(dataMap);
+    artifact.setForeignKeys(createForeignKeyMap(properties));
+
+    return artifact;
+  }
+
+  /**
+   * Create a map of foreign keys and values which can be persisted.
+   * This map will include the short fk names of the key properties as well
+   * as the 'persist id' representation of the value which is returned
+   * by the type registration.
+   *
+   * @param properties  request properties
+   * @return an ordered map of key name to value
+   *
+   * @throws AmbariException an unexpected exception occurred
+   */
+  private TreeMap<String, String> createForeignKeyMap(Map<String, Object> properties) throws AmbariException {
+    TreeMap<String, String> foreignKeys = new TreeMap<String, String>();
+    for (String keyProperty : keyPropertyIds.values()) {
+      if (! keyProperty.equals(ARTIFACT_NAME_PROPERTY)) {
+        String origValue = (String) properties.get(keyProperty);
+        if (origValue != null && ! origValue.isEmpty()) {
+          TypeRegistration typeRegistration = typeRegistrationsByFK.get(keyProperty);
+          foreignKeys.put(typeRegistration.getShortFKPropertyName(), typeRegistration.toPersistId(origValue));
+        }
+      }
+    }
+    return foreignKeys;
+  }
+
+  /**
+   * Create a resource instance from an artifact entity.
+   * This will convert short fk property names to the full property name as well
+   * as converting the value from the 'persist id' representation which is written
+   * to the database.
+   *
+   * @param entity        artifact entity
+   * @param requestedIds  requested id's
+   *
+   * @return a new resource instance for the given artifact entity
+   */
+  private Resource toResource(ArtifactEntity entity, Set<String> requestedIds) throws AmbariException {
+    Resource resource = new ResourceImpl(Resource.Type.Artifact);
+    setResourceProperty(resource, ARTIFACT_NAME_PROPERTY, entity.getArtifactName(), requestedIds);
+    setResourceProperty(resource, ARTIFACT_DATA_PROPERTY, entity.getArtifactData(), requestedIds);
+
+    for (Map.Entry<String, String> entry : entity.getForeignKeys().entrySet()) {
+      TypeRegistration typeRegistration = typeRegistrationsByShortFK.get(entry.getKey());
+      setResourceProperty(resource, typeRegistration.getFKPropertyName(),
+          typeRegistration.fromPersistId(entry.getValue()), requestedIds);
+    }
+    return resource;
+  }
+
+  /**
+   * Determine if the request was for an instance resource.
+   *
+   * @param requestProps  request properties
+   *
+   * @return true if the request was for a specific instance, false otherwise
+   */
+  private boolean isInstanceRequest(Set<Map<String, Object>> requestProps) {
+    return requestProps.size() == 1 &&
+        requestProps.iterator().next().get(ARTIFACT_NAME_PROPERTY) != null;
+  }
+
+  //todo: when static registration is changed to external registration, this interface
+  //todo: should be extracted as a first class interface.
+  /**
+   * Used to register a dynamic sub-resource with an existing resource type.
+   */
+  public static interface TypeRegistration {
+    /**
+     * Allows the management controller to be set on the registration.
+     * This is called as part of the registration process.
+     * For registrations that need access to the management controller,
+     * they should assign this controller to a member field.
+     *
+     * @param  controller  management controller
+     */
+    public void setManagementController(AmbariManagementController controller);
+
+    /**
+     * Get the type of the registering resource.
+     *
+     * @return type of the register resource
+     */
+    public Resource.Type getType();
+
+    /**
+     * Full foreign key property name to use in the artifact resource.
+     * At this time, all foreign key properties should be in the "Artifacts" category.
+     *
+     * @return  the absolute foreign key property name.
+     *          For example: "Artifacts/cluster_name
+     */
+    //todo: use relative property names
+    public String getFKPropertyName();
+
+    /**
+     * Shortened foreign key name that is written to the database.
+     * This name doesn't need to be in any category but must be unique
+     * across all registrations.
+     *
+     * @return short fk name.  For example: "cluster_name"
+     */
+    public String getShortFKPropertyName();
+
+    /**
+     * Convert the foreign key value to a value that is persisted to the database.
+     * In most cases this will be the original value.
+     * <p>
+     * An example of when this will be different is when the fk value value needs
+     * to be converted to the unique id for the resource.
+     * <p>
+     * For example, the cluster_name to the cluster_id.
+     * <p>
+     * This returned value will later be converted back to the normal form via
+     * {@link #fromPersistId(String)}.
+     *
+     * @param value normal form of the fk value used by the api
+     *
+     * @return persist id form of the fk value
+     *
+     * @throws AmbariException if unable to convert the value
+     */
+    public String toPersistId(String value) throws AmbariException;
+
+    /**
+     * Convert the persist id form of the foreign key which is written to the database
+     * to the form used by the api. In most cases, this will be the same.
+     * <p>
+     * This method takes the value returned from {@link #toPersistId(String)} and converts
+     * it back to the original value which is used by the api.
+     * <p>
+     * An  example of this is the converting the cluster name to the cluster id in
+     * {@link #toPersistId(String)} and then back to the cluster name by this method.  The
+     * api always uses the cluster name so we wouldn't want to return the id back as the
+     * value for a cluster_name foreign key.
+     *
+     * @param value  persist id form of the fk value
+     *
+     * @return  normal form of the fk value used by the api
+     *
+     * @throws AmbariException if unable to convert the value
+     */
+    public String fromPersistId(String value) throws AmbariException;
+
+    /**
+     * Get a map of ancestor type to foreign key.
+     * <p>
+     * <b>Note: Currently, if a parent resource has also registered the same dynamic resource,
+     * the foreign key name used here has to match the value returned by the parent resource
+     * in {@link #getFKPropertyName()}</b>
+     *
+     * @return map of ancestor type to foreign key
+     */
+    //todo: look at the need to use the same name as specified by ancestors
+    public Map<Resource.Type, String> getForeignKeyInfo();
+
+    /**
+     * Determine if the instance identified by the provided properties exists.
+     *
+     * @param keyMap      map of resource type to foreign key properties
+     * @param properties  request properties
+     *
+     * @return true if the resource instance exists, false otherwise
+     *
+     * @throws AmbariException  an exception occurs trying to determine if the instance exists
+     */
+    public boolean instanceExists(Map<Resource.Type, String> keyMap,
+                                  Map<String, Object> properties) throws AmbariException;
+  }
+
+
+  //todo: Registration should be done externally and these implementations should be moved
+  //todo: to a location where the registering resource definition has access to them.
+  /**
+   * Cluster resource registration.
+   */
+  private static class ClusterTypeRegistration implements TypeRegistration {
+    /**
+     * management controller instance
+     */
+    private AmbariManagementController controller = null;
+
+    /**
+     * cluster name property name
+     */
+    private static final String CLUSTER_NAME = PropertyHelper.getPropertyId("Artifacts", "cluster_name");
+
+    @Override
+    public void setManagementController(AmbariManagementController controller) {
+      this.controller = controller;
+    }
+
+    @Override
+    public Resource.Type getType() {
+      return Resource.Type.Cluster;
+    }
+
+    @Override
+    public String getFKPropertyName() {
+      return CLUSTER_NAME;
+    }
+
+    @Override
+    public String getShortFKPropertyName() {
+      return "cluster";
+    }
+
+    @Override
+    public String toPersistId(String value) throws AmbariException {
+      return String.valueOf(controller.getClusters().getCluster(value).getClusterId());
+    }
+
+    @Override
+    public String fromPersistId(String value) throws AmbariException {
+      return controller.getClusters().getClusterById(Long.valueOf(value)).getClusterName();
+    }
+
+    @Override
+    public Map<Resource.Type, String> getForeignKeyInfo() {
+      return Collections.emptyMap();
+    }
+
+    @Override
+    public boolean instanceExists(Map<Resource.Type, String> keyMap,
+                                  Map<String, Object> properties) throws AmbariException {
+      try {
+        String clusterName = String.valueOf(properties.get(CLUSTER_NAME));
+        controller.getClusters().getCluster(clusterName);
+        return true;
+      } catch (ObjectNotFoundException e) {
+        // doesn't exist
+      }
+      return false;
+    }
+  }
+
+  /**
+   * Service resource registration.
+   */
+  private static class ServiceTypeRegistration implements TypeRegistration {
+    /**
+     * management controller instance
+     */
+    private AmbariManagementController controller = null;
+
+    /**
+     * service name property name
+     */
+    private static final String SERVICE_NAME = PropertyHelper.getPropertyId("Artifacts", "service_name");
+
+    @Override
+    public void setManagementController(AmbariManagementController controller) {
+      this.controller = controller;
+    }
+
+    @Override
+    public Resource.Type getType() {
+      return Resource.Type.Service;
+    }
+
+    @Override
+    public String getFKPropertyName() {
+      return SERVICE_NAME;
+    }
+
+    @Override
+    public String getShortFKPropertyName() {
+      return "service";
+    }
+
+    @Override
+    public String toPersistId(String value) {
+      return value;
+    }
+
+    @Override
+    public String fromPersistId(String value) {
+      return value;
+    }
+
+    @Override
+    public Map<Resource.Type, String> getForeignKeyInfo() {
+      return Collections.singletonMap(Resource.Type.Cluster, "Artifacts/cluster_name");
+    }
+
+    @Override
+    public boolean instanceExists(Map<Resource.Type, String> keyMap,
+                                  Map<String, Object> properties) throws AmbariException {
+
+      String clusterName = String.valueOf(properties.get(keyMap.get(Resource.Type.Cluster)));
+      try {
+        Cluster cluster = controller.getClusters().getCluster(clusterName);
+        cluster.getService(String.valueOf(properties.get(SERVICE_NAME)));
+        return true;
+      } catch (ObjectNotFoundException e) {
+        // doesn't exist
+      }
+      return false;
+    }
+  }
+}

+ 2 - 0
ambari-server/src/main/java/org/apache/ambari/server/controller/internal/DefaultProviderModule.java

@@ -114,6 +114,8 @@ public class DefaultProviderModule extends AbstractProviderModule {
         return new OperatingSystemResourceProvider(managementController);
       case Repository:
         return new RepositoryResourceProvider(managementController);
+      case Artifact:
+        return new ArtifactResourceProvider(managementController);
 
       default:
         return AbstractControllerResourceProvider.getResourceProvider(type, propertyIds,

+ 3 - 1
ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java

@@ -136,7 +136,8 @@ public interface Resource {
     UpgradeItem,
     PreUpgradeCheck,
     Stage,
-    StackArtifact;
+    StackArtifact,
+    Artifact;
 
     /**
      * Get the {@link Type} that corresponds to this InternalType.
@@ -234,6 +235,7 @@ public interface Resource {
     public static final Type PreUpgradeCheck = InternalType.PreUpgradeCheck.getType();
     public static final Type Stage = InternalType.Stage.getType();
     public static final Type StackArtifact = InternalType.StackArtifact.getType();
+    public static final Type Artifact = InternalType.Artifact.getType();
 
     /**
      * The type name.

+ 109 - 0
ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ArtifactDAO.java

@@ -0,0 +1,109 @@
+/**
+ * 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.orm.dao;
+
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.persist.Transactional;
+import org.apache.ambari.server.orm.RequiresSession;
+import org.apache.ambari.server.orm.entities.ArtifactEntity;
+
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+
+/**
+ * Cluster Artifact Data Access Object.
+ */
+@Singleton
+public class ArtifactDAO {
+  /**
+   * JPA entity manager
+   */
+  @Inject
+  Provider<EntityManager> entityManagerProvider;
+
+  /**
+   * Find an artifact with the given name and foreign keys.
+   *
+   * @param artifactName name of artifact to find
+   * @param foreignKeys  foreign keys of artifact as json representation of
+   *                     a map of key properties to values
+   *
+   * @return  a matching artifact or null
+   */
+  @RequiresSession
+  public ArtifactEntity findByNameAndForeignKeys(String artifactName, TreeMap<String, String> foreignKeys) {
+    //todo: need to update PK in DB
+    TypedQuery<ArtifactEntity> query = entityManagerProvider.get()
+        .createNamedQuery("artifactByNameAndForeignKeys", ArtifactEntity.class);
+    query.setParameter("artifactName", artifactName);
+    query.setParameter("foreignKeys", ArtifactEntity.serializeForeignKeys(foreignKeys));
+
+    try {
+      return query.getSingleResult();
+    } catch (NoResultException ignored) {
+      return null;
+    }
+  }
+
+  /**
+   * Find all artifacts for the specified foreign keys.
+   *
+   * @param foreignKeys  foreign keys of artifact as json representation of
+   *                     a map of key properties to values
+   *
+   * @return all artifacts for the specified foreign keys or an empty List
+   */
+  @RequiresSession
+  public List<ArtifactEntity> findByForeignKeys(TreeMap<String, String> foreignKeys) {
+    TypedQuery<ArtifactEntity> query = entityManagerProvider.get().
+        createNamedQuery("artifactByForeignKeys", ArtifactEntity.class);
+    query.setParameter("foreignKeys", ArtifactEntity.serializeForeignKeys(foreignKeys));
+
+    return query.getResultList();
+  }
+
+  /**
+   * Refresh the state of the instance from the database,
+   * overwriting changes made to the entity, if any.
+   *
+   * @param entity  entity to refresh
+   */
+  @Transactional
+  public void refresh(ArtifactEntity entity) {
+    entityManagerProvider.get().refresh(entity);
+  }
+
+  /**
+   * Make an instance managed and persistent.
+   *
+   * @param entity  entity to persist
+   */
+  @Transactional
+  public void create(ArtifactEntity entity) {
+    entityManagerProvider.get().persist(entity);
+  }
+}

+ 137 - 0
ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntity.java

@@ -0,0 +1,137 @@
+/**
+ * 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.orm.entities;
+
+import com.google.gson.Gson;
+
+import javax.persistence.Basic;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import javax.persistence.Transient;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Entity representing an Artifact.
+ */
+@IdClass(ArtifactEntityPK.class)
+@Table(name = "artifact")
+@NamedQueries({
+  @NamedQuery(name = "artifactByNameAndForeignKeys",
+    query = "SELECT artifact FROM ArtifactEntity artifact " +
+              "WHERE artifact.artifactName=:artifactName AND artifact.foreignKeys=:foreignKeys"),
+  @NamedQuery(name = "artifactByForeignKeys",
+    query = "SELECT artifact FROM ArtifactEntity artifact " +
+            "WHERE artifact.foreignKeys=:foreignKeys")
+})
+
+@Entity
+public class ArtifactEntity {
+  @Id
+  @Column(name = "artifact_name", nullable = false, insertable = true, updatable = false, unique = true)
+  private String artifactName;
+
+  @Id
+  @Column(name = "foreign_keys", nullable = false, insertable = true, updatable = false)
+  @Basic
+  private String foreignKeys;
+
+  @Column(name = "artifact_data", nullable = false, insertable = true, updatable = false)
+  @Basic
+  private String artifactData;
+
+  @Transient
+  private static final Gson jsonSerializer = new Gson();
+
+
+  /**
+   * Get the artifact name.
+   *
+   * @return artifact name
+   */
+  public String getArtifactName() {
+    return artifactName;
+  }
+
+  /**
+   * Set the artifact name.
+   *
+   * @param artifactName  the artifact name
+   */
+  public void setArtifactName(String artifactName) {
+    this.artifactName = artifactName;
+  }
+
+  /**
+   * Set the artifact data by specifying a map that is then
+   * converted to a json string.
+   *
+   * @param artifactData  artifact data map
+   */
+  public void setArtifactData(Map<String, Object> artifactData) {
+    this.artifactData = jsonSerializer.toJson(artifactData);
+  }
+
+  /**
+   * Get the artifact data as a map
+   *
+   * @return artifact data as a map
+   */
+  public Map<String, Object> getArtifactData() {
+    return jsonSerializer.<Map<String, Object>>fromJson(
+        artifactData, Map.class);
+  }
+
+  /**
+   * Set the foreign keys.
+   *
+   * @param foreignKeys  ordered map of foreign key property names to values
+   */
+  public void setForeignKeys(TreeMap<String, String> foreignKeys) {
+    this.foreignKeys = serializeForeignKeys(foreignKeys);
+  }
+
+  /**
+   * Get the foreign keys.
+   *
+   * @return foreign key map of property name to value
+   */
+  public Map<String, String> getForeignKeys() {
+    return foreignKeys == null ?
+        Collections.<String, String>emptyMap() :
+        jsonSerializer.<Map<String, String>>fromJson(foreignKeys, Map.class);
+  }
+
+  /**
+   * Serialize a map of foreign keys to a string.
+   *
+   * @param foreignKeys  map of foreign keys to values
+   *
+   * @return string representation of the foreign keys map
+   */
+  public static String serializeForeignKeys(TreeMap<String, String> foreignKeys) {
+    return jsonSerializer.toJson(foreignKeys);
+  }
+}

+ 98 - 0
ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntityPK.java

@@ -0,0 +1,98 @@
+/**
+ * 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.orm.entities;
+
+import javax.persistence.Column;
+import javax.persistence.Id;
+
+/**
+ * Composite primary key for ArtifactEntity.
+ */
+public class ArtifactEntityPK {
+  @Id
+  @Column(name = "artifact_name", nullable = false, insertable = true, updatable = false)
+  private String artifactName;
+
+  @Id
+  @Column(name = "foreign_keys", nullable = false, insertable = true, updatable = false)
+  private String foreignKeys;
+
+  /**
+   * Constructor.
+   *
+   @param artifactName  artifact name
+   @param foreignKeys   foreign key information
+   */
+  public ArtifactEntityPK(String artifactName, String foreignKeys) {
+    this.artifactName = artifactName;
+    this.foreignKeys = foreignKeys;
+  }
+
+  /**
+   * Get the name of the associated artifact.
+   *
+   * @return artifact name
+   */
+  public String getArtifactName() {
+    return artifactName;
+  }
+
+  /**
+   * Set the name of the associated artifact.
+   *
+   * @param name  artifact name
+   */
+  public void setArtifactName(String name) {
+    artifactName = name;
+  }
+
+  /**
+   * Get the foreign key information of the associated artifact.
+   *
+   * @return foreign key information
+   */
+  public String getForeignKeys() {
+    return foreignKeys;
+  }
+
+  /**
+   * Set the foreign key information of the associated artifact.
+   *
+   * @param foreignKeys  foreign key information
+   */
+  public void setForeignKeys(String foreignKeys) {
+    this.foreignKeys = foreignKeys;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    ArtifactEntityPK that = (ArtifactEntityPK) o;
+
+    return this.artifactName.equals(that.artifactName) &&
+        this.foreignKeys.equals(that.foreignKeys);
+  }
+
+  @Override
+  public int hashCode() {
+    return 31 * artifactName.hashCode() + foreignKeys.hashCode();
+  }
+}

+ 10 - 0
ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog200.java

@@ -63,6 +63,7 @@ public class UpgradeCatalog200 extends AbstractUpgradeCatalog {
   private static final String ALERT_TARGET_TABLE = "alert_target";
   private static final String ALERT_TARGET_STATES_TABLE = "alert_target_states";
   private static final String ALERT_CURRENT_TABLE = "alert_current";
+  private static final String ARTIFACT_TABLE = "artifact";
 
   /**
    * {@inheritDoc}
@@ -107,6 +108,7 @@ public class UpgradeCatalog200 extends AbstractUpgradeCatalog {
   protected void executeDDLUpdates() throws AmbariException, SQLException {
     prepareRollingUpgradesDDL();
     executeAlertDDLUpdates();
+    createArtifactTable();
 
     // add security_state to various tables
     dbAccessor.addColumn("hostcomponentdesiredstate", new DBColumnInfo(
@@ -255,6 +257,14 @@ public class UpgradeCatalog200 extends AbstractUpgradeCatalog {
     dbAccessor.executeQuery("INSERT INTO ambari_sequences(sequence_name, sequence_value) VALUES('upgrade_item_id_seq', 0)", false);
   }
 
+  private void createArtifactTable() throws SQLException {
+    ArrayList<DBColumnInfo> columns = new ArrayList<DBColumnInfo>();
+    columns.add(new DBColumnInfo("artifact_name", String.class, 255, null, false));
+    columns.add(new DBColumnInfo("foreign_keys", String.class, null, null, false));
+    columns.add(new DBColumnInfo("artifact_data", char[].class, null, null, false));
+    dbAccessor.createTable(ARTIFACT_TABLE, columns, "artifact_name", "foreign_keys");
+  }
+
   // ----- UpgradeCatalog ----------------------------------------------------
 
   /**

+ 6 - 0
ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql

@@ -517,6 +517,12 @@ CREATE TABLE repo_version (
   PRIMARY KEY(repo_version_id)
 );
 
+CREATE TABLE artifact (
+  artifact_name VARCHAR(255) NOT NULL,
+  foreign_keys LONGTEXT NOT NULL,
+  artifact_data VARCHAR(4096) NOT NULL,
+  PRIMARY KEY(artifact_name, foreign_keys));
+
 -- altering tables by creating unique constraints----------
 ALTER TABLE users ADD CONSTRAINT UNQ_users_0 UNIQUE (user_name, ldap_user);
 ALTER TABLE groups ADD CONSTRAINT UNQ_groups_0 UNIQUE (group_name, ldap_group);

+ 6 - 0
ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql

@@ -507,6 +507,12 @@ CREATE TABLE repo_version (
   PRIMARY KEY(repo_version_id)
 );
 
+CREATE TABLE artifact (
+  artifact_name VARCHAR2(255) NOT NULL,
+  foreign_keys CLOB NOT NULL,
+  artifact_data VARCHAR2(4096) NOT NULL,
+  PRIMARY KEY(artifact_name, foreign_keys));
+
 --------altering tables by creating unique constraints----------
 ALTER TABLE users ADD CONSTRAINT UNQ_users_0 UNIQUE (user_name, ldap_user);
 ALTER TABLE groups ADD CONSTRAINT UNQ_groups_0 UNIQUE (group_name, ldap_group);

+ 7 - 0
ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql

@@ -449,6 +449,13 @@ GRANT ALL PRIVILEGES ON TABLE ambari.hostgroup_component TO :username;
 GRANT ALL PRIVILEGES ON TABLE ambari.blueprint_configuration TO :username;
 GRANT ALL PRIVILEGES ON TABLE ambari.hostgroup_configuration TO :username;
 
+CREATE TABLE ambari.artifact (
+  artifact_name VARCHAR(255) NOT NULL,
+  artifact_data TEXT NOT NULL,
+  foreign_keys VARCHAR(4096) NOT NULL,
+  PRIMARY KEY (artifact_name, foreign_keys));
+GRANT ALL PRIVILEGES ON TABLE ambari.artifact TO :username;
+
 CREATE TABLE ambari.viewmain (
   view_name VARCHAR(255) NOT NULL,
   label VARCHAR(255),

+ 3 - 1
ambari-server/src/main/resources/META-INF/persistence.xml

@@ -75,8 +75,10 @@
     <class>org.apache.ambari.server.orm.entities.ViewInstancePropertyEntity</class>
     <class>org.apache.ambari.server.orm.entities.ViewParameterEntity</class>
     <class>org.apache.ambari.server.orm.entities.ViewResourceEntity</class>
+    <class>org.apache.ambari.server.orm.entities.ArtifactEntity</class>
 
-    <properties>
+
+      <properties>
       <!--<property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost/ambari" />-->
       <!--<property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />-->
       <property name="eclipselink.cache.size.default" value="10000" />

+ 2 - 1
ambari-server/src/test/java/org/apache/ambari/server/api/query/render/DefaultRendererTest.java

@@ -51,6 +51,7 @@ public class DefaultRendererTest {
     // schema expectations
     expect(schemaFactory.getSchema(Resource.Type.Component)).andReturn(schema).anyTimes();
     expect(schemaFactory.getSchema(Resource.Type.Alert)).andReturn(schema).anyTimes();
+    expect(schemaFactory.getSchema(Resource.Type.Artifact)).andReturn(schema).anyTimes();
     expect(schema.getKeyPropertyId(Resource.Type.Component)).andReturn("ServiceComponentInfo/component_name").anyTimes();
     expect(schema.getKeyPropertyId(Resource.Type.Service)).andReturn("ServiceComponentInfo/service_name").anyTimes();
 
@@ -64,7 +65,7 @@ public class DefaultRendererTest {
     TreeNode<Set<String>> propertyTree = renderer.finalizeProperties(queryTree, false);
     // no properties should have been added
     assertTrue(propertyTree.getObject().isEmpty());
-    assertEquals(2, propertyTree.getChildren().size());
+    assertEquals(3, propertyTree.getChildren().size());
 
     TreeNode<Set<String>> componentNode = propertyTree.getChild("Component");
     assertEquals(2, componentNode.getObject().size());

+ 2 - 1
ambari-server/src/test/java/org/apache/ambari/server/api/query/render/MinimalRendererTest.java

@@ -58,6 +58,7 @@ public class MinimalRendererTest {
     // schema expectations
     expect(schemaFactory.getSchema(Resource.Type.Component)).andReturn(schema).anyTimes();
     expect(schemaFactory.getSchema(Resource.Type.Alert)).andReturn(schema).anyTimes();
+    expect(schemaFactory.getSchema(Resource.Type.Artifact)).andReturn(schema).anyTimes();
     expect(schema.getKeyPropertyId(Resource.Type.Component)).andReturn("ServiceComponentInfo/component_name").anyTimes();
 
     replay(schemaFactory, schema);
@@ -70,7 +71,7 @@ public class MinimalRendererTest {
     TreeNode<Set<String>> propertyTree = renderer.finalizeProperties(queryTree, false);
     // no properties should have been added
     assertTrue(propertyTree.getObject().isEmpty());
-    assertEquals(2, propertyTree.getChildren().size());
+    assertEquals(3, propertyTree.getChildren().size());
 
     TreeNode<Set<String>> componentNode = propertyTree.getChild("Component");
     assertEquals(1, componentNode.getObject().size());

+ 2 - 1
ambari-server/src/test/java/org/apache/ambari/server/api/resources/ClusterResourceDefinitionTest.java

@@ -50,7 +50,7 @@ public class ClusterResourceDefinitionTest {
     ResourceDefinition resource = new ClusterResourceDefinition();
     Set<SubResourceDefinition> subResources = resource.getSubResourceDefinitions();
 
-    assertEquals(11, subResources.size());
+    assertEquals(12, subResources.size());
     assertTrue(includesType(subResources, Resource.Type.Service));
     assertTrue(includesType(subResources, Resource.Type.Host));
     assertTrue(includesType(subResources, Resource.Type.Configuration));
@@ -62,6 +62,7 @@ public class ClusterResourceDefinitionTest {
     assertTrue(includesType(subResources, Resource.Type.ClusterPrivilege));
     assertTrue(includesType(subResources, Resource.Type.Alert));
     assertTrue(includesType(subResources, Resource.Type.ClusterStackVersion));
+    assertTrue(includesType(subResources, Resource.Type.Artifact));
   }
 
   @Test

+ 10 - 0
ambari-server/src/test/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImplTest.java

@@ -37,4 +37,14 @@ public class ResourceInstanceFactoryImplTest {
     assertEquals("artifacts", resourceDefinition.getPluralName());
     assertEquals(Resource.Type.StackArtifact, resourceDefinition.getType());
   }
+
+  @Test
+  public void testGetArtifactDefinition() {
+    ResourceDefinition resourceDefinition = ResourceInstanceFactoryImpl.getResourceDefinition(
+        Resource.Type.Artifact, null);
+
+    assertEquals("artifact", resourceDefinition.getSingularName());
+    assertEquals("artifacts", resourceDefinition.getPluralName());
+    assertEquals(Resource.Type.Artifact, resourceDefinition.getType());
+  }
 }

+ 64 - 0
ambari-server/src/test/java/org/apache/ambari/server/api/resources/ServiceResourceDefinitionTest.java

@@ -0,0 +1,64 @@
+/**
+ * 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.api.resources;
+
+import org.apache.ambari.server.controller.spi.Resource;
+import org.junit.Test;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * ServiceResourceDefinition unit tests.
+ */
+public class ServiceResourceDefinitionTest {
+
+  @Test
+  public void testGetPluralName() {
+    assertEquals("services", new ServiceResourceDefinition().getPluralName());
+  }
+
+  @Test
+  public void testGetSingularName() {
+    assertEquals("service", new ServiceResourceDefinition().getSingularName());
+  }
+
+  @Test
+  public void testGetSubResourceDefinitions() {
+    ResourceDefinition resource = new ServiceResourceDefinition();
+    Set<SubResourceDefinition> subResources = resource.getSubResourceDefinitions();
+
+    assertEquals(3, subResources.size());
+    assertTrue(includesType(subResources, Resource.Type.Component));
+    assertTrue(includesType(subResources, Resource.Type.Alert));
+    assertTrue(includesType(subResources, Resource.Type.Artifact));
+  }
+
+  private boolean includesType(Set<SubResourceDefinition> resources, Resource.Type type) {
+    for (SubResourceDefinition subResource : resources) {
+      if (subResource.getType() == type) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+}

+ 229 - 0
ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ArtifactResourceProviderTest.java

@@ -0,0 +1,229 @@
+/**
+ * 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.internal;
+
+import org.apache.ambari.server.controller.AmbariManagementController;
+import org.apache.ambari.server.controller.spi.Predicate;
+import org.apache.ambari.server.controller.spi.Request;
+import org.apache.ambari.server.controller.spi.Resource;
+import org.apache.ambari.server.controller.utilities.PredicateBuilder;
+import org.apache.ambari.server.orm.dao.ArtifactDAO;
+import org.apache.ambari.server.orm.entities.ArtifactEntity;
+import org.apache.ambari.server.state.Cluster;
+import org.apache.ambari.server.state.Clusters;
+import org.easymock.Capture;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.persistence.EntityManager;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * ArtifactResourceProvider unit tests.
+ */
+public class ArtifactResourceProviderTest {
+
+  private ArtifactDAO dao = createStrictMock(ArtifactDAO.class);
+  private EntityManager em = createStrictMock(EntityManager.class);
+  private AmbariManagementController controller = createStrictMock(AmbariManagementController.class);
+  private Request request = createStrictMock(Request.class);
+  private Clusters clusters = createStrictMock(Clusters.class);
+  private Cluster cluster = createStrictMock(Cluster.class);
+  private ArtifactEntity entity = createMock(ArtifactEntity.class);
+  private ArtifactEntity entity2 = createMock(ArtifactEntity.class);
+
+  ArtifactResourceProvider resourceProvider;
+
+  @Before
+  public void setUp() throws Exception {
+    reset(dao, em, controller, request, clusters, cluster, entity, entity2);
+    resourceProvider = new ArtifactResourceProvider(controller);
+    setPrivateField(resourceProvider, "artifactDAO", dao);
+  }
+
+  @Test
+  public void testGetResources_instance() throws Exception {
+    Set<String> propertyIds = new HashSet<String>();
+    TreeMap<String, String> foreignKeys = new TreeMap<String, String>();
+    foreignKeys.put("cluster", "500");
+
+    Map<String, Object> artifact_data = Collections.<String, Object>singletonMap("foo", "bar");
+
+    Map<String, String> responseForeignKeys = new HashMap<String, String>();
+    responseForeignKeys.put("cluster", "500");
+
+    // expectations
+    expect(controller.getClusters()).andReturn(clusters).anyTimes();
+    expect(clusters.getCluster("test-cluster")).andReturn(cluster).anyTimes();
+    expect(clusters.getClusterById(500L)).andReturn(cluster).anyTimes();
+    expect(cluster.getClusterId()).andReturn(500L).anyTimes();
+    expect(cluster.getClusterName()).andReturn("test-cluster").anyTimes();
+
+    expect(request.getPropertyIds()).andReturn(propertyIds).anyTimes();
+
+    expect(dao.findByNameAndForeignKeys(eq("test-artifact"), eq(foreignKeys))).andReturn(entity).once();
+    expect(entity.getArtifactName()).andReturn("test-artifact").anyTimes();
+    expect(entity.getForeignKeys()).andReturn(responseForeignKeys).anyTimes();
+    expect(entity.getArtifactData()).andReturn(artifact_data).anyTimes();
+
+    // end of expectation setting
+    replay(dao, em, controller, request, clusters, cluster, entity, entity2);
+
+    // test
+    PredicateBuilder pb = new PredicateBuilder();
+    Predicate predicate = pb.begin().property("Artifacts/cluster_name").equals("test-cluster").and().
+        property("Artifacts/artifact_name").equals("test-artifact").end().toPredicate();
+
+    Set<Resource> response = resourceProvider.getResources(request, predicate);
+    assertEquals(1, response.size());
+    Resource resource = response.iterator().next();
+    assertEquals("test-artifact", resource.getPropertyValue("Artifacts/artifact_name"));
+    assertEquals("test-cluster", resource.getPropertyValue("Artifacts/cluster_name"));
+    assertEquals("bar", resource.getPropertyValue("artifact_data/foo"));
+  }
+
+  @Test
+  public void testGetResources_collection() throws Exception {
+    Set<String> propertyIds = new HashSet<String>();
+    TreeMap<String, String> foreignKeys = new TreeMap<String, String>();
+    foreignKeys.put("cluster", "500");
+
+    List<ArtifactEntity> entities = new ArrayList<ArtifactEntity>();
+    entities.add(entity);
+    entities.add(entity2);
+
+    Map<String, Object> artifact_data = Collections.<String, Object>singletonMap("foo", "bar");
+    Map<String, Object> artifact_data2 = Collections.<String, Object>singletonMap("foo2", "bar2");
+
+    Map<String, String> responseForeignKeys = new HashMap<String, String>();
+    responseForeignKeys.put("cluster", "500");
+
+    // expectations
+    expect(controller.getClusters()).andReturn(clusters).anyTimes();
+    expect(clusters.getCluster("test-cluster")).andReturn(cluster).anyTimes();
+    expect(clusters.getClusterById(500L)).andReturn(cluster).anyTimes();
+    expect(cluster.getClusterId()).andReturn(500L).anyTimes();
+    expect(cluster.getClusterName()).andReturn("test-cluster").anyTimes();
+
+    expect(request.getPropertyIds()).andReturn(propertyIds).anyTimes();
+
+    expect(dao.findByForeignKeys(eq(foreignKeys))).andReturn(entities).anyTimes();
+    expect(entity.getArtifactName()).andReturn("test-artifact").anyTimes();
+    expect(entity.getForeignKeys()).andReturn(responseForeignKeys).anyTimes();
+    expect(entity.getArtifactData()).andReturn(artifact_data).anyTimes();
+    expect(entity2.getArtifactName()).andReturn("test-artifact2").anyTimes();
+    expect(entity2.getForeignKeys()).andReturn(responseForeignKeys).anyTimes();
+    expect(entity2.getArtifactData()).andReturn(artifact_data2).anyTimes();
+
+    // end of expectation setting
+    replay(dao, em, controller, request, clusters, cluster, entity, entity2);
+
+    // test
+    PredicateBuilder pb = new PredicateBuilder();
+    Predicate predicate = pb.begin().property("Artifacts/cluster_name").equals("test-cluster").end().toPredicate();
+
+    Set<Resource> response = resourceProvider.getResources(request, predicate);
+    assertEquals(2, response.size());
+
+    boolean artifact1Returned = false;
+    boolean artifact2Returned = false;
+    for (Resource resource : response) {
+      if (resource.getPropertyValue("Artifacts/artifact_name").equals("test-artifact")) {
+        artifact1Returned = true;
+        assertEquals("bar", resource.getPropertyValue("artifact_data/foo"));
+        assertEquals("test-cluster", resource.getPropertyValue("Artifacts/cluster_name"));
+      } else if (resource.getPropertyValue("Artifacts/artifact_name").equals("test-artifact2")) {
+        artifact2Returned = true;
+        assertEquals("bar2", resource.getPropertyValue("artifact_data/foo2"));
+        assertEquals("test-cluster", resource.getPropertyValue("Artifacts/cluster_name"));
+      } else {
+        fail("unexpected artifact name");
+      }
+    }
+    assertTrue(artifact1Returned);
+    assertTrue(artifact2Returned);
+  }
+
+  @Test
+  public void testCreateResource() throws Exception {
+    Capture<ArtifactEntity> createEntityCapture = new Capture<ArtifactEntity>();
+
+    Map<String, Object> artifact_data = Collections.<String, Object>singletonMap("foo", "bar");
+
+    TreeMap<String, String> foreignKeys = new TreeMap<String, String>();
+    foreignKeys.put("cluster", "500");
+
+    Map<String, Object> properties = new HashMap<String, Object>();
+    properties.put("Artifacts/artifact_name", "test-artifact");
+    properties.put("Artifacts/cluster_name", "test-cluster");
+    properties.put("artifact_data/foo", "bar");
+    Set<Map<String, Object>> requestProperties = Collections.singleton(properties);
+
+    // expectations
+    expect(request.getProperties()).andReturn(requestProperties).anyTimes();
+    expect(controller.getClusters()).andReturn(clusters).anyTimes();
+    expect(clusters.getCluster("test-cluster")).andReturn(cluster).anyTimes();
+    expect(clusters.getClusterById(500L)).andReturn(cluster).anyTimes();
+    expect(cluster.getClusterId()).andReturn(500L).anyTimes();
+    expect(cluster.getClusterName()).andReturn("test-cluster").anyTimes();
+
+    // check to see if entity already exists
+    expect(dao.findByNameAndForeignKeys(eq("test-artifact"), eq(foreignKeys))).andReturn(null).once();
+    // create
+    dao.create(capture(createEntityCapture));
+
+    // end of expectation setting
+    replay(dao, em, controller, request, clusters, cluster, entity, entity2);
+
+    resourceProvider.createResources(request);
+
+    ArtifactEntity createEntity = createEntityCapture.getValue();
+    assertEquals("test-artifact", createEntity.getArtifactName());
+    assertEquals(createEntity.getArtifactData(), artifact_data);
+    assertEquals(foreignKeys, createEntity.getForeignKeys());
+  }
+
+
+  private void setPrivateField(Object o, String field, Object value) throws Exception{
+    Class<?> c = o.getClass();
+    Field f = c.getDeclaredField(field);
+    f.setAccessible(true);
+    f.set(o, value);
+  }
+}

+ 37 - 0
ambari-server/src/test/java/org/apache/ambari/server/upgrade/UpgradeCatalog200Test.java

@@ -23,6 +23,7 @@ import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
 import static org.easymock.EasyMock.capture;
 import static org.easymock.EasyMock.createMockBuilder;
 import static org.easymock.EasyMock.createNiceMock;
@@ -131,6 +132,7 @@ public class UpgradeCatalog200Test {
     Capture<DBAccessor.DBColumnInfo> valueColumnCapture = new Capture<DBAccessor.DBColumnInfo>();
     Capture<DBAccessor.DBColumnInfo> dataValueColumnCapture = new Capture<DBAccessor.DBColumnInfo>();
     Capture<List<DBAccessor.DBColumnInfo>> alertTargetStatesCapture = new Capture<List<DBAccessor.DBColumnInfo>>();
+    Capture<List<DBAccessor.DBColumnInfo>> artifactCapture = new Capture<List<DBAccessor.DBColumnInfo>>();
 
     Capture<List<DBAccessor.DBColumnInfo>> upgradeCapture = new Capture<List<DBAccessor.DBColumnInfo>>();
     Capture<List<DBAccessor.DBColumnInfo>> upgradeGroupCapture = new Capture<List<DBAccessor.DBColumnInfo>>();
@@ -199,6 +201,9 @@ public class UpgradeCatalog200Test {
     // Upgrade item
     dbAccessor.createTable(eq("upgrade_item"), capture(upgradeItemCapture), eq("upgrade_item_id"));
 
+    // artifact
+    dbAccessor.createTable(eq("artifact"), capture(artifactCapture),
+        eq("artifact_name"), eq("foreign_keys"));
 
     setViewInstancePropertyExpectations(dbAccessor, valueColumnCapture);
     setViewInstanceDataExpectations(dbAccessor, dataValueColumnCapture);
@@ -264,6 +269,11 @@ public class UpgradeCatalog200Test {
     verifyViewParameterColumns(viewparameterLabelColumnCapture, viewparameterPlaceholderColumnCapture,
         viewparameterDefaultValueColumnCapture);
 
+    // verify artifact columns
+    List<DBAccessor.DBColumnInfo> artifactColumns = artifactCapture.getValue();
+    testCreateArtifactTable(artifactColumns);
+
+
     // Verify capture group sizes
     assertEquals(7, clusterVersionCapture.getValue().size());
     assertEquals(4, hostVersionCapture.getValue().size());
@@ -529,4 +539,31 @@ public class UpgradeCatalog200Test {
     assertNull(column.getDefaultValue());
     assertTrue(column.isNullable());
   }
+
+  /**
+   * assert artifact table creation
+   *
+   * @param artifactColumns artifact table columns
+   */
+  private void testCreateArtifactTable(List<DBColumnInfo> artifactColumns) {
+    assertEquals(3, artifactColumns.size());
+    for (DBColumnInfo column : artifactColumns) {
+      if (column.getName().equals("artifact_name")) {
+        assertNull(column.getDefaultValue());
+        assertEquals(String.class, column.getType());
+        assertEquals(255, (int) column.getLength());
+        assertEquals(false, column.isNullable());
+      } else if (column.getName().equals("foreign_keys")) {
+        assertNull(column.getDefaultValue());
+        assertEquals(String.class, column.getType());
+        assertEquals(false, column.isNullable());
+      } else if (column.getName().equals("artifact_data")) {
+        assertNull(column.getDefaultValue());
+        assertEquals(char[].class, column.getType());
+        assertEquals(false, column.isNullable());
+      } else {
+        fail("unexpected column name");
+      }
+    }
+  }
 }