You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by na...@apache.org on 2015/10/30 11:30:56 UTC

[3/3] jclouds git commit: JCLOUDS-717: Join Enterprise and OpenSource Chef

JCLOUDS-717: Join Enterprise and OpenSource Chef


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

Branch: refs/heads/master
Commit: 9df30c5a091eb6e3487ca8578674b872ab77963b
Parents: c9306c1
Author: Ignasi Barrera <na...@apache.org>
Authored: Fri Oct 30 00:42:03 2015 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Fri Oct 30 01:02:53 2015 +0100

----------------------------------------------------------------------
 apis/chef/src/main/clojure/org/jclouds/chef.clj |   4 +-
 .../src/main/java/org/jclouds/chef/ChefApi.java |  11 +-
 .../java/org/jclouds/chef/ChefApiMetadata.java  |   8 +-
 .../BindGroupToUpdateRequestJsonPayload.java    |  79 +++
 .../org/jclouds/chef/binders/GroupName.java     |  38 ++
 .../chef/config/BaseChefHttpApiModule.java      | 204 -------
 .../jclouds/chef/config/ChefHttpApiModule.java  | 180 +++++-
 .../java/org/jclouds/chef/domain/Group.java     | 211 ++++++++
 .../main/java/org/jclouds/chef/domain/User.java | 220 ++++++++
 .../jclouds/chef/features/OrganizationApi.java  | 130 +++++
 .../jclouds/chef/filters/SignedHeaderAuth.java  |   2 +-
 .../chef/options/CreateClientOptions.java       |   2 +-
 .../org/jclouds/chef/BaseChefApiExpectTest.java |   1 +
 .../org/jclouds/chef/ChefApiDelegationTest.java |  55 ++
 .../org/jclouds/chef/ChefApiExpectTest.java     |   3 -
 .../java/org/jclouds/chef/ChefApiLiveTest.java  | 522 +++++++++++++++++-
 ...BindGroupToUpdateRequestJsonPayloadTest.java |  82 +++
 .../org/jclouds/chef/binders/GroupNameTest.java |  40 ++
 .../features/OrganizationApiExpectTest.java     | 207 +++++++
 .../chef/features/OrganizationApiLiveTest.java  | 120 ++++
 .../chef/functions/ParseClientFromJsonTest.java |   2 -
 .../chef/internal/BaseChefApiLiveTest.java      | 542 -------------------
 .../jclouds/chef/internal/BaseChefLiveTest.java |  16 +-
 .../chef/internal/BaseStubbedOhaiLiveTest.java  |  60 --
 ...CleanupStaleNodesAndClientsImplLiveTest.java |   3 +-
 ...PopulateAutomaticAttributesImplLiveTest.java |   3 +-
 ...eAndPopulateAutomaticAttributesImplTest.java |   6 +-
 ...DeleteAllApisAndNodesInListImplLiveTest.java |   3 +-
 ...okbookVersionsInEnvironmentImplLiveTest.java |   7 +-
 .../internal/ListNodesImplLiveTest.java         |  13 +-
 .../ListNodesInEnvironmentImplLiveTest.java     |  13 +-
 ...teAutomaticAttributesOnNodeImplLiveTest.java |   3 +-
 apis/chef/src/test/resources/group-update.json  |   1 +
 apis/chef/src/test/resources/group.json         |  14 +
 apis/chef/src/test/resources/groups.json        |   7 +
 apis/chef/src/test/resources/user.json          |   9 +
 providers/enterprisechef/pom.xml                | 114 ----
 .../enterprisechef/EnterpriseChefApi.java       | 128 -----
 .../EnterpriseChefApiMetadata.java              |  81 ---
 .../EnterpriseChefProviderMetadata.java         |  79 ---
 .../BindGroupToUpdateRequestJsonPayload.java    |  79 ---
 .../enterprisechef/binders/GroupName.java       |  38 --
 .../config/EnterpriseChefHttpApiModule.java     |  36 --
 .../jclouds/enterprisechef/domain/Group.java    | 211 --------
 .../org/jclouds/enterprisechef/domain/User.java | 220 --------
 .../EnterpriseChefApiExpectTest.java            | 209 -------
 .../EnterpriseChefApiLiveTest.java              | 122 -----
 .../EnterpriseChefProviderMetadataTest.java     |  31 --
 ...BindGroupToUpdateRequestJsonPayloadTest.java |  82 ---
 .../enterprisechef/binders/GroupNameTest.java   |  40 --
 .../src/test/resources/group-update.json        |   1 -
 .../src/test/resources/group.json               |  14 -
 .../src/test/resources/groups.json              |   7 -
 .../enterprisechef/src/test/resources/user.json |   9 -
 providers/pom.xml                               |   1 -
 55 files changed, 1952 insertions(+), 2371 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/clojure/org/jclouds/chef.clj
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/clojure/org/jclouds/chef.clj b/apis/chef/src/main/clojure/org/jclouds/chef.clj
index ad0f200..964cd00 100644
--- a/apis/chef/src/main/clojure/org/jclouds/chef.clj
+++ b/apis/chef/src/main/clojure/org/jclouds/chef.clj
@@ -19,7 +19,7 @@
   #^{:author "Adrian Cole"
      :doc "A clojure binding to the jclouds chef interface.
 
-Here's a quick example of how to manipulate a databag on the Opscode Platform, 
+Here's a quick example of how to manipulate a databag on the Chef Platform, 
 which is basically Chef Server as a Service.
 
 (use 'org.jclouds.chef)
@@ -28,7 +28,7 @@ which is basically Chef Server as a Service.
 ;; load the rsa key from ~/.chef/CLIENT_NAME.pem
 (def credential (load-pem client))
 
-;; create a connection to the opscode platform
+;; create a connection to the chef platform
 (def chef (chef-service \"chef\" client credential :chef.endpoint \"https://api.opscode.com/organizations/YOUR_ORG\"))
 
 (with-chef-service [chef]

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/ChefApi.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/ChefApi.java b/apis/chef/src/main/java/org/jclouds/chef/ChefApi.java
index 1fe0f1b..836e1e9 100644
--- a/apis/chef/src/main/java/org/jclouds/chef/ChefApi.java
+++ b/apis/chef/src/main/java/org/jclouds/chef/ChefApi.java
@@ -55,10 +55,11 @@ import org.jclouds.chef.domain.Role;
 import org.jclouds.chef.domain.Sandbox;
 import org.jclouds.chef.domain.SearchResult;
 import org.jclouds.chef.domain.UploadSandbox;
+import org.jclouds.chef.features.OrganizationApi;
 import org.jclouds.chef.filters.SignedHeaderAuth;
+import org.jclouds.chef.functions.ParseCookbookDefinitionFromJson;
 import org.jclouds.chef.functions.ParseCookbookDefinitionListFromJson;
 import org.jclouds.chef.functions.ParseCookbookNamesFromJson;
-import org.jclouds.chef.functions.ParseCookbookDefinitionFromJson;
 import org.jclouds.chef.functions.ParseCookbookVersionsFromJson;
 import org.jclouds.chef.functions.ParseKeySetFromJson;
 import org.jclouds.chef.functions.ParseSearchClientsFromJson;
@@ -71,6 +72,7 @@ import org.jclouds.chef.options.CreateClientOptions;
 import org.jclouds.chef.options.SearchOptions;
 import org.jclouds.io.Payload;
 import org.jclouds.rest.annotations.BinderParam;
+import org.jclouds.rest.annotations.Delegate;
 import org.jclouds.rest.annotations.EndpointParam;
 import org.jclouds.rest.annotations.Fallback;
 import org.jclouds.rest.annotations.Headers;
@@ -85,6 +87,7 @@ import org.jclouds.rest.annotations.SkipEncoding;
 import org.jclouds.rest.annotations.WrapWith;
 import org.jclouds.rest.binders.BindToJsonPayload;
 
+import com.google.common.base.Optional;
 import com.google.inject.Provides;
 
 /**
@@ -100,6 +103,12 @@ public interface ChefApi extends Closeable {
     */
    @Provides
    ChefService chefService();
+   
+   /**
+    * Provides access to the organization, user and group management endpoints.
+    */
+   @Delegate
+   Optional<OrganizationApi> organizationApi();
 
    // Clients
 

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/ChefApiMetadata.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/ChefApiMetadata.java b/apis/chef/src/main/java/org/jclouds/chef/ChefApiMetadata.java
index 41a46df..bbbdfe6 100644
--- a/apis/chef/src/main/java/org/jclouds/chef/ChefApiMetadata.java
+++ b/apis/chef/src/main/java/org/jclouds/chef/ChefApiMetadata.java
@@ -40,7 +40,7 @@ import com.google.common.collect.ImmutableSet;
 import com.google.inject.Module;
 
 /**
- * Implementation of {@link ApiMetadata} for OpsCode's Chef api.
+ * Implementation of {@link ApiMetadata} for Chef.
  */
 @AutoService(ApiMetadata.class)
 public class ChefApiMetadata extends BaseHttpApiMetadata<ChefApi> {
@@ -48,7 +48,7 @@ public class ChefApiMetadata extends BaseHttpApiMetadata<ChefApi> {
    /**
     * The default Chef Server API version to use.
     */
-   public static final String DEFAULT_API_VERSION = "0.10.8";
+   public static final String DEFAULT_API_VERSION = "12.0.2";
 
    @Override
    public Builder toBuilder() {
@@ -86,11 +86,11 @@ public class ChefApiMetadata extends BaseHttpApiMetadata<ChefApi> {
 
       protected Builder() {
          id("chef")
-               .name("OpsCode Chef Api")
+               .name("Chef Api")
                .identityName("User")
                .credentialName("Certificate")
                .version(DEFAULT_API_VERSION)
-               .documentation(URI.create("http://wiki.opscode.com/display/chef/Server+API"))
+               .documentation(URI.create("https://docs.chef.io/api_chef_server.html"))
                .defaultEndpoint("http://localhost:4000")
                .defaultProperties(ChefApiMetadata.defaultProperties())
                .defaultModules(

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/binders/BindGroupToUpdateRequestJsonPayload.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/binders/BindGroupToUpdateRequestJsonPayload.java b/apis/chef/src/main/java/org/jclouds/chef/binders/BindGroupToUpdateRequestJsonPayload.java
new file mode 100644
index 0000000..469423d
--- /dev/null
+++ b/apis/chef/src/main/java/org/jclouds/chef/binders/BindGroupToUpdateRequestJsonPayload.java
@@ -0,0 +1,79 @@
+/*
+ * 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.jclouds.chef.binders;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.chef.domain.Group;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.json.Json;
+import org.jclouds.rest.binders.BindToJsonPayload;
+
+/**
+ * Binds a group to the payload expected for the Put method in the Enterprise Chef
+ * Api.
+ */
+@Singleton
+public class BindGroupToUpdateRequestJsonPayload extends BindToJsonPayload {
+
+   @Inject
+   public BindGroupToUpdateRequestJsonPayload(Json jsonBinder) {
+      super(jsonBinder);
+   }
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Object payload) {
+      checkArgument(checkNotNull(payload, "payload") instanceof Group, "this binder is only valid for Group objects");
+      GroupUpdateRequest updateGroup = new GroupUpdateRequest((Group) payload);
+      return super.bindToRequest(request, updateGroup);
+   }
+
+   @SuppressWarnings("unused")
+   private static class GroupUpdateRequest {
+      private String name;
+      private String groupname;
+      private String orgname;
+      private ActorConfiguration actors;
+
+      public GroupUpdateRequest(Group group) {
+         this.name = group.getName();
+         this.groupname = group.getGroupname();
+         this.orgname = group.getOrgname();
+         this.actors = new ActorConfiguration(group);
+      }
+   }
+
+   @SuppressWarnings("unused")
+   private static class ActorConfiguration {
+      private Set<String> clients;
+      private Set<String> groups;
+      private Set<String> users;
+
+      public ActorConfiguration(Group group) {
+         this.clients = group.getClients();
+         this.groups = group.getGroups();
+         this.users = group.getUsers();
+      }
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/binders/GroupName.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/binders/GroupName.java b/apis/chef/src/main/java/org/jclouds/chef/binders/GroupName.java
new file mode 100644
index 0000000..18f1b5b
--- /dev/null
+++ b/apis/chef/src/main/java/org/jclouds/chef/binders/GroupName.java
@@ -0,0 +1,38 @@
+/*
+ * 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.jclouds.chef.binders;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.inject.Singleton;
+
+import org.jclouds.chef.domain.Group;
+
+import com.google.common.base.Function;
+
+/**
+ * Gets the name of a group.
+ */
+@Singleton
+public class GroupName implements Function<Object, String> {
+
+   @Override
+   public String apply(Object from) {
+      return ((Group) checkNotNull(from, "from")).getGroupname();
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/config/BaseChefHttpApiModule.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/config/BaseChefHttpApiModule.java b/apis/chef/src/main/java/org/jclouds/chef/config/BaseChefHttpApiModule.java
deleted file mode 100644
index c60be06..0000000
--- a/apis/chef/src/main/java/org/jclouds/chef/config/BaseChefHttpApiModule.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * 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.jclouds.chef.config;
-
-import static com.google.common.base.Suppliers.compose;
-import static com.google.common.base.Suppliers.memoizeWithExpiration;
-import static com.google.common.base.Throwables.propagate;
-import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
-import static org.jclouds.chef.config.ChefProperties.CHEF_VALIDATOR_CREDENTIAL;
-import static org.jclouds.chef.config.ChefProperties.CHEF_VALIDATOR_NAME;
-import static org.jclouds.crypto.Pems.privateKeySpec;
-
-import java.io.IOException;
-import java.security.PrivateKey;
-import java.security.spec.InvalidKeySpecException;
-import java.util.concurrent.TimeUnit;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import org.jclouds.chef.domain.BootstrapConfig;
-import org.jclouds.chef.domain.Client;
-import org.jclouds.chef.functions.BootstrapConfigForGroup;
-import org.jclouds.chef.functions.ClientForGroup;
-import org.jclouds.chef.handlers.ChefApiErrorRetryHandler;
-import org.jclouds.chef.handlers.ChefErrorHandler;
-import org.jclouds.crypto.Crypto;
-import org.jclouds.crypto.Pems;
-import org.jclouds.date.DateService;
-import org.jclouds.date.TimeStamp;
-import org.jclouds.domain.Credentials;
-import org.jclouds.http.HttpErrorHandler;
-import org.jclouds.http.HttpRetryHandler;
-import org.jclouds.http.annotation.ClientError;
-import org.jclouds.http.annotation.Redirection;
-import org.jclouds.http.annotation.ServerError;
-import org.jclouds.rest.ConfiguresHttpApi;
-import org.jclouds.rest.config.HttpApiModule;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Charsets;
-import com.google.common.base.Function;
-import com.google.common.base.Optional;
-import com.google.common.base.Supplier;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.io.ByteSource;
-import com.google.inject.ConfigurationException;
-import com.google.inject.Injector;
-import com.google.inject.Key;
-import com.google.inject.Provides;
-import com.google.inject.name.Names;
-
-/**
- * Configures the Chef connection.
- */
-@ConfiguresHttpApi
-public abstract class BaseChefHttpApiModule<S> extends HttpApiModule<S> {
-
-   @Provides
-   @TimeStamp
-   protected final String guiceProvideTimeStamp(@TimeStamp Supplier<String> cache) {
-      return provideTimeStamp(cache);
-   }
-
-   protected String provideTimeStamp(@TimeStamp Supplier<String> cache) {
-      return cache.get();
-   }
-
-   /**
-    * borrowing concurrency code to ensure that caching takes place properly
-    */
-   @Provides
-   @TimeStamp
-   final Supplier<String> provideTimeStampCache(@Named(PROPERTY_SESSION_INTERVAL) long seconds, final DateService dateService) {
-      return memoizeWithExpiration(new Supplier<String>() {
-         @Override
-         public String get() {
-            return dateService.iso8601SecondsDateFormat();
-         }
-      }, seconds, TimeUnit.SECONDS);
-   }
-
-   // TODO: potentially change this
-   @Provides
-   @Singleton
-   public final Supplier<PrivateKey> supplyKey(final LoadingCache<Credentials, PrivateKey> keyCache,
-         @org.jclouds.location.Provider final Supplier<Credentials> creds) {
-      return compose(new Function<Credentials, PrivateKey>() {
-         @Override
-         public PrivateKey apply(Credentials in) {
-            return keyCache.getUnchecked(in);
-         }
-      }, creds);
-   }
-
-   @Provides
-   @Singleton
-   final LoadingCache<Credentials, PrivateKey> privateKeyCache(PrivateKeyForCredentials loader) {
-      // throw out the private key related to old credentials
-      return CacheBuilder.newBuilder().maximumSize(2).build(loader);
-   }
-
-   /**
-    * it is relatively expensive to extract a private key from a PEM. cache the
-    * relationship between current credentials so that the private key is only
-    * recalculated once.
-    */
-   @VisibleForTesting
-   @Singleton
-   private static class PrivateKeyForCredentials extends CacheLoader<Credentials, PrivateKey> {
-      private final Crypto crypto;
-
-      @Inject
-      private PrivateKeyForCredentials(Crypto crypto) {
-         this.crypto = crypto;
-      }
-
-      @Override
-      public PrivateKey load(Credentials in) {
-         try {
-            return crypto.rsaKeyFactory().generatePrivate(
-                  privateKeySpec(ByteSource.wrap(in.credential.getBytes(Charsets.UTF_8))));
-         } catch (InvalidKeySpecException e) {
-            throw propagate(e);
-         } catch (IOException e) {
-            throw propagate(e);
-         }
-      }
-   }
-
-   @Provides
-   @Singleton
-   @Validator
-   public final Optional<String> provideValidatorName(Injector injector) {
-      // Named properties can not be injected as optional here, so let's use the
-      // injector to bypass it
-      Key<String> key = Key.get(String.class, Names.named(CHEF_VALIDATOR_NAME));
-      try {
-         return Optional.<String> of(injector.getInstance(key));
-      } catch (ConfigurationException ex) {
-         return Optional.<String> absent();
-      }
-   }
-
-   @Provides
-   @Singleton
-   @Validator
-   public final Optional<PrivateKey> provideValidatorCredential(Crypto crypto, Injector injector)
-         throws InvalidKeySpecException, IOException {
-      // Named properties can not be injected as optional here, so let's use the
-      // injector to bypass it
-      Key<String> key = Key.get(String.class, Names.named(CHEF_VALIDATOR_CREDENTIAL));
-      try {
-         String validatorCredential = injector.getInstance(key);
-         PrivateKey validatorKey = crypto.rsaKeyFactory().generatePrivate(
-               Pems.privateKeySpec(ByteSource.wrap(validatorCredential.getBytes(Charsets.UTF_8))));
-         return Optional.<PrivateKey> of(validatorKey);
-      } catch (ConfigurationException ex) {
-         return Optional.<PrivateKey> absent();
-      }
-   }
-
-   @Provides
-   @Singleton
-   final CacheLoader<String, BootstrapConfig> bootstrapConfigForGroup(BootstrapConfigForGroup bootstrapConfigForGroup) {
-      return CacheLoader.from(bootstrapConfigForGroup);
-   }
-
-   @Provides
-   @Singleton
-   final CacheLoader<String, Client> groupToClient(ClientForGroup clientForGroup) {
-      return CacheLoader.from(clientForGroup);
-   }
-
-   @Override
-   protected void bindErrorHandlers() {
-      bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(ChefErrorHandler.class);
-      bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(ChefErrorHandler.class);
-      bind(HttpErrorHandler.class).annotatedWith(ServerError.class).to(ChefErrorHandler.class);
-   }
-
-   @Override
-   protected void bindRetryHandlers() {
-      bind(HttpRetryHandler.class).annotatedWith(ClientError.class).to(ChefApiErrorRetryHandler.class);
-   }
-
-}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/config/ChefHttpApiModule.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/config/ChefHttpApiModule.java b/apis/chef/src/main/java/org/jclouds/chef/config/ChefHttpApiModule.java
index ffd3b0b..f00bdd4 100644
--- a/apis/chef/src/main/java/org/jclouds/chef/config/ChefHttpApiModule.java
+++ b/apis/chef/src/main/java/org/jclouds/chef/config/ChefHttpApiModule.java
@@ -16,13 +16,191 @@
  */
 package org.jclouds.chef.config;
 
+import static com.google.common.base.Suppliers.compose;
+import static com.google.common.base.Suppliers.memoizeWithExpiration;
+import static com.google.common.base.Throwables.propagate;
+import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
+import static org.jclouds.chef.config.ChefProperties.CHEF_VALIDATOR_CREDENTIAL;
+import static org.jclouds.chef.config.ChefProperties.CHEF_VALIDATOR_NAME;
+import static org.jclouds.crypto.Pems.privateKeySpec;
+
+import java.io.IOException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
 import org.jclouds.chef.ChefApi;
+import org.jclouds.chef.domain.BootstrapConfig;
+import org.jclouds.chef.domain.Client;
+import org.jclouds.chef.functions.BootstrapConfigForGroup;
+import org.jclouds.chef.functions.ClientForGroup;
+import org.jclouds.chef.handlers.ChefApiErrorRetryHandler;
+import org.jclouds.chef.handlers.ChefErrorHandler;
+import org.jclouds.crypto.Crypto;
+import org.jclouds.crypto.Pems;
+import org.jclouds.date.DateService;
+import org.jclouds.date.TimeStamp;
+import org.jclouds.domain.Credentials;
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.HttpRetryHandler;
+import org.jclouds.http.annotation.ClientError;
+import org.jclouds.http.annotation.Redirection;
+import org.jclouds.http.annotation.ServerError;
 import org.jclouds.rest.ConfiguresHttpApi;
+import org.jclouds.rest.config.HttpApiModule;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Supplier;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.io.ByteSource;
+import com.google.inject.ConfigurationException;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provides;
+import com.google.inject.name.Names;
 
 /**
  * Configures the Chef connection.
  */
 @ConfiguresHttpApi
-public class ChefHttpApiModule extends BaseChefHttpApiModule<ChefApi> {
+public class ChefHttpApiModule extends HttpApiModule<ChefApi> {
+
+   @Provides
+   @TimeStamp
+   protected final String guiceProvideTimeStamp(@TimeStamp Supplier<String> cache) {
+      return provideTimeStamp(cache);
+   }
+
+   protected String provideTimeStamp(@TimeStamp Supplier<String> cache) {
+      return cache.get();
+   }
+
+   /**
+    * borrowing concurrency code to ensure that caching takes place properly
+    */
+   @Provides
+   @TimeStamp
+   final Supplier<String> provideTimeStampCache(@Named(PROPERTY_SESSION_INTERVAL) long seconds,
+         final DateService dateService) {
+      return memoizeWithExpiration(new Supplier<String>() {
+         @Override
+         public String get() {
+            return dateService.iso8601SecondsDateFormat();
+         }
+      }, seconds, TimeUnit.SECONDS);
+   }
+
+   // TODO: potentially change this
+   @Provides
+   @Singleton
+   public final Supplier<PrivateKey> supplyKey(final LoadingCache<Credentials, PrivateKey> keyCache,
+         @org.jclouds.location.Provider final Supplier<Credentials> creds) {
+      return compose(new Function<Credentials, PrivateKey>() {
+         @Override
+         public PrivateKey apply(Credentials in) {
+            return keyCache.getUnchecked(in);
+         }
+      }, creds);
+   }
+
+   @Provides
+   @Singleton
+   final LoadingCache<Credentials, PrivateKey> privateKeyCache(PrivateKeyForCredentials loader) {
+      // throw out the private key related to old credentials
+      return CacheBuilder.newBuilder().maximumSize(2).build(loader);
+   }
+
+   /**
+    * it is relatively expensive to extract a private key from a PEM. cache the
+    * relationship between current credentials so that the private key is only
+    * recalculated once.
+    */
+   @VisibleForTesting
+   @Singleton
+   private static class PrivateKeyForCredentials extends CacheLoader<Credentials, PrivateKey> {
+      private final Crypto crypto;
+
+      @Inject
+      private PrivateKeyForCredentials(Crypto crypto) {
+         this.crypto = crypto;
+      }
+
+      @Override
+      public PrivateKey load(Credentials in) {
+         try {
+            return crypto.rsaKeyFactory().generatePrivate(
+                  privateKeySpec(ByteSource.wrap(in.credential.getBytes(Charsets.UTF_8))));
+         } catch (InvalidKeySpecException e) {
+            throw propagate(e);
+         } catch (IOException e) {
+            throw propagate(e);
+         }
+      }
+   }
+
+   @Provides
+   @Singleton
+   @Validator
+   public final Optional<String> provideValidatorName(Injector injector) {
+      // Named properties can not be injected as optional here, so let's use the
+      // injector to bypass it
+      Key<String> key = Key.get(String.class, Names.named(CHEF_VALIDATOR_NAME));
+      try {
+         return Optional.<String> of(injector.getInstance(key));
+      } catch (ConfigurationException ex) {
+         return Optional.<String> absent();
+      }
+   }
+
+   @Provides
+   @Singleton
+   @Validator
+   public final Optional<PrivateKey> provideValidatorCredential(Crypto crypto, Injector injector)
+         throws InvalidKeySpecException, IOException {
+      // Named properties can not be injected as optional here, so let's use the
+      // injector to bypass it
+      Key<String> key = Key.get(String.class, Names.named(CHEF_VALIDATOR_CREDENTIAL));
+      try {
+         String validatorCredential = injector.getInstance(key);
+         PrivateKey validatorKey = crypto.rsaKeyFactory().generatePrivate(
+               Pems.privateKeySpec(ByteSource.wrap(validatorCredential.getBytes(Charsets.UTF_8))));
+         return Optional.<PrivateKey> of(validatorKey);
+      } catch (ConfigurationException ex) {
+         return Optional.<PrivateKey> absent();
+      }
+   }
+
+   @Provides
+   @Singleton
+   final CacheLoader<String, BootstrapConfig> bootstrapConfigForGroup(BootstrapConfigForGroup bootstrapConfigForGroup) {
+      return CacheLoader.from(bootstrapConfigForGroup);
+   }
+
+   @Provides
+   @Singleton
+   final CacheLoader<String, Client> groupToClient(ClientForGroup clientForGroup) {
+      return CacheLoader.from(clientForGroup);
+   }
+
+   @Override
+   protected void bindErrorHandlers() {
+      bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(ChefErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(ChefErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ServerError.class).to(ChefErrorHandler.class);
+   }
+
+   @Override
+   protected void bindRetryHandlers() {
+      bind(HttpRetryHandler.class).annotatedWith(ClientError.class).to(ChefApiErrorRetryHandler.class);
+   }
 
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/domain/Group.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/domain/Group.java b/apis/chef/src/main/java/org/jclouds/chef/domain/Group.java
new file mode 100644
index 0000000..eabfcdd
--- /dev/null
+++ b/apis/chef/src/main/java/org/jclouds/chef/domain/Group.java
@@ -0,0 +1,211 @@
+/*
+ * 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.jclouds.chef.domain;
+
+import static org.jclouds.chef.util.CollectionUtils.copyOfOrEmpty;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+import java.util.Set;
+
+import org.jclouds.javax.annotation.Nullable;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Group object.
+ */
+public class Group {
+   public static Builder builder(String groupname) {
+      return new Builder(groupname);
+   }
+
+   public static class Builder {
+      private String name;
+      private String groupname;
+      private String orgname;
+      private ImmutableSet.Builder<String> actors = ImmutableSet.builder();
+      private ImmutableSet.Builder<String> clients = ImmutableSet.builder();
+      private ImmutableSet.Builder<String> groups = ImmutableSet.builder();
+      private ImmutableSet.Builder<String> users = ImmutableSet.builder();
+
+      public Builder(String groupname) {
+         this.groupname = groupname;
+      }
+
+      public Builder name(String name) {
+         this.name = checkNotNull(name, "name");
+         return this;
+      }
+
+      public Builder groupname(String groupname) {
+         this.groupname = checkNotNull(groupname, "groupname");
+         return this;
+      }
+
+      public Builder orgname(String orgname) {
+         this.orgname = checkNotNull(orgname, "orgname");
+         return this;
+      }
+
+      public Builder actor(String actor) {
+         this.actors.add(checkNotNull(actor, "actor"));
+         return this;
+      }
+
+      public Builder actors(Iterable<String> actors) {
+         this.actors.addAll(checkNotNull(actors, "actors"));
+         return this;
+      }
+
+      public Builder client(String client) {
+         this.clients.add(checkNotNull(client, "client"));
+         return this;
+      }
+
+      public Builder clients(Iterable<String> clients) {
+         this.clients.addAll(checkNotNull(clients, "clients"));
+         return this;
+      }
+
+      public Builder group(String group) {
+         this.groups.add(checkNotNull(group, "group"));
+         return this;
+      }
+
+      public Builder groups(Iterable<String> groups) {
+         this.groups.addAll(checkNotNull(groups, "groups"));
+         return this;
+      }
+
+      public Builder user(String user) {
+         this.users.add(checkNotNull(user, "user"));
+         return this;
+      }
+
+      public Builder users(Iterable<String> users) {
+         this.users.addAll(checkNotNull(users, "users"));
+         return this;
+      }
+
+      public Group build() {
+         return new Group(name, checkNotNull(groupname, "groupname"), orgname, actors.build(), clients.build(),
+               groups.build(), users.build());
+      }
+   }
+
+   private final String name;
+   private final String groupname;
+   private final String orgname;
+   private final Set<String> actors;
+   private final Set<String> clients;
+   private final Set<String> groups;
+   private final Set<String> users;
+
+   @ConstructorProperties({ "name", "groupname", "orgname", "actors", "clients", "groups", "users" })
+   public Group(String name, String groupname, String orgname, @Nullable Set<String> actors,
+         @Nullable Set<String> clients, @Nullable Set<String> groups, @Nullable Set<String> users) {
+      this.name = name;
+      this.groupname = groupname;
+      this.orgname = orgname;
+      this.actors = copyOfOrEmpty(actors);
+      this.clients = copyOfOrEmpty(clients);
+      this.groups = copyOfOrEmpty(groups);
+      this.users = copyOfOrEmpty(users);
+   }
+
+   public String getName() {
+      return name;
+   }
+
+   public String getGroupname() {
+      return groupname;
+   }
+
+   public String getOrgname() {
+      return orgname;
+   }
+
+   public Set<String> getActors() {
+      return actors;
+   }
+
+   public Set<String> getClients() {
+      return clients;
+   }
+
+   public Set<String> getGroups() {
+      return groups;
+   }
+
+   public Set<String> getUsers() {
+      return users;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + (groupname == null ? 0 : groupname.hashCode());
+      result = prime * result + (name == null ? 0 : name.hashCode());
+      result = prime * result + (orgname == null ? 0 : orgname.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      Group other = (Group) obj;
+      if (groupname == null) {
+         if (other.groupname != null) {
+            return false;
+         }
+      } else if (!groupname.equals(other.groupname)) {
+         return false;
+      }
+      if (name == null) {
+         if (other.name != null) {
+            return false;
+         }
+      } else if (!name.equals(other.name)) {
+         return false;
+      }
+      if (orgname == null) {
+         if (other.orgname != null) {
+            return false;
+         }
+      } else if (!orgname.equals(other.orgname)) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "Group [name=" + name + ", groupname=" + groupname + ", orgname=" + orgname + ", actors=" + actors
+            + ", clients=" + clients + ", groups=" + groups + ", users=" + users + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/domain/User.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/domain/User.java b/apis/chef/src/main/java/org/jclouds/chef/domain/User.java
new file mode 100644
index 0000000..935bece
--- /dev/null
+++ b/apis/chef/src/main/java/org/jclouds/chef/domain/User.java
@@ -0,0 +1,220 @@
+/*
+ * 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.jclouds.chef.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+import java.security.PublicKey;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * User object.
+ */
+public class User {
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   public static class Builder {
+      private String username;
+      private String firstName;
+      private String middleName;
+      private String lastName;
+      private String displayName;
+      private String email;
+      private PublicKey publicKey;
+
+      public Builder username(String username) {
+         this.username = checkNotNull(username, "username");
+         return this;
+      }
+
+      public Builder firstName(String firstName) {
+         this.firstName = checkNotNull(firstName, "firstName");
+         return this;
+      }
+
+      public Builder middleName(String middleName) {
+         this.middleName = checkNotNull(middleName, "middleName");
+         return this;
+      }
+
+      public Builder lastName(String lastName) {
+         this.lastName = checkNotNull(lastName, "lastName");
+         return this;
+      }
+
+      public Builder displayName(String displayName) {
+         this.displayName = checkNotNull(displayName, "displayName");
+         return this;
+      }
+
+      public Builder email(String email) {
+         this.email = checkNotNull(email, "email");
+         return this;
+      }
+
+      public Builder publicKey(PublicKey publicKey) {
+         this.publicKey = checkNotNull(publicKey, "publicKey");
+         return this;
+      }
+
+      public User build() {
+         return new User(username, firstName, middleName, lastName, displayName, email, publicKey);
+      }
+   }
+
+   private final String username;
+   @SerializedName("first_name")
+   private final String firstName;
+   @SerializedName("middle_name")
+   private final String middleName;
+   @SerializedName("last_name")
+   private final String lastName;
+   @SerializedName("display_name")
+   private final String displayName;
+   private final String email;
+   @SerializedName("public_key")
+   private final PublicKey publicKey;
+
+   @ConstructorProperties({ "username", "first_name", "middle_name", "last_name", "display_name", "email", "public_key" })
+   protected User(String username, String firstName, String middleName, String lastName, String displayName,
+         String email, PublicKey publicKey) {
+      this.username = username;
+      this.firstName = firstName;
+      this.middleName = middleName;
+      this.lastName = lastName;
+      this.displayName = displayName;
+      this.email = email;
+      this.publicKey = publicKey;
+   }
+
+   public String getUsername() {
+      return username;
+   }
+
+   public String getFirstName() {
+      return firstName;
+   }
+
+   public String getMiddleName() {
+      return middleName;
+   }
+
+   public String getLastName() {
+      return lastName;
+   }
+
+   public String getDisplayName() {
+      return displayName;
+   }
+
+   public String getEmail() {
+      return email;
+   }
+
+   public PublicKey getPublicKey() {
+      return publicKey;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + (displayName == null ? 0 : displayName.hashCode());
+      result = prime * result + (email == null ? 0 : email.hashCode());
+      result = prime * result + (firstName == null ? 0 : firstName.hashCode());
+      result = prime * result + (lastName == null ? 0 : lastName.hashCode());
+      result = prime * result + (middleName == null ? 0 : middleName.hashCode());
+      result = prime * result + (publicKey == null ? 0 : publicKey.hashCode());
+      result = prime * result + (username == null ? 0 : username.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      User other = (User) obj;
+      if (displayName == null) {
+         if (other.displayName != null) {
+            return false;
+         }
+      } else if (!displayName.equals(other.displayName)) {
+         return false;
+      }
+      if (email == null) {
+         if (other.email != null) {
+            return false;
+         }
+      } else if (!email.equals(other.email)) {
+         return false;
+      }
+      if (firstName == null) {
+         if (other.firstName != null) {
+            return false;
+         }
+      } else if (!firstName.equals(other.firstName)) {
+         return false;
+      }
+      if (lastName == null) {
+         if (other.lastName != null) {
+            return false;
+         }
+      } else if (!lastName.equals(other.lastName)) {
+         return false;
+      }
+      if (middleName == null) {
+         if (other.middleName != null) {
+            return false;
+         }
+      } else if (!middleName.equals(other.middleName)) {
+         return false;
+      }
+      if (publicKey == null) {
+         if (other.publicKey != null) {
+            return false;
+         }
+      } else if (!publicKey.equals(other.publicKey)) {
+         return false;
+      }
+      if (username == null) {
+         if (other.username != null) {
+            return false;
+         }
+      } else if (!username.equals(other.username)) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "User [username=" + username + ", firstName=" + firstName + ", middleName=" + middleName + ", lastName="
+            + lastName + ", displayName=" + displayName + ", email=" + email + ", publicKey=" + publicKey + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/features/OrganizationApi.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/features/OrganizationApi.java b/apis/chef/src/main/java/org/jclouds/chef/features/OrganizationApi.java
new file mode 100644
index 0000000..3624ddd
--- /dev/null
+++ b/apis/chef/src/main/java/org/jclouds/chef/features/OrganizationApi.java
@@ -0,0 +1,130 @@
+/*
+ * 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.jclouds.chef.features;
+
+import java.util.Set;
+
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Constants;
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.chef.ChefApi;
+import org.jclouds.chef.binders.BindGroupToUpdateRequestJsonPayload;
+import org.jclouds.chef.binders.GroupName;
+import org.jclouds.chef.domain.Group;
+import org.jclouds.chef.domain.User;
+import org.jclouds.chef.filters.SignedHeaderAuth;
+import org.jclouds.chef.functions.ParseKeySetFromJson;
+import org.jclouds.rest.annotations.BinderParam;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.Headers;
+import org.jclouds.rest.annotations.ParamParser;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.annotations.SinceApiVersion;
+import org.jclouds.rest.annotations.WrapWith;
+
+/**
+ * Provides synchronous access to the Enterprise Chef Api.
+ */
+@RequestFilters(SignedHeaderAuth.class)
+@Consumes(MediaType.APPLICATION_JSON)
+@Headers(keys = "X-Chef-Version", values = "{" + Constants.PROPERTY_API_VERSION + "}")
+@SinceApiVersion("12.0.0")
+public interface OrganizationApi extends ChefApi {
+   /**
+    * Retrieves an existing user.
+    * 
+    * @param name
+    *           The name of the user to get.
+    * @return The details of the user or <code>null</code> if not found.
+    */
+   @Named("user:get")
+   @GET
+   @Path("/users/{name}")
+   @Fallback(NullOnNotFoundOr404.class)
+   User getUser(@PathParam("name") String name);
+
+   /**
+    * List all existing groups.
+    * 
+    * @return The list of groups.
+    */
+   @Named("group:list")
+   @GET
+   @Path("/groups")
+   @ResponseParser(ParseKeySetFromJson.class)
+   Set<String> listGroups();
+
+   /**
+    * Retrieves an existing group.
+    * 
+    * @param name
+    *           The name of the group to get.
+    * @return The details of the group or <code>null</code> if not found.
+    */
+   @Named("group:get")
+   @GET
+   @Path("/groups/{name}")
+   @Fallback(NullOnNotFoundOr404.class)
+   Group getGroup(@PathParam("name") String name);
+
+   /**
+    * Creates a new group.
+    * 
+    * @param name
+    *           The name of the group to create.
+    */
+   @Named("group:create")
+   @POST
+   @Path("/groups")
+   void createGroup(@WrapWith("groupname") String name);
+
+   /**
+    * Updates a group.
+    * <p>
+    * This method can be used to add actors (clients, groups) to the group.
+    * 
+    * @param group
+    *           The group with the updated information.
+    */
+   @Named("group:update")
+   @PUT
+   @Path("/groups/{name}")
+   void updateGroup(
+         @PathParam("name") @ParamParser(GroupName.class) @BinderParam(BindGroupToUpdateRequestJsonPayload.class) Group group);
+
+   /**
+    * Deletes a group.
+    * 
+    * @param name
+    *           The name of the group to delete.
+    */
+   @Named("group:delete")
+   @DELETE
+   @Path("/groups/{name}")
+   void deleteGroup(@PathParam("name") String name);
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java b/apis/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java
index 50d26a1..ab2cf3c 100644
--- a/apis/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java
+++ b/apis/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java
@@ -63,7 +63,7 @@ import com.google.common.io.ByteSource;
 /**
  * Ported from mixlib-authentication in order to sign Chef requests.
  * 
- * @see <a href= "http://github.com/opscode/mixlib-authentication" />
+ * @see <a href= "https://github.com/chef/mixlib-authentication" />
  */
 @Singleton
 public class SignedHeaderAuth implements HttpRequestFilter {

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/main/java/org/jclouds/chef/options/CreateClientOptions.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/main/java/org/jclouds/chef/options/CreateClientOptions.java b/apis/chef/src/main/java/org/jclouds/chef/options/CreateClientOptions.java
index 776d7bb..45d94ee 100644
--- a/apis/chef/src/main/java/org/jclouds/chef/options/CreateClientOptions.java
+++ b/apis/chef/src/main/java/org/jclouds/chef/options/CreateClientOptions.java
@@ -20,7 +20,7 @@ package org.jclouds.chef.options;
  * Options for the create client method.
  */
 public class CreateClientOptions implements Cloneable {
-   /** Administrator flag. This flag will be ignored in Opscode Hosted Chef. */
+   /** Administrator flag. This flag will be ignored in Chef 12. */
    private boolean admin;
 
    public CreateClientOptions() {

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/test/java/org/jclouds/chef/BaseChefApiExpectTest.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/test/java/org/jclouds/chef/BaseChefApiExpectTest.java b/apis/chef/src/test/java/org/jclouds/chef/BaseChefApiExpectTest.java
index 23013a9..8cb3288 100644
--- a/apis/chef/src/test/java/org/jclouds/chef/BaseChefApiExpectTest.java
+++ b/apis/chef/src/test/java/org/jclouds/chef/BaseChefApiExpectTest.java
@@ -33,6 +33,7 @@ public abstract class BaseChefApiExpectTest<S> extends BaseRestApiExpectTest<S>
    protected SignedHeaderAuth signedHeaderAuth;
 
    public BaseChefApiExpectTest() {
+      provider = "chef";
       credential = PRIVATE_KEY;
       signedHeaderAuth = createInjector(Functions.forMap(ImmutableMap.<HttpRequest, HttpResponse> of()),
             createModule(), setupProperties()).getInstance(SignedHeaderAuth.class);

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/test/java/org/jclouds/chef/ChefApiDelegationTest.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/test/java/org/jclouds/chef/ChefApiDelegationTest.java b/apis/chef/src/test/java/org/jclouds/chef/ChefApiDelegationTest.java
new file mode 100644
index 0000000..893d9d3
--- /dev/null
+++ b/apis/chef/src/test/java/org/jclouds/chef/ChefApiDelegationTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.jclouds.chef;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+
+import org.jclouds.ContextBuilder;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "ChefApiDelegationTest")
+public class ChefApiDelegationTest {
+
+   public void testOrganizationApiNotAvailableInOldVersions() throws IOException {
+      ChefApi chef = ContextBuilder.newBuilder(new ChefApiMetadata())
+            .credentials("foo", "bar")
+            .apiVersion("11.0.4")
+            .buildApi(ChefApi.class);
+      
+      try {
+         assertFalse(chef.organizationApi().isPresent());
+      } finally {
+         chef.close();
+      }
+   }
+   
+   public void testOrganizationApiPresentInRecentVersions() throws IOException {
+      ChefApi chef = ContextBuilder.newBuilder(new ChefApiMetadata())
+            .credentials("foo", "bar")
+            .apiVersion("12.0.4")
+            .buildApi(ChefApi.class);
+      
+      try {
+         assertTrue(chef.organizationApi().isPresent());
+      } finally {
+         chef.close();
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/test/java/org/jclouds/chef/ChefApiExpectTest.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/test/java/org/jclouds/chef/ChefApiExpectTest.java b/apis/chef/src/test/java/org/jclouds/chef/ChefApiExpectTest.java
index 478959b..1bf308b 100644
--- a/apis/chef/src/test/java/org/jclouds/chef/ChefApiExpectTest.java
+++ b/apis/chef/src/test/java/org/jclouds/chef/ChefApiExpectTest.java
@@ -42,9 +42,6 @@ import com.google.inject.Module;
  */
 @Test(groups = "unit", testName = "ChefApiExpectTest")
 public class ChefApiExpectTest extends BaseChefApiExpectTest<ChefApi> {
-   public ChefApiExpectTest() {
-     provider = "chef";
-   }
 
    private HttpRequest.Builder<?> getHttpRequestBuilder(String method, String endPoint) {
       return HttpRequest.builder() //

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/test/java/org/jclouds/chef/ChefApiLiveTest.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/test/java/org/jclouds/chef/ChefApiLiveTest.java b/apis/chef/src/test/java/org/jclouds/chef/ChefApiLiveTest.java
index e4ad583..c00e0d9 100644
--- a/apis/chef/src/test/java/org/jclouds/chef/ChefApiLiveTest.java
+++ b/apis/chef/src/test/java/org/jclouds/chef/ChefApiLiveTest.java
@@ -16,17 +16,527 @@
  */
 package org.jclouds.chef;
 
-import org.jclouds.chef.internal.BaseChefApiLiveTest;
+import static com.google.common.collect.Iterables.any;
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.hash.Hashing.md5;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.jclouds.util.Closeables2.closeQuietly;
+import static org.jclouds.util.Predicates2.retry;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import org.jclouds.chef.domain.ChecksumStatus;
+import org.jclouds.chef.domain.Client;
+import org.jclouds.chef.domain.CookbookDefinition;
+import org.jclouds.chef.domain.CookbookVersion;
+import org.jclouds.chef.domain.DatabagItem;
+import org.jclouds.chef.domain.Environment;
+import org.jclouds.chef.domain.Metadata;
+import org.jclouds.chef.domain.Node;
+import org.jclouds.chef.domain.Resource;
+import org.jclouds.chef.domain.Role;
+import org.jclouds.chef.domain.Sandbox;
+import org.jclouds.chef.domain.SearchResult;
+import org.jclouds.chef.domain.UploadSandbox;
+import org.jclouds.chef.internal.BaseChefLiveTest;
+import org.jclouds.chef.options.CreateClientOptions;
+import org.jclouds.chef.options.SearchOptions;
+import org.jclouds.crypto.Pems;
+import org.jclouds.io.ByteStreams2;
+import org.jclouds.io.Payloads;
+import org.jclouds.io.payloads.FilePayload;
+import org.jclouds.rest.ResourceNotFoundException;
+import org.testng.annotations.AfterClass;
 import org.testng.annotations.Test;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.hash.Hashing;
+import com.google.common.io.Files;
+import com.google.common.primitives.Bytes;
+
 /**
- * Tests behavior of {@code ChefApi} against a Chef Server <= 0.9.8.
+ * Tests behavior of {@code ChefApi}
  */
-@Test(groups = { "live" })
-public class ChefApiLiveTest extends BaseChefApiLiveTest<ChefApi> {
+@Test(groups = { "live", "integration" })
+public class ChefApiLiveTest extends BaseChefLiveTest {
+   public static final String PREFIX = "jcloudstest-" + System.getProperty("user.name");
+   public static final String ADMIN_PREFIX = "jcloudstest-adm-" + System.getProperty("user.name");
+   public static final String ENV_NODE = PREFIX + "-env-node";
+
+   // It may take a bit until the search index is populated
+   protected int maxWaitForIndexInMs = 60000;
+
+   // The id of the data bag item used in search tests
+   private String databagitemId;
+
+   public void testCreateNewCookbook() throws Exception {
+      // Define the file you want in the cookbook
+      File file = new File(System.getProperty("user.dir"), "pom.xml");
+      FilePayload content = uploadAndGetFilePayload(file);
+
+      // Create the metadata of the cookbook
+      Metadata metadata = Metadata.builder() //
+            .name(PREFIX) //
+            .version("0.0.0") //
+            .description("Jclouds test uploaded cookbook") //
+            .maintainer("jclouds") //
+            .maintainerEmail("someone@jclouds.org") //
+            .license("Apache 2.0") //
+            .build();
+
+      // Create a new cookbook
+      CookbookVersion cookbook = CookbookVersion.builder(PREFIX, "0.0.0") //
+            .metadata(metadata) //
+            .rootFile(Resource.builder().fromPayload(content).build()) //
+            .attribute(Resource.builder().fromPayload(content).name("default.rb").build())
+            .build();
+
+      // upload the cookbook to the remote server
+      api.updateCookbook(PREFIX, "0.0.0", cookbook);
+   }
+
+   private FilePayload uploadAndGetFilePayload(File file) throws IOException {
+      FilePayload content = Payloads.newFilePayload(file);
+      content.getContentMetadata().setContentType("application/x-binary");
+
+      // Get an md5 so that you can see if the server already has it or not
+      content.getContentMetadata().setContentMD5(Files.asByteSource(file).hash(Hashing.md5()).asBytes());
+
+      // Note that java collections cannot effectively do equals or hashcodes on
+      // byte arrays, so let's convert to a list of bytes.
+      List<Byte> md5 = Bytes.asList(content.getContentMetadata().getContentMD5());
+
+      // Request an upload site for this file
+      UploadSandbox site = api.createUploadSandboxForChecksums(ImmutableSet.of(md5));
+      assertTrue(site.getChecksums().containsKey(md5), md5 + " not in " + site.getChecksums());
+
+      try {
+         // Upload the file contents, if still not uploaded
+         ChecksumStatus status = site.getChecksums().get(md5);
+         if (status.needsUpload()) {
+            api.uploadContent(status.getUrl(), content);
+         }
+         Sandbox sandbox = api.commitSandbox(site.getSandboxId(), true);
+         assertTrue(sandbox.isCompleted(), "Sandbox should be completed after uploading");
+      } catch (RuntimeException e) {
+         api.commitSandbox(site.getSandboxId(), false);
+         fail("Could not upload content");
+      }
+      return content;
+   }
+
+   public void testListCookbooks() throws Exception {
+      Set<String> cookbookNames = api.listCookbooks();
+      assertFalse(cookbookNames.isEmpty(), "No cookbooks were found");
+
+      for (String cookbookName : cookbookNames) {
+         Set<String> versions = api.listVersionsOfCookbook(cookbookName);
+         assertFalse(versions.isEmpty(), "There are no versions of the cookbook: " + cookbookName);
+
+         for (String version : api.listVersionsOfCookbook(cookbookName)) {
+            CookbookVersion cookbook = api.getCookbook(cookbookName, version);
+            assertNotNull(cookbook, "Could not get cookbook: " + cookbookName);
+         }
+      }
+   }
+
+   @Test(dependsOnMethods = "testListCookbooks")
+   public void testListCookbookVersionsWithChefService() throws Exception {
+      Iterable<? extends CookbookVersion> cookbooks = chefService.listCookbookVersions();
+      assertFalse(isEmpty(cookbooks), "No cookbooks were found");
+   }
+
+   @Test(dependsOnMethods = "testListCookbookVersionsWithChefService")
+   public void testDownloadCookbooks() throws Exception {
+      Iterable<? extends CookbookVersion> cookbooks = chefService.listCookbookVersions();
+      for (CookbookVersion cookbook : cookbooks) {
+         for (Resource resource : ImmutableList.<Resource> builder().addAll(cookbook.getDefinitions())
+                 .addAll(cookbook.getFiles()).addAll(cookbook.getLibraries()).addAll(cookbook.getSuppliers())
+                 .addAll(cookbook.getRecipes()).addAll(cookbook.getResources()).addAll(cookbook.getRootFiles())
+                 .addAll(cookbook.getTemplates()).addAll(cookbook.getAttributes()).build()) {
+
+            InputStream stream = api.getResourceContents(resource);
+            assertNotNull(stream, "Resource contents are null for resource: " + resource.getName());
+
+            byte[] md5 = ByteStreams2.hashAndClose(stream, md5()).asBytes();
+            assertEquals(md5, resource.getChecksum());
+         }
+      }
+   }
+
+   @Test(dependsOnMethods = "testCreateNewCookbook")
+   public void testUpdateCookbook() throws Exception {
+      CookbookVersion cookbook = api.getCookbook(PREFIX, "0.0.0");
+      assertNotNull(cookbook, "Cookbook not found: " + PREFIX);
+      assertNotNull(api.updateCookbook(PREFIX, "0.0.0", cookbook), "Updated cookbook was null");
+   }
+
+   @Test(dependsOnMethods = { "testCreateNewCookbook", "testUpdateCookbook" })
+   public void testDeleteCookbook() throws Exception {
+      assertNotNull(api.deleteCookbook(PREFIX, "0.0.0"), "Deleted cookbook was null");
+   }
+
+   @Test
+   public void testCreateClient() throws Exception {
+      api.deleteClient(PREFIX);
+      String credential = Pems.pem(api.createClient(PREFIX).getPrivateKey());
+      assertClientCreated(PREFIX, credential);
+   }
+
+   @Test
+   public void testCreateAdminClient() throws Exception {
+      api.deleteClient(ADMIN_PREFIX);
+      String credential = Pems.pem(api.createClient(ADMIN_PREFIX, CreateClientOptions.Builder.admin()).getPrivateKey());
+      assertClientCreated(ADMIN_PREFIX, credential);
+   }
+
+   @Test(dependsOnMethods = "testCreateClient")
+   public void testGenerateKeyForClient() throws Exception {
+      String credential = Pems.pem(api.generateKeyForClient(PREFIX).getPrivateKey());
+      assertClientCreated(PREFIX, credential);
+   }
+
+   @Test
+   public void testListNodes() throws Exception {
+      Set<String> nodes = api.listNodes();
+      assertNotNull(nodes, "No nodes were found");
+   }
+
+   @Test(dependsOnMethods = "testCreateRole")
+   public void testCreateNode() throws Exception {
+      api.deleteNode(PREFIX);
+      api.createNode(Node.builder().name(PREFIX).runListElement("role[" + PREFIX + "]").environment("_default").build());
+      Node node = api.getNode(PREFIX);
+      // TODO check recipes
+      assertNotNull(node, "Created node should not be null");
+      Set<String> nodes = api.listNodes();
+      assertTrue(nodes.contains(PREFIX), String.format("node %s not in %s", PREFIX, nodes));
+   }
+
+   @Test(dependsOnMethods = "testCreateNode")
+   public void testUpdateNode() throws Exception {
+      for (String nodename : api.listNodes()) {
+         Node node = api.getNode(nodename);
+         api.updateNode(node);
+      }
+   }
+
+   @Test
+   public void testListRoles() throws Exception {
+      Set<String> roles = api.listRoles();
+      assertNotNull(roles, "Role list was null");
+   }
+
+   @Test
+   public void testCreateRole() throws Exception {
+      api.deleteRole(PREFIX);
+      api.createRole(Role.builder().name(PREFIX).runListElement("recipe[java]").build());
+      Role role = api.getRole(PREFIX);
+      assertNotNull(role, "Created role should not be null");
+      assertEquals(role.getName(), PREFIX);
+      assertEquals(role.getRunList(), Collections.singleton("recipe[java]"));
+   }
+
+   @Test(dependsOnMethods = "testCreateRole")
+   public void testUpdateRole() throws Exception {
+      for (String rolename : api.listRoles()) {
+         Role role = api.getRole(rolename);
+         api.updateRole(role);
+      }
+   }
+
+   @Test
+   public void testListDatabags() throws Exception {
+      Set<String> databags = api.listDatabags();
+      assertNotNull(databags, "Data bag list was null");
+   }
+
+   @Test
+   public void testCreateDatabag() throws Exception {
+      api.deleteDatabag(PREFIX);
+      api.createDatabag(PREFIX);
+   }
 
-   protected ChefApiLiveTest() {
-      provider = "chef";
+   @Test(dependsOnMethods = "testCreateDatabagItem")
+   public void testListDatabagItems() throws Exception {
+      Set<String> databagItems = api.listDatabagItems(PREFIX);
+      assertNotNull(databagItems, "Data bag item list was null");
    }
 
+   @Test(dependsOnMethods = "testCreateDatabag")
+   public void testCreateDatabagItem() throws Exception {
+      Properties config = new Properties();
+      config.setProperty("foo", "bar");
+      api.deleteDatabagItem(PREFIX, PREFIX);
+      DatabagItem databagItem = api.createDatabagItem(PREFIX, new DatabagItem("config", json.toJson(config)));
+      databagitemId = databagItem.getId();
+      assertNotNull(databagItem, "Created data bag item should not be null");
+      assertEquals(databagItem.getId(), "config");
+
+      // The databagItem json contains extra keys: (the name and the type if the
+      // item)
+      Properties props = json.fromJson(databagItem.toString(), Properties.class);
+      for (Object key : config.keySet()) {
+         assertTrue(props.containsKey(key));
+         assertEquals(config.get(key), props.get(key));
+      }
+   }
+
+   @Test(dependsOnMethods = "testCreateDatabagItem")
+   public void testUpdateDatabagItem() throws Exception {
+      for (String databagItemId : api.listDatabagItems(PREFIX)) {
+         DatabagItem databagItem = api.getDatabagItem(PREFIX, databagItemId);
+         api.updateDatabagItem(PREFIX, databagItem);
+      }
+   }
+
+   @Test(dependsOnMethods = "testSearchDatabagWithOptions")
+   public void testDeleteDatabagItem() throws Exception {
+      for (String databagItemId : api.listDatabagItems(PREFIX)) {
+         DatabagItem databagItem = api.deleteDatabagItem(PREFIX, databagItemId);
+         assertNotNull(databagItem, "Deleted data bag item should not be null");
+         assertEquals(databagItem.getId(), databagItemId, "Deleted data bag item id must match the original id");
+         assertNull(api.getDatabagItem(PREFIX, databagItemId), "Data bag item should not exist");
+      }
+   }
+
+   @Test
+   public void testListSearchIndexes() throws Exception {
+      Set<String> indexes = api.listSearchIndexes();
+      assertNotNull(indexes, "The index list should not be null");
+      assertTrue(indexes.contains("node"));
+      assertTrue(indexes.contains("client"));
+      assertTrue(indexes.contains("role"));
+   }
+
+   @Test
+   public void testSearchNodes() throws Exception {
+      SearchResult<? extends Node> results = api.searchNodes();
+      assertNotNull(results, "Node result list should not be null");
+   }
+
+   @Test(dependsOnMethods = { "testListSearchIndexes", "testCreateNode" })
+   public void testSearchNodesWithOptions() throws Exception {
+      Predicate<SearchOptions> waitForIndex = retry(new Predicate<SearchOptions>() {
+         @Override
+         public boolean apply(SearchOptions input) {
+            SearchResult<? extends Node> results = api.searchNodes(input);
+            assertNotNull(results);
+            if (results.size() > 0) {
+               assertEquals(results.size(), 1);
+               assertEquals(results.iterator().next().getName(), PREFIX);
+               return true;
+            } else {
+               // The index may still not be populated
+               return false;
+            }
+         }
+      }, maxWaitForIndexInMs, 5000L, MILLISECONDS);
+
+      SearchOptions options = SearchOptions.Builder.query("name:" + PREFIX);
+      assertTrue(waitForIndex.apply(options));
+   }
+
+   @Test
+   public void testSearchClients() throws Exception {
+      SearchResult<? extends Client> results = api.searchClients();
+      assertNotNull(results, "Client result list should not be null");
+   }
+
+   @Test(dependsOnMethods = { "testListSearchIndexes", "testCreateClient" })
+   public void testSearchClientsWithOptions() throws Exception {
+      Predicate<SearchOptions> waitForIndex = retry(new Predicate<SearchOptions>() {
+         @Override
+         public boolean apply(SearchOptions input) {
+            SearchResult<? extends Client> results = api.searchClients(input);
+            assertNotNull(results);
+            if (results.size() > 0) {
+               assertEquals(results.size(), 1);
+               assertEquals(results.iterator().next().getName(), PREFIX);
+               return true;
+            } else {
+               // The index may still not be populated
+               return false;
+            }
+         }
+      }, maxWaitForIndexInMs, 5000L, MILLISECONDS);
+
+      SearchOptions options = SearchOptions.Builder.query("name:" + PREFIX);
+      assertTrue(waitForIndex.apply(options));
+   }
+
+   @Test
+   public void testSearchRoles() throws Exception {
+      SearchResult<? extends Role> results = api.searchRoles();
+      assertNotNull(results, "Role result list should not be null");
+   }
+
+   @Test(dependsOnMethods = { "testListSearchIndexes", "testCreateRole" })
+   public void testSearchRolesWithOptions() throws Exception {
+      Predicate<SearchOptions> waitForIndex = retry(new Predicate<SearchOptions>() {
+         @Override
+         public boolean apply(SearchOptions input) {
+            SearchResult<? extends Role> results = api.searchRoles(input);
+            assertNotNull(results);
+            if (results.size() > 0) {
+               assertEquals(results.size(), 1);
+               assertEquals(results.iterator().next().getName(), PREFIX);
+               return true;
+            } else {
+               // The index may still not be populated
+               return false;
+            }
+         }
+      }, maxWaitForIndexInMs, 5000L, MILLISECONDS);
+
+      SearchOptions options = SearchOptions.Builder.query("name:" + PREFIX);
+      assertTrue(waitForIndex.apply(options));
+   }
+
+   @Test(dependsOnMethods = { "testListSearchIndexes", "testCreateDatabagItem" })
+   public void testSearchDatabag() throws Exception {
+      SearchResult<? extends DatabagItem> results = api.searchDatabagItems(PREFIX);
+      assertNotNull(results, "Data bag item result list should not be null");
+   }
+
+   @Test(dependsOnMethods = { "testListSearchIndexes", "testCreateDatabagItem" })
+   public void testSearchDatabagWithOptions() throws Exception {
+      Predicate<SearchOptions> waitForIndex = retry(new Predicate<SearchOptions>() {
+         @Override
+         public boolean apply(SearchOptions input) {
+            SearchResult<? extends DatabagItem> results = api.searchDatabagItems(PREFIX, input);
+            assertNotNull(results);
+            if (results.size() > 0) {
+               assertEquals(results.size(), 1);
+               assertEquals(results.iterator().next().getId(), databagitemId);
+               return true;
+            } else {
+               // The index may still not be populated
+               return false;
+            }
+         }
+      }, maxWaitForIndexInMs, 5000L, MILLISECONDS);
+
+      SearchOptions options = SearchOptions.Builder.query("id:" + databagitemId);
+      assertTrue(waitForIndex.apply(options));
+   }
+
+   @Test(expectedExceptions = ResourceNotFoundException.class, dependsOnMethods = "testListSearchIndexes")
+   public void testSearchDatabagNotFound() throws Exception {
+      SearchResult<? extends DatabagItem> results = api.searchDatabagItems("whoopie");
+      assertNotNull(results, "Data bag item result list should not be null");
+   }
+
+   @Test
+   public void testCreateEnvironment() {
+      api.deleteEnvironment(PREFIX);
+      api.createEnvironment(Environment.builder().name(PREFIX).description(PREFIX).build());
+      Environment env = api.getEnvironment(PREFIX);
+      assertNotNull(env, "Created environment should not be null");
+      assertEquals(env.getName(), PREFIX);
+      assertEquals(env.getDescription(), PREFIX);
+   }
+
+   @Test(dependsOnMethods = "testCreateEnvironment")
+   public void testListEnvironment() {
+      Set<String> envList = api.listEnvironments();
+      assertNotNull(envList, "Environment list was null");
+      assertTrue(envList.contains(PREFIX));
+   }
+
+   @Test(dependsOnMethods = "testCreateEnvironment")
+   public void testSearchEnvironments() throws Exception {
+      SearchResult<? extends Environment> results = api.searchEnvironments();
+      assertNotNull(results, "Environment result list was null");
+   }
+
+   @Test(dependsOnMethods = { "testListSearchIndexes", "testCreateEnvironment" })
+   public void testSearchEnvironmentsWithOptions() throws Exception {
+      Predicate<SearchOptions> waitForIndex = retry(new Predicate<SearchOptions>() {
+         @Override
+         public boolean apply(SearchOptions input) {
+            SearchResult<? extends Environment> results = api.searchEnvironments(input);
+            assertNotNull(results);
+            if (results.size() > 0) {
+               assertEquals(results.size(), 1);
+               assertEquals(results.iterator().next().getName(), PREFIX);
+               return true;
+            } else {
+               // The index may still not be populated
+               return false;
+            }
+         }
+      }, maxWaitForIndexInMs, 5000L, MILLISECONDS);
+
+      SearchOptions options = SearchOptions.Builder.query("name:" + PREFIX);
+      assertTrue(waitForIndex.apply(options));
+   }
+
+   @Test(dependsOnMethods = "testCreateEnvironment")
+   public void testListRecipesInEnvironment() {
+      Set<String> recipeList = api.listRecipesInEnvironment(PREFIX);
+      assertTrue(!recipeList.isEmpty());
+   }
+
+   @Test(dependsOnMethods = "testCreateEnvironment")
+   public void testListNodesInEnvironment() {
+      api.deleteNode(ENV_NODE);
+      api.createNode(Node.builder().name(ENV_NODE).runListElement("role[" + PREFIX + "]").environment(PREFIX).build());
+      Node node = api.getNode(ENV_NODE);
+      assertNotNull(node, "Created node should not be null");
+      Set<String> nodeList = api.listNodesInEnvironment(PREFIX);
+      assertTrue(!nodeList.isEmpty());
+   }
+
+   @Test(dependsOnMethods = "testCreateNewCookbook")
+   public void testListCookbooksInEnvironment() throws Exception {
+      Set<CookbookDefinition> cookbooks = api.listCookbooksInEnvironment("_default");
+      assertTrue(any(cookbooks, new Predicate<CookbookDefinition>() {
+         @Override
+         public boolean apply(CookbookDefinition input) {
+            return PREFIX.equals(input.getName());
+         }}), String.format("Cookbook %s not in %s", PREFIX, cookbooks));
+   }
+
+   @AfterClass(groups = { "live", "integration" })
+   @Override
+   public void tearDown() {
+      api.deleteClient(PREFIX);
+      api.deleteClient(ADMIN_PREFIX);
+      api.deleteNode(PREFIX);
+      api.deleteNode(ENV_NODE);
+      api.deleteRole(PREFIX);
+      api.deleteDatabag(PREFIX);
+      api.deleteEnvironment(PREFIX);
+      super.tearDown();
+   }
+
+   private void assertClientCreated(String identity, String credential) {
+      Properties overrides = super.setupProperties();
+      overrides.setProperty(provider + ".identity", identity);
+      overrides.setProperty(provider + ".credential", credential);
+
+      ChefApi clientApi = create(overrides, setupModules());
+
+      try {
+         Client client = clientApi.getClient(identity);
+         assertNotNull(client, "Client not found: " + identity);
+      } finally {
+         closeQuietly(clientApi);
+      }
+   }
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/test/java/org/jclouds/chef/binders/BindGroupToUpdateRequestJsonPayloadTest.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/test/java/org/jclouds/chef/binders/BindGroupToUpdateRequestJsonPayloadTest.java b/apis/chef/src/test/java/org/jclouds/chef/binders/BindGroupToUpdateRequestJsonPayloadTest.java
new file mode 100644
index 0000000..f8d7b2b
--- /dev/null
+++ b/apis/chef/src/test/java/org/jclouds/chef/binders/BindGroupToUpdateRequestJsonPayloadTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.jclouds.chef.binders;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.jclouds.chef.ChefApiMetadata;
+import org.jclouds.chef.config.ChefParserModule;
+import org.jclouds.chef.domain.Group;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.json.config.GsonModule;
+import org.jclouds.rest.annotations.ApiVersion;
+import org.jclouds.util.Strings2;
+import org.testng.annotations.Test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+/**
+ * Unit tests for the {@link BindGroupToUpdateRequestJsonPayload} class.
+ */
+@Test(groups = "unit", testName = "BindGroupToUpdateRequestJsonPayloadTest")
+public class BindGroupToUpdateRequestJsonPayloadTest {
+
+   private Injector injector = Guice.createInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+         bind(String.class).annotatedWith(ApiVersion.class).toInstance(ChefApiMetadata.DEFAULT_API_VERSION);
+      }
+   }, new ChefParserModule(), new GsonModule());
+
+   private BindGroupToUpdateRequestJsonPayload binder = injector.getInstance(BindGroupToUpdateRequestJsonPayload.class);
+
+   @Test(expectedExceptions = NullPointerException.class)
+   public void testInvalidNullInput() {
+      HttpRequest request = HttpRequest.builder().method("POST").endpoint(URI.create("http://localhost")).build();
+      binder.bindToRequest(request, null);
+   }
+
+   @Test(expectedExceptions = IllegalArgumentException.class)
+   public void testInvalidTypeInput() {
+      HttpRequest request = HttpRequest.builder().method("POST").endpoint(URI.create("http://localhost")).build();
+      binder.bindToRequest(request, new Object());
+   }
+
+   public void testBindOnlyName() throws IOException {
+      HttpRequest request = HttpRequest.builder().method("POST").endpoint(URI.create("http://localhost")).build();
+      HttpRequest newRequest = binder.bindToRequest(request, Group.builder("foo").build());
+
+      String payload = Strings2.toStringAndClose(newRequest.getPayload().getInput());
+      assertEquals(payload, "{\"groupname\":\"foo\",\"actors\":{\"clients\":[],\"groups\":[],\"users\":[]}}");
+   }
+
+   public void testBindNameAndLists() throws IOException {
+      Group group = Group.builder("foo").client("nacx-validator").group("admins").user("nacx").build();
+
+      HttpRequest request = HttpRequest.builder().method("POST").endpoint(URI.create("http://localhost")).build();
+      HttpRequest newRequest = binder.bindToRequest(request, group);
+
+      String payload = Strings2.toStringAndClose(newRequest.getPayload().getInput());
+      assertEquals(payload,
+            "{\"groupname\":\"foo\",\"actors\":{\"clients\":[\"nacx-validator\"],\"groups\":[\"admins\"],\"users\":[\"nacx\"]}}");
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9df30c5a/apis/chef/src/test/java/org/jclouds/chef/binders/GroupNameTest.java
----------------------------------------------------------------------
diff --git a/apis/chef/src/test/java/org/jclouds/chef/binders/GroupNameTest.java b/apis/chef/src/test/java/org/jclouds/chef/binders/GroupNameTest.java
new file mode 100644
index 0000000..34303e8
--- /dev/null
+++ b/apis/chef/src/test/java/org/jclouds/chef/binders/GroupNameTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.jclouds.chef.binders;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.IOException;
+
+import org.jclouds.chef.domain.Group;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the {@link GroupName} class.
+ */
+@Test(groups = "unit", testName = "GroupNameTest")
+public class GroupNameTest {
+
+   @Test(expectedExceptions = NullPointerException.class)
+   public void testInvalidNullInput() {
+      new GroupName().apply(null);
+   }
+
+   public void testApplyGroupName() throws IOException {
+      assertEquals(new GroupName().apply(Group.builder("foo").build()), "foo");
+   }
+}