You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by ya...@apache.org on 2016/10/10 17:27:55 UTC

mesos git commit: Refactor parsing of resources.

Repository: mesos
Updated Branches:
  refs/heads/master ba9adeebd -> d0c0430c4


Refactor parsing of resources.

Refactored `Resources::parse()` into 2 separate static functions:
1. Resources::fromJSON() to parse JSON representation of resources.
2. Resources::fromSimpleString() to parse text representation of
   resources.

Note that these 2 new functions return a `Try<vector<Resource>>`, the
existing `Resources::parse()` calls them and converts the result to a
`Resources` object after validating each item in the vector. This
refactor is done to expose all successfully parsed resources for
further processing before semantic validation.

Review: https://reviews.apache.org/r/51999/


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

Branch: refs/heads/master
Commit: d0c0430c4b506e56d1da71d631d70b5678e634be
Parents: ba9adee
Author: Anindya Sinha <an...@apple.com>
Authored: Thu Oct 6 11:27:37 2016 -0700
Committer: Jiang Yan Xu <xu...@apple.com>
Committed: Mon Oct 10 10:27:44 2016 -0700

----------------------------------------------------------------------
 include/mesos/resources.hpp    |  69 +++++++++----
 include/mesos/v1/resources.hpp |  69 +++++++++----
 src/common/resources.cpp       | 196 ++++++++++++++++++------------------
 src/tests/resources_tests.cpp  |   4 +-
 src/v1/resources.cpp           | 195 +++++++++++++++++------------------
 5 files changed, 290 insertions(+), 243 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/include/mesos/resources.hpp
----------------------------------------------------------------------
diff --git a/include/mesos/resources.hpp b/include/mesos/resources.hpp
index ffbd466..f569c93 100644
--- a/include/mesos/resources.hpp
+++ b/include/mesos/resources.hpp
@@ -53,16 +53,6 @@
 
 namespace mesos {
 
-// Forward declarations required for making
-// `convertJSON` a friend of `Resources`.
-class Resources;
-
-namespace internal {
-  Try<Resources> convertJSON(
-      const JSON::Array& resourcesJSON,
-      const std::string& defaultRole);
-}
-
 // NOTE: Resource objects stored in the class are always valid and
 // kept combined if possible. It is the caller's responsibility to
 // validate any Resource object or repeated Resource protobufs before
@@ -164,12 +154,11 @@ public:
   /**
    * Parses Resources from an input string.
    *
-   * Parses Resources from text in the form of a JSON array. If that fails,
-   * parses text in the form "name(role):value;name:value;...". Any resource
-   * that doesn't specify a role is assigned to the provided default role. See
-   * the `Resource` protobuf definition for precise JSON formatting.
-   *
-   * Example JSON: [{"name":cpus","type":"SCALAR","scalar":{"value":8}}]
+   * Parses Resources from text in the form of a JSON array or as a simple
+   * string in the form of "name(role):value;name:value;...". i.e., this
+   * method calls `fromJSON()` or `fromSimpleString()` and validates the
+   * resulting `vector<Resource>` before converting it to a `Resources`
+   * object.
    *
    * @param text The input string.
    * @param defaultRole The default role.
@@ -181,6 +170,50 @@ public:
       const std::string& defaultRole = "*");
 
   /**
+   * Parses an input JSON array into a vector of Resource objects.
+   *
+   * Parses into a vector of Resource objects from a JSON array. Any
+   * resource that doesn't specify a role is assigned to the provided
+   * default role. See the `Resource` protobuf definition for precise
+   * JSON formatting.
+   *
+   * Example JSON: [{"name":"cpus","type":"SCALAR","scalar":{"value":8}}]
+   *
+   * NOTE: The `Resource` objects in the result vector may not be valid
+   * semantically (i.e., they may not pass `Resources::validate()`). This
+   * is to allow additional handling of the parsing results in some cases.
+   *
+   * @param resourcesJSON The input JSON array.
+   * @param defaultRole The default role.
+   * @return A `Try` which contains the parsed vector of Resource objects
+   *     if parsing was successful, or an Error otherwise.
+   */
+  static Try<std::vector<Resource>> fromJSON(
+      const JSON::Array& resourcesJSON,
+      const std::string& defaultRole = "*");
+
+  /**
+   * Parses an input text string into a vector of Resource objects.
+   *
+   * Parses into a vector of Resource objects from text. Any resource that
+   * doesn't specify a role is assigned to the provided default role.
+   *
+   * Example text: name(role):value;name:value;...
+   *
+   * NOTE: The `Resource` objects in the result vector may not be valid
+   * semantically (i.e., they may not pass `Resources::validate()`). This
+   * is to allow additional handling of the parsing results in some cases.
+   *
+   * @param text The input text string.
+   * @param defaultRole The default role.
+   * @return A `Try` which contains the parsed vector of Resource objects
+   *     if parsing was successful, or an Error otherwise.
+   */
+  static Try<std::vector<Resource>> fromSimpleString(
+      const std::string& text,
+      const std::string& defaultRole = "*");
+
+  /**
    * Validates a Resource object.
    *
    * Validates the given Resource object. Returns Error if it is not valid. A
@@ -487,10 +520,6 @@ public:
   friend std::ostream& operator<<(
       std::ostream& stream, const Resource_& resource_);
 
-  friend Try<Resources> internal::convertJSON(
-      const JSON::Array& resourcesJSON,
-      const std::string& defaultRole);
-
 private:
   // Similar to 'contains(const Resource&)' but skips the validity
   // check. This can be used to avoid the performance overhead of

http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/include/mesos/v1/resources.hpp
----------------------------------------------------------------------
diff --git a/include/mesos/v1/resources.hpp b/include/mesos/v1/resources.hpp
index 5ce038b..f60ab79 100644
--- a/include/mesos/v1/resources.hpp
+++ b/include/mesos/v1/resources.hpp
@@ -53,16 +53,6 @@
 namespace mesos {
 namespace v1 {
 
-// Forward declarations required for making
-// `convertJSON` a friend of `Resources`.
-class Resources;
-
-namespace internal {
-  Try<Resources> convertJSON(
-      const JSON::Array& resourcesJSON,
-      const std::string& defaultRole);
-}
-
 // NOTE: Resource objects stored in the class are always valid and
 // kept combined if possible. It is the caller's responsibility to
 // validate any Resource object or repeated Resource protobufs before
@@ -164,12 +154,11 @@ public:
   /**
    * Parses Resources from an input string.
    *
-   * Parses Resources from text in the form of a JSON array. If that fails,
-   * parses text in the form "name(role):value;name:value;...". Any resource
-   * that doesn't specify a role is assigned to the provided default role. See
-   * the `Resource` protobuf definition for precise JSON formatting.
-   *
-   * Example JSON: [{"name":cpus","type":"SCALAR","scalar":{"value":8}}]
+   * Parses Resources from text in the form of a JSON array or as a simple
+   * string in the form of "name(role):value;name:value;...". i.e., this
+   * method calls `fromJSON()` or `fromSimpleString()` and validates the
+   * resulting `vector<Resource>` before converting it to a `Resources`
+   * object.
    *
    * @param text The input string.
    * @param defaultRole The default role.
@@ -181,6 +170,50 @@ public:
       const std::string& defaultRole = "*");
 
   /**
+   * Parses an input JSON array into a vector of Resource objects.
+   *
+   * Parses into a vector of Resource objects from a JSON array. Any
+   * resource that doesn't specify a role is assigned to the provided
+   * default role. See the `Resource` protobuf definition for precise
+   * JSON formatting.
+   *
+   * Example JSON: [{"name":"cpus","type":"SCALAR","scalar":{"value":8}}]
+   *
+   * NOTE: The `Resource` objects in the result vector may not be valid
+   * semantically (i.e., they may not pass `Resources::validate()`). This
+   * is to allow additional handling of the parsing results in some cases.
+   *
+   * @param resourcesJSON The input JSON array.
+   * @param defaultRole The default role.
+   * @return A `Try` which contains the parsed vector of Resource objects
+   *     if parsing was successful, or an Error otherwise.
+   */
+  static Try<std::vector<Resource>> fromJSON(
+      const JSON::Array& resourcesJSON,
+      const std::string& defaultRole = "*");
+
+  /**
+   * Parses an input text string into a vector of Resource objects.
+   *
+   * Parses into a vector of Resource objects from text. Any resource that
+   * doesn't specify a role is assigned to the provided default role.
+   *
+   * Example text: name(role):value;name:value;...
+   *
+   * NOTE: The `Resource` objects in the result vector may not be valid
+   * semantically (i.e., they may not pass `Resources::validate()`). This
+   * is to allow additional handling of the parsing results in some cases.
+   *
+   * @param text The input text string.
+   * @param defaultRole The default role.
+   * @return A `Try` which contains the parsed vector of Resource objects
+   *     if parsing was successful, or an Error otherwise.
+   */
+  static Try<std::vector<Resource>> fromSimpleString(
+      const std::string& text,
+      const std::string& defaultRole = "*");
+
+  /**
    * Validates a Resource object.
    *
    * Validates the given Resource object. Returns Error if it is not valid. A
@@ -487,10 +520,6 @@ public:
   friend std::ostream& operator<<(
       std::ostream& stream, const Resource_& resource_);
 
-  friend Try<Resources> internal::convertJSON(
-      const JSON::Array& resourcesJSON,
-      const std::string& defaultRole);
-
 private:
   // Similar to 'contains(const Resource&)' but skips the validity
   // check. This can be used to avoid the performance overhead of

http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/src/common/resources.cpp
----------------------------------------------------------------------
diff --git a/src/common/resources.cpp b/src/common/resources.cpp
index 0774ff0..4bb9bef 100644
--- a/src/common/resources.cpp
+++ b/src/common/resources.cpp
@@ -430,65 +430,6 @@ static Option<Error> validateCommandLineResources(const Resources& resources)
   return None();
 }
 
-
-/**
- * Converts a JSON Array to a Resources object.
- *
- * Converts a JSON Array to a Resources object. This uses JSON to protobuf
- * conversion, so the Array should contain JSON Objects, each of which follows
- * the format of the Resource protobuf message. If no role is specified, the
- * provided default role will be assigned. `Resources::validate()` is used to
- * validate the input objects, and empty Resource objects will return an error.
- *
- * Example: [{"name":cpus","type":"SCALAR","scalar":{"value":8}}]
- *
- * @param resourcesJSON The input JSON Array.
- * @param defaultRole The default role.
- * @return A `Try` containing a Resources object if conversion was successful,
- *     or an Error otherwise.
- */
-inline Try<Resources> convertJSON(
-    const JSON::Array& resourcesJSON,
-    const string& defaultRole)
-{
-  // Convert the JSON Array into a protobuf message and use
-  // that to construct a new Resources object.
-  Try<RepeatedPtrField<Resource>> resourcesProtobuf =
-      protobuf::parse<RepeatedPtrField<Resource>>(resourcesJSON);
-
-  if (resourcesProtobuf.isError()) {
-    return Error(
-        "Some JSON resources were not formatted properly: "
-        + resourcesProtobuf.error());
-  }
-
-  // TODO(greggomann): Refactor this `RepeatedPtrField<Resource>` to `Resources`
-  // conversion if/when there is a factory function for Resources. Use of the
-  // `Resources(RepeatedPtrField<Resource>)` constructor is avoided here because
-  // it doesn't allow us to catch errors that occur during construction.
-  // Note: the related JIRA ticket is MESOS-3852.
-  Resources result;
-
-  foreach (Resource& resource, resourcesProtobuf.get()) {
-    // Set the default role if none was specified.
-    if (!resource.has_role()) {
-      resource.set_role(defaultRole);
-    }
-
-    // Validate the Resource and make sure it isn't empty.
-    Option<Error> error = Resources::validate(resource);
-    if (error.isSome()) {
-      return error.get();
-    } else if (Resources::isEmpty(resource)) {
-      return Error("Some JSON resources were empty: " + stringify(resource));
-    }
-
-    result.add(resource);
-  }
-
-  return result;
-}
-
 } // namespace internal {
 
 
@@ -584,64 +525,119 @@ Try<Resources> Resources::parse(
     const string& text,
     const string& defaultRole)
 {
+  // Try to parse as a JSON Array. Otherwise, parse as a text string.
+  Try<JSON::Array> json = JSON::parse<JSON::Array>(text);
+
+  Try<vector<Resource>> resources = json.isSome() ?
+    Resources::fromJSON(json.get(), defaultRole) :
+    Resources::fromSimpleString(text, defaultRole);
+
+  if (resources.isError()) {
+    return Error(resources.error());
+  }
+
   Resources result;
 
-  // Try to parse as a JSON Array.
-  Try<JSON::Array> resourcesJSON = JSON::parse<JSON::Array>(text);
-  if (resourcesJSON.isSome()) {
-    Try<Resources> resources =
-      internal::convertJSON(resourcesJSON.get(), defaultRole);
-    if (resources.isError()) {
-      return resources;
+  // Validate individual Resource objects.
+  foreach(const Resource& resource, resources.get()) {
+    // If invalid, propgate error instead of skipping the resource.
+    Option<Error> error = Resources::validate(resource);
+    if (error.isSome()) {
+      return error.get();
     }
 
-    result = resources.get();
-  } else {
-    foreach (const string& token, strings::tokenize(text, ";")) {
-      vector<string> pair = strings::tokenize(token, ":");
-      if (pair.size() != 2) {
-        return Error(
-            "Bad value for resources, missing or extra ':' in " + token);
-      }
+    result.add(resource);
+  }
 
-      string name;
-      string role;
-      size_t openParen = pair[0].find("(");
-      if (openParen == string::npos) {
-        name = strings::trim(pair[0]);
-        role = defaultRole;
-      } else {
-        size_t closeParen = pair[0].find(")");
-        if (closeParen == string::npos || closeParen < openParen) {
-          return Error(
-              "Bad value for resources, mismatched parentheses in " + token);
-        }
+  // TODO(jmlvanre): Move this up into `Containerizer::resources`.
+  Option<Error> error = internal::validateCommandLineResources(result);
+  if (error.isSome()) {
+    return error.get();
+  }
 
-        name = strings::trim(pair[0].substr(0, openParen));
+  return result;
+}
 
-        role = strings::trim(pair[0].substr(
-            openParen + 1,
-            closeParen - openParen - 1));
-      }
 
-      Try<Resource> resource = Resources::parse(name, pair[1], role);
-      if (resource.isError()) {
-        return Error(resource.error());
-      }
+Try<vector<Resource>> Resources::fromJSON(
+    const JSON::Array& resourcesJSON,
+    const string& defaultRole)
+{
+  // Convert the JSON Array into a protobuf message and use
+  // that to construct a vector of Resource object.
+  Try<RepeatedPtrField<Resource>> resourcesProtobuf =
+    protobuf::parse<RepeatedPtrField<Resource>>(resourcesJSON);
 
-      result += resource.get();
-    }
+  if (resourcesProtobuf.isError()) {
+    return Error(
+        "Some JSON resources were not formatted properly: " +
+        resourcesProtobuf.error());
   }
 
-  Option<Error> error = internal::validateCommandLineResources(result);
-  if (error.isSome()) {
-    return error.get();
+  vector<Resource> result;
+
+  foreach (Resource& resource, resourcesProtobuf.get()) {
+    // Set the default role if none was specified.
+    if (!resource.has_role()) {
+      resource.set_role(defaultRole);
+    }
+
+    // We add the Resource object even if it is empty or invalid.
+    result.push_back(resource);
   }
 
   return result;
 }
 
 
+Try<vector<Resource>> Resources::fromSimpleString(
+    const string& text,
+    const string& defaultRole)
+{
+  vector<Resource> resources;
+
+  foreach (const string& token, strings::tokenize(text, ";")) {
+    // TODO(anindya_sinha): Allow text based representation of resources
+    // to specify PATH or MOUNT type disks along with its root.
+    vector<string> pair = strings::tokenize(token, ":");
+    if (pair.size() != 2) {
+      return Error(
+          "Bad value for resources, missing or extra ':' in " + token);
+    }
+
+    string name;
+    string role;
+    size_t openParen = pair[0].find("(");
+    if (openParen == string::npos) {
+      name = strings::trim(pair[0]);
+      role = defaultRole;
+    } else {
+      size_t closeParen = pair[0].find(")");
+      if (closeParen == string::npos || closeParen < openParen) {
+        return Error(
+            "Bad value for resources, mismatched parentheses in " + token);
+      }
+
+      name = strings::trim(pair[0].substr(0, openParen));
+
+      role = strings::trim(pair[0].substr(
+          openParen + 1,
+          closeParen - openParen - 1));
+    }
+
+    Try<Resource> resource = Resources::parse(name, pair[1], role);
+    if (resource.isError()) {
+      return Error(resource.error());
+    }
+
+    // We add the Resource object even if it is empty or invalid.
+    resources.push_back(resource.get());
+  }
+
+  return resources;
+}
+
+
 Option<Error> Resources::validate(const Resource& resource)
 {
   if (resource.name().empty()) {

http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/src/tests/resources_tests.cpp
----------------------------------------------------------------------
diff --git a/src/tests/resources_tests.cpp b/src/tests/resources_tests.cpp
index 1823f25..6a12783 100644
--- a/src/tests/resources_tests.cpp
+++ b/src/tests/resources_tests.cpp
@@ -627,14 +627,14 @@ TEST(ResourcesTest, ParsingFromJSONError)
 
   EXPECT_ERROR(Resources::parse(jsonString));
 
-  // Empty Resources.
+  // Negative Resources.
   jsonString =
     "["
     "  {"
     "    \"name\": \"panda_power\","
     "    \"type\": \"SCALAR\","
     "    \"scalar\": {"
-    "      \"value\": 0"
+    "      \"value\": -1"
     "    }"
     "  },"
     "  {"

http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/src/v1/resources.cpp
----------------------------------------------------------------------
diff --git a/src/v1/resources.cpp b/src/v1/resources.cpp
index 62a644e..46cc00f 100644
--- a/src/v1/resources.cpp
+++ b/src/v1/resources.cpp
@@ -432,65 +432,6 @@ static Option<Error> validateCommandLineResources(const Resources& resources)
   return None();
 }
 
-
-/**
- * Converts a JSON Array to a Resources object.
- *
- * Converts a JSON Array to a Resources object. This uses JSON to protobuf
- * conversion, so the Array should contain JSON Objects, each of which follows
- * the format of the Resource protobuf message. If no role is specified, the
- * provided default role will be assigned. `Resources::validate()` is used to
- * validate the input objects, and empty Resource objects will return an error.
- *
- * Example: [{"name":cpus","type":"SCALAR","scalar":{"value":8}}]
- *
- * @param resourcesJSON The input JSON Array.
- * @param defaultRole The default role.
- * @return A `Try` containing a Resources object if conversion was successful,
- *     or an Error otherwise.
- */
-inline Try<Resources> convertJSON(
-    const JSON::Array& resourcesJSON,
-    const string& defaultRole)
-{
-  // Convert the JSON Array into a protobuf message and use
-  // that to construct a new Resources object.
-  Try<RepeatedPtrField<Resource>> resourcesProtobuf =
-      protobuf::parse<RepeatedPtrField<Resource>>(resourcesJSON);
-
-  if (resourcesProtobuf.isError()) {
-    return Error(
-        "Some JSON resources were not formatted properly: "
-        + resourcesProtobuf.error());
-  }
-
-  // TODO(greggomann): Refactor this `RepeatedPtrField<Resource>` to `Resources`
-  // conversion if/when there is a factory function for Resources. Use of the
-  // `Resources(RepeatedPtrField<Resource>)` constructor is avoided here because
-  // it doesn't allow us to catch errors that occur during construction.
-  // Note: the related JIRA ticket is MESOS-3852.
-  Resources result;
-
-  foreach (Resource& resource, resourcesProtobuf.get()) {
-    // Set the default role if none was specified.
-    if (!resource.has_role()) {
-      resource.set_role(defaultRole);
-    }
-
-    // Validate the Resource and make sure it isn't empty.
-    Option<Error> error = Resources::validate(resource);
-    if (error.isSome()) {
-      return error.get();
-    } else if (Resources::isEmpty(resource)) {
-      return Error("Some JSON resources were empty: " + stringify(resource));
-    }
-
-    result.add(resource);
-  }
-
-  return result;
-}
-
 } // namespace internal {
 
 
@@ -586,53 +527,28 @@ Try<Resources> Resources::parse(
     const string& text,
     const string& defaultRole)
 {
-  Resources result;
-
-  // Try to parse as a JSON Array.
-  Try<JSON::Array> resourcesJSON = JSON::parse<JSON::Array>(text);
-  if (resourcesJSON.isSome()) {
-    Try<Resources> resources =
-      internal::convertJSON(resourcesJSON.get(), defaultRole);
-    if (resources.isError()) {
-      return resources;
-    }
-
-    result = resources.get();
-  } else {
-    foreach (const string& token, strings::tokenize(text, ";")) {
-      vector<string> pair = strings::tokenize(token, ":");
-      if (pair.size() != 2) {
-        return Error(
-            "Bad value for resources, missing or extra ':' in " + token);
-      }
-
-      string name;
-      string role;
-      size_t openParen = pair[0].find("(");
-      if (openParen == string::npos) {
-        name = strings::trim(pair[0]);
-        role = defaultRole;
-      } else {
-        size_t closeParen = pair[0].find(")");
-        if (closeParen == string::npos || closeParen < openParen) {
-          return Error(
-              "Bad value for resources, mismatched parentheses in " + token);
-        }
+  // Try to parse as a JSON Array. Otherwise, parse as a text string.
+  Try<JSON::Array> json = JSON::parse<JSON::Array>(text);
 
-        name = strings::trim(pair[0].substr(0, openParen));
+  Try<vector<Resource>> resources = json.isSome() ?
+    Resources::fromJSON(json.get(), defaultRole) :
+    Resources::fromSimpleString(text, defaultRole);
 
-        role = strings::trim(pair[0].substr(
-            openParen + 1,
-            closeParen - openParen - 1));
-      }
+  if (resources.isError()) {
+    return Error(resources.error());
+  }
 
-      Try<Resource> resource = Resources::parse(name, pair[1], role);
-      if (resource.isError()) {
-        return Error(resource.error());
-      }
+  Resources result;
 
-      result += resource.get();
+  // Validate individual Resource objects.
+  foreach(const Resource& resource, resources.get()) {
+    // If invalid, propgate error instead of skipping the resource.
+    Option<Error> error = Resources::validate(resource);
+    if (error.isSome()) {
+      return error.get();
     }
+
+    result.add(resource);
   }
 
   // TODO(jmlvanre): Move this up into `Containerizer::resources`.
@@ -645,6 +561,83 @@ Try<Resources> Resources::parse(
 }
 
 
+Try<vector<Resource>> Resources::fromJSON(
+    const JSON::Array& resourcesJSON,
+    const string& defaultRole)
+{
+  // Convert the JSON Array into a protobuf message and use
+  // that to construct a vector of Resource object.
+  Try<RepeatedPtrField<Resource>> resourcesProtobuf =
+    protobuf::parse<RepeatedPtrField<Resource>>(resourcesJSON);
+
+  if (resourcesProtobuf.isError()) {
+    return Error(
+        "Some JSON resources were not formatted properly: " +
+        resourcesProtobuf.error());
+  }
+
+  vector<Resource> result;
+
+  foreach (Resource& resource, resourcesProtobuf.get()) {
+    // Set the default role if none was specified.
+    if (!resource.has_role()) {
+      resource.set_role(defaultRole);
+    }
+
+    // We add the Resource object even if it is empty or invalid.
+    result.push_back(resource);
+  }
+
+  return result;
+}
+
+
+Try<vector<Resource>> Resources::fromSimpleString(
+    const string& text,
+    const string& defaultRole)
+{
+  vector<Resource> resources;
+
+  foreach (const string& token, strings::tokenize(text, ";")) {
+    vector<string> pair = strings::tokenize(token, ":");
+    if (pair.size() != 2) {
+      return Error(
+          "Bad value for resources, missing or extra ':' in " + token);
+    }
+
+    string name;
+    string role;
+    size_t openParen = pair[0].find("(");
+    if (openParen == string::npos) {
+      name = strings::trim(pair[0]);
+      role = defaultRole;
+    } else {
+      size_t closeParen = pair[0].find(")");
+      if (closeParen == string::npos || closeParen < openParen) {
+        return Error(
+            "Bad value for resources, mismatched parentheses in " + token);
+      }
+
+      name = strings::trim(pair[0].substr(0, openParen));
+
+      role = strings::trim(pair[0].substr(
+          openParen + 1,
+          closeParen - openParen - 1));
+    }
+
+    Try<Resource> resource = Resources::parse(name, pair[1], role);
+    if (resource.isError()) {
+      return Error(resource.error());
+    }
+
+    // We add the Resource object even if it is empty or invalid.
+    resources.push_back(resource.get());
+  }
+
+  return resources;
+}
+
+
 Option<Error> Resources::validate(const Resource& resource)
 {
   if (resource.name().empty()) {