You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by js...@apache.org on 2015/01/15 19:27:53 UTC

ambari git commit: AMBARI-9142. Create new API endpoints for cluster and service kerberos descriptors

Repository: ambari
Updated Branches:
  refs/heads/trunk e2b3f4deb -> d902509f7


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


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/d902509f
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/d902509f
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/d902509f

Branch: refs/heads/trunk
Commit: d902509f755701285bda9bc23b51bd2e58064133
Parents: e2b3f4d
Author: John Speidel <js...@hortonworks.com>
Authored: Wed Jan 14 22:34:49 2015 -0500
Committer: John Speidel <js...@hortonworks.com>
Committed: Thu Jan 15 13:27:42 2015 -0500

----------------------------------------------------------------------
 .../resources/ClusterResourceDefinition.java    |   2 +
 .../resources/ResourceInstanceFactoryImpl.java  |   4 +
 .../resources/ServiceResourceDefinition.java    |   3 +
 .../server/api/services/ClusterService.java     |  95 ++-
 .../server/api/services/ServiceService.java     |  92 ++-
 .../internal/ArtifactResourceProvider.java      | 695 +++++++++++++++++++
 .../internal/DefaultProviderModule.java         |   2 +
 .../ambari/server/controller/spi/Resource.java  |   4 +-
 .../ambari/server/orm/dao/ArtifactDAO.java      | 109 +++
 .../server/orm/entities/ArtifactEntity.java     | 137 ++++
 .../server/orm/entities/ArtifactEntityPK.java   |  98 +++
 .../server/upgrade/UpgradeCatalog200.java       |  10 +
 .../main/resources/Ambari-DDL-MySQL-CREATE.sql  |   6 +
 .../main/resources/Ambari-DDL-Oracle-CREATE.sql |   6 +
 .../Ambari-DDL-Postgres-EMBEDDED-CREATE.sql     |   7 +
 .../src/main/resources/META-INF/persistence.xml |   4 +-
 .../api/query/render/DefaultRendererTest.java   |   3 +-
 .../api/query/render/MinimalRendererTest.java   |   3 +-
 .../ClusterResourceDefinitionTest.java          |   3 +-
 .../ResourceInstanceFactoryImplTest.java        |  10 +
 .../ServiceResourceDefinitionTest.java          |  64 ++
 .../internal/ArtifactResourceProviderTest.java  | 229 ++++++
 .../server/upgrade/UpgradeCatalog200Test.java   |  37 +
 23 files changed, 1616 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ClusterResourceDefinition.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ClusterResourceDefinition.java b/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ClusterResourceDefinition.java
index 422cf1c..9b744d0 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ClusterResourceDefinition.java
+++ b/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;
   }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImpl.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImpl.java b/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImpl.java
index a75729d..fc28c13 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImpl.java
+++ b/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);
     }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ServiceResourceDefinition.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ServiceResourceDefinition.java b/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ServiceResourceDefinition.java
index be8e0b4..9abf3a7 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/api/resources/ServiceResourceDefinition.java
+++ b/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;
   }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java b/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java
index ead49ca..5f44b2f 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java
+++ b/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;
@@ -170,6 +172,81 @@ public class ClusterService extends BaseService {
   }
 
   /**
+   * 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
    *
    * @param request      the request
@@ -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
    */
@@ -476,6 +553,22 @@ public class ClusterService extends BaseService {
   }
 
   /**
+   * 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
    * given cluster name.

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java b/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
index a51b50b..8a2642b 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
@@ -200,6 +200,79 @@ public class ServiceService extends BaseService {
   }
 
   /**
+   * 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
    *
    * @param request
@@ -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);
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ArtifactResourceProvider.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ArtifactResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ArtifactResourceProvider.java
new file mode 100644
index 0000000..b3eb159
--- /dev/null
+++ b/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;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/DefaultProviderModule.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/DefaultProviderModule.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/DefaultProviderModule.java
index be2a9ad..b0e23e9 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/DefaultProviderModule.java
+++ b/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,

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java
index 740f37e..aa5cf64 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java
+++ b/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.

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ArtifactDAO.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ArtifactDAO.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ArtifactDAO.java
new file mode 100644
index 0000000..4b7cf25
--- /dev/null
+++ b/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);
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntity.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntity.java
new file mode 100644
index 0000000..849a938
--- /dev/null
+++ b/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);
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntityPK.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntityPK.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ArtifactEntityPK.java
new file mode 100644
index 0000000..67b52c9
--- /dev/null
+++ b/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();
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog200.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog200.java b/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog200.java
index f7bd080..3a043b9 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog200.java
+++ b/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 ----------------------------------------------------
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
index 6f3f094..972053c 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
+++ b/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);

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
index 11d5de3..0117fed 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
+++ b/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);

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
index 2ae175b..7c2d7ae 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
+++ b/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),

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/main/resources/META-INF/persistence.xml
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/META-INF/persistence.xml b/ambari-server/src/main/resources/META-INF/persistence.xml
index dc49b7c..07bd67d 100644
--- a/ambari-server/src/main/resources/META-INF/persistence.xml
+++ b/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" />

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/test/java/org/apache/ambari/server/api/query/render/DefaultRendererTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/query/render/DefaultRendererTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/query/render/DefaultRendererTest.java
index 3e0617e..4981a67 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/api/query/render/DefaultRendererTest.java
+++ b/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());

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/test/java/org/apache/ambari/server/api/query/render/MinimalRendererTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/query/render/MinimalRendererTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/query/render/MinimalRendererTest.java
index 9b03c51..37bf33c 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/api/query/render/MinimalRendererTest.java
+++ b/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());

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ClusterResourceDefinitionTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ClusterResourceDefinitionTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ClusterResourceDefinitionTest.java
index 6d897b2..cb68dfa 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ClusterResourceDefinitionTest.java
+++ b/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

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImplTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImplTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImplTest.java
index b5cfcc3..0daffce 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImplTest.java
+++ b/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());
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ServiceResourceDefinitionTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ServiceResourceDefinitionTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/resources/ServiceResourceDefinitionTest.java
new file mode 100644
index 0000000..013c9b9
--- /dev/null
+++ b/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;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ArtifactResourceProviderTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ArtifactResourceProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ArtifactResourceProviderTest.java
new file mode 100644
index 0000000..4ed1a51
--- /dev/null
+++ b/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);
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d902509f/ambari-server/src/test/java/org/apache/ambari/server/upgrade/UpgradeCatalog200Test.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/upgrade/UpgradeCatalog200Test.java b/ambari-server/src/test/java/org/apache/ambari/server/upgrade/UpgradeCatalog200Test.java
index 6bb8a95..9cf016e 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/upgrade/UpgradeCatalog200Test.java
+++ b/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");
+      }
+    }
+  }
 }