Added support for Role definition in Chef Solo

Allow to define Roles on the fly when configuring the Chef Solo
statement in order to be able to use them later when defining the run
list for the compute node.
This commit is contained in:
Ignasi Barrera 2012-11-06 22:15:28 +01:00
parent e4fd8f2995
commit ee8e82f57b
4 changed files with 418 additions and 36 deletions

View File

@ -0,0 +1,193 @@
/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds 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.scriptbuilder.domain.chef;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.transform;
import java.util.List;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
/**
* A Role to be configured for a Chef Solo run.
*
* @author Ignasi Barrera
*/
public class Role {
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name;
private String description;
private String jsonDefaultAttributes;
private String jsonOverrideAttributes;
private List<String> runlist = Lists.newArrayList();
public Builder name(String name) {
this.name = checkNotNull(name, "name must be set");
return this;
}
public Builder description(String description) {
this.description = checkNotNull(description, "description must be set");
return this;
}
public Builder jsonDefaultAttributes(String jsonDefaultAttributes) {
this.jsonDefaultAttributes = checkNotNull(jsonDefaultAttributes, "jsonDefaultAttributes must be set");
return this;
}
public Builder jsonOverrideAttributes(String jsonOverrideAttributes) {
this.jsonOverrideAttributes = checkNotNull(jsonOverrideAttributes, "jsonOverrideAttributes must be set");
return this;
}
public Builder installRecipe(String recipe) {
this.runlist.add("recipe[" + checkNotNull(recipe, "recipe must be set") + "]");
return this;
}
public Builder installRecipes(Iterable<String> recipes) {
this.runlist.addAll(Lists.newArrayList(transform(checkNotNull(recipes, "recipes must be set"),
new Function<String, String>() {
@Override
public String apply(String input) {
return "recipe[" + input + "]";
}
})));
return this;
}
public Builder installRole(String role) {
this.runlist.add("role[" + checkNotNull(role, "role must be set") + "]");
return this;
}
public Builder installRoles(Iterable<String> roles) {
this.runlist.addAll(Lists.newArrayList(transform(checkNotNull(roles, "roles must be set"),
new Function<String, String>() {
@Override
public String apply(String input) {
return "role[" + input + "]";
}
})));
return this;
}
public Role build() {
return new Role(name, Optional.fromNullable(description), Optional.fromNullable(jsonDefaultAttributes),
Optional.fromNullable(jsonOverrideAttributes), runlist);
}
}
private String name;
private Optional<String> description;
private Optional<String> jsonDefaultAttributes;
private Optional<String> jsonOverrideAttributes;
private List<String> runlist;
protected Role(String name, Optional<String> description, Optional<String> jsonDefaultAttributes,
Optional<String> jsonOverrideAttributes, List<String> runlist) {
this.name = checkNotNull(name, "name must be set");
this.description = checkNotNull(description, "description must be set");
this.jsonDefaultAttributes = checkNotNull(jsonDefaultAttributes, "jsonDefaultAttributes must be set");
this.jsonOverrideAttributes = checkNotNull(jsonOverrideAttributes, "jsonOverrideAttributes must be set");
this.runlist = ImmutableList.<String> copyOf(checkNotNull(runlist, "runlist must be set"));
}
public String toJsonString() {
StringBuilder json = new StringBuilder();
json.append("{");
json.append("\"name\": \"").append(name).append("\",");
json.append("\"description\":\"").append(description.or("")).append("\",");
json.append("\"default_attributes\":").append(jsonDefaultAttributes.or("{}")).append(",");
json.append("\"override_attributes\":").append(jsonOverrideAttributes.or("{}")).append(",");
json.append("\"json_class\":\"Chef::Role\",");
json.append("\"chef_type\":\"role\",");
json.append("\"run_list\":[").append(runlistToJsonString(runlist)).append("]");
json.append("}");
return json.toString();
}
public String getName() {
return name;
}
public Optional<String> getDescription() {
return description;
}
public Optional<String> getJsonDefaultAttributes() {
return jsonDefaultAttributes;
}
public Optional<String> getJsonOverrideAttributes() {
return jsonOverrideAttributes;
}
public List<String> getRunlist() {
return runlist;
}
@Override
public int hashCode() {
return Objects.hashCode(name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Role other = Role.class.cast(obj);
return Objects.equal(name, other.name);
}
@Override
public String toString() {
return Objects.toStringHelper(this).omitNullValues().add("name", name).add("description", description.orNull())
.toString();
}
private static String runlistToJsonString(List<String> runlist) {
return Joiner.on(',').join(transform(runlist, new Function<String, String>() {
@Override
public String apply(String input) {
return "\"" + input + "\"";
}
}));
}
}

View File

@ -29,12 +29,14 @@ import org.jclouds.scriptbuilder.domain.OsFamily;
import org.jclouds.scriptbuilder.domain.Statement;
import org.jclouds.scriptbuilder.domain.StatementList;
import org.jclouds.scriptbuilder.domain.Statements;
import org.jclouds.scriptbuilder.domain.chef.Role;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
/**
@ -43,6 +45,7 @@ import com.google.common.collect.Lists;
* @author Ignasi Barrera
*/
public class ChefSolo implements Statement {
public static Builder builder() {
return new Builder();
}
@ -50,7 +53,8 @@ public class ChefSolo implements Statement {
public static class Builder {
private String cookbooksArchiveLocation;
private String jsonAttributes;
private List<String> recipes = Lists.newArrayList();
private List<Role> roles = Lists.newArrayList();
private List<String> runlist = Lists.newArrayList();
public Builder cookbooksArchiveLocation(String cookbooksArchiveLocation) {
this.cookbooksArchiveLocation = checkNotNull(cookbooksArchiveLocation, "cookbooksArchiveLocation");
@ -62,30 +66,64 @@ public class ChefSolo implements Statement {
return this;
}
public Builder defineRole(Role role) {
this.roles.add(checkNotNull(role, "role"));
return this;
}
public Builder defineRoles(Iterable<Role> roles) {
this.roles = ImmutableList.<Role> copyOf(checkNotNull(roles, "roles"));
return this;
}
public Builder installRecipe(String recipe) {
this.recipes.add(checkNotNull(recipe, "recipe"));
this.runlist.add("recipe[" + checkNotNull(recipe, "recipe") + "]");
return this;
}
public Builder installRecipes(Iterable<String> recipes) {
this.recipes = ImmutableList.<String> copyOf(checkNotNull(recipes, "recipes"));
this.runlist.addAll(Lists.newArrayList(transform(checkNotNull(recipes, "recipes"),
new Function<String, String>() {
@Override
public String apply(String input) {
return "recipe[" + input + "]";
}
})));
return this;
}
public Builder installRole(String role) {
this.runlist.add("role[" + checkNotNull(role, "role") + "]");
return this;
}
public Builder installRoles(Iterable<String> roles) {
this.runlist.addAll(Lists.newArrayList(transform(checkNotNull(roles, "roles"), new Function<String, String>() {
@Override
public String apply(String input) {
return "role[" + input + "]";
}
})));
return this;
}
public ChefSolo build() {
return new ChefSolo(cookbooksArchiveLocation, recipes, Optional.fromNullable(jsonAttributes));
return new ChefSolo(cookbooksArchiveLocation, Optional.fromNullable(jsonAttributes), roles, runlist);
}
}
private String cookbooksArchiveLocation;
private Optional<String> jsonAttributes;
private List<String> recipes;
private List<Role> roles;
private List<String> runlist;
private final InstallChefGems installChefGems = new InstallChefGems();
public ChefSolo(String cookbooksArchiveLocation, List<String> recipes, Optional<String> jsonAttributes) {
public ChefSolo(String cookbooksArchiveLocation, Optional<String> jsonAttributes, List<Role> roles,
List<String> runlist) {
this.cookbooksArchiveLocation = checkNotNull(cookbooksArchiveLocation, "cookbooksArchiveLocation must be set");
this.recipes = ImmutableList.copyOf(checkNotNull(recipes, "recipes must be set"));
this.roles = ImmutableList.copyOf(checkNotNull(roles, "roles must be set"));
this.runlist = ImmutableList.copyOf(checkNotNull(runlist, "runlist must be set"));
this.jsonAttributes = checkNotNull(jsonAttributes, "jsonAttributes must be set");
}
@ -97,23 +135,31 @@ public class ChefSolo implements Statement {
ImmutableList.Builder<Statement> statements = ImmutableList.builder();
statements.add(installChefGems);
statements.add(exec("{md} /var/chef"));
// The roles directory must contain one file for each role definition
if (!roles.isEmpty()) {
statements.add(exec("{md} /var/chef/roles"));
for (Role role : roles) {
statements.add(createOrOverwriteFile("/var/chef/roles/" + role.getName() + ".json",
ImmutableSet.of(role.toJsonString())));
}
}
ImmutableMap.Builder<String, String> chefSoloOptions = ImmutableMap.builder();
chefSoloOptions.put("-N", "`hostname`");
chefSoloOptions.put("-r", cookbooksArchiveLocation);
if (jsonAttributes.isPresent()) {
statements.add(exec("{md} /var/chef"));
statements.add(createOrOverwriteFile("/var/chef/node.json", jsonAttributes.asSet()));
chefSoloOptions.put("-j", "/var/chef/node.json");
}
if (!recipes.isEmpty()) {
chefSoloOptions.put("-o", recipesToRunlistString(recipes));
if (!runlist.isEmpty()) {
chefSoloOptions.put("-o", Joiner.on(',').join(runlist));
}
String options = Joiner.on(' ').withKeyValueSeparator(" ").join(chefSoloOptions.build());
statements.add(Statements.exec(String.format("chef-solo %s", options)));
return new StatementList(statements.build()).render(family);
@ -124,13 +170,4 @@ public class ChefSolo implements Statement {
return installChefGems.functionDependencies(family);
}
private static String recipesToRunlistString(List<String> recipes) {
return Joiner.on(',').join(transform(recipes, new Function<String, String>() {
@Override
public String apply(String input) {
return "recipe[" + input + "]";
}
}));
}
}

View File

@ -0,0 +1,115 @@
/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds 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.scriptbuilder.domain.chef;
import static org.testng.Assert.assertEquals;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
/**
* Unit tests for the {@link Role} class.
*
* @author Ignasi Barrera
*/
@Test(groups = "unit", testName = "RoleTest")
public class RoleTest {
public void testToJsonStringWithOnlyName() {
Role role = Role.builder().name("foo").build();
assertEquals(role.toJsonString(), "{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},"
+ "\"override_attributes\":{},\"json_class\":\"Chef::Role\",\"chef_type\":\"role\",\"run_list\":[]}");
}
public void testToJsonStringWithDescription() {
Role role = Role.builder().name("foo").description("Foo role").build();
assertEquals(role.toJsonString(), "{\"name\": \"foo\",\"description\":\"Foo role\",\"default_attributes\":{},"
+ "\"override_attributes\":{},\"json_class\":\"Chef::Role\",\"chef_type\":\"role\",\"run_list\":[]}");
}
public void testToJsonStringWithDefaultAttributes() {
Role role = Role.builder().name("foo").jsonDefaultAttributes("{\"foo\":\"bar\"}").build();
assertEquals(role.toJsonString(),
"{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{\"foo\":\"bar\"},"
+ "\"override_attributes\":{},\"json_class\":\"Chef::Role\",\"chef_type\":\"role\","
+ "\"run_list\":[]}");
}
public void testToJsonStringWithOverrideAttributes() {
Role role = Role.builder().name("foo").jsonOverrideAttributes("{\"foo\":\"bar\"}").build();
assertEquals(role.toJsonString(), "{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},"
+ "\"override_attributes\":{\"foo\":\"bar\"},\"json_class\":\"Chef::Role\",\"chef_type\":\"role\","
+ "\"run_list\":[]}");
}
public void testToJsonStringWithSingleRecipe() {
Role role = Role.builder().name("foo").installRecipe("apache2").build();
assertEquals(role.toJsonString(),
"{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\",\"run_list\":[\"recipe[apache2]\"]}");
}
public void testToJsonStringWithMultipleRecipes() {
Role role = Role.builder().name("foo").installRecipe("apache2").installRecipe("git").build();
assertEquals(role.toJsonString(),
"{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\","
+ "\"run_list\":[\"recipe[apache2]\",\"recipe[git]\"]}");
}
public void testToJsonStringWithMultipleRecipesInList() {
Role role = Role.builder().name("foo").installRecipes(ImmutableList.of("apache2", "git")).build();
assertEquals(role.toJsonString(),
"{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\","
+ "\"run_list\":[\"recipe[apache2]\",\"recipe[git]\"]}");
}
public void testToJsonStringWithSingleRole() {
Role role = Role.builder().name("foo").installRole("webserver").build();
assertEquals(role.toJsonString(),
"{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\",\"run_list\":[\"role[webserver]\"]}");
}
public void testToJsonStringWithMultipleRoles() {
Role role = Role.builder().name("foo").installRole("webserver").installRole("firewall").build();
assertEquals(role.toJsonString(),
"{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\","
+ "\"run_list\":[\"role[webserver]\",\"role[firewall]\"]}");
}
public void testToJsonStringWithMultipleRolesInList() {
Role role = Role.builder().name("foo").installRoles(ImmutableList.of("webserver", "firewall")).build();
assertEquals(role.toJsonString(),
"{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\","
+ "\"run_list\":[\"role[webserver]\",\"role[firewall]\"]}");
}
public void testToJsonStringWithRolesAndRecipes() {
Role role = Role.builder().name("foo").installRole("webserver").installRecipe("git").build();
assertEquals(role.toJsonString(),
"{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\","
+ "\"run_list\":[\"role[webserver]\",\"recipe[git]\"]}");
}
}

View File

@ -24,12 +24,11 @@ import java.io.IOException;
import org.jclouds.scriptbuilder.domain.OsFamily;
import org.jclouds.scriptbuilder.domain.ShellToken;
import org.jclouds.scriptbuilder.domain.chef.Role;
import org.testng.annotations.Test;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
@Test(groups = "unit", testName = "ChefSoloTest")
@ -40,16 +39,6 @@ public class ChefSoloTest {
ChefSolo.builder().build();
}
@Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "recipes must be set")
public void testChefSoloWithoutRecipes() {
new ChefSolo("/tmp/foo", null, Optional.<String> absent());
}
@Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "jsonAttributes must be set")
public void testChefSoloWithoutAttributes() {
new ChefSolo("/tmp/foo", Lists.<String> newArrayList(), null);
}
@Test(expectedExceptions = UnsupportedOperationException.class, expectedExceptionsMessageRegExp = "windows not yet implemented")
public void testChefSoloInWindows() {
ChefSolo.builder().cookbooksArchiveLocation("/tmp/cookbooks").build().render(OsFamily.WINDOWS);
@ -60,7 +49,8 @@ public class ChefSoloTest {
assertEquals(
script,
Resources.toString(Resources.getResource("test_install_ruby." + ShellToken.SH.to(OsFamily.UNIX)),
Charsets.UTF_8) + "installChefGems || return 1\nchef-solo -N `hostname` -r /tmp/cookbooks\n");
Charsets.UTF_8)
+ "installChefGems || return 1\nmkdir -p /var/chef\nchef-solo -N `hostname` -r /tmp/cookbooks\n");
}
public void testChefSoloWithCookbooksLocationAndSingleRecipe() throws IOException {
@ -70,7 +60,8 @@ public class ChefSoloTest {
script,
Resources.toString(Resources.getResource("test_install_ruby." + ShellToken.SH.to(OsFamily.UNIX)),
Charsets.UTF_8)
+ "installChefGems || return 1\nchef-solo -N `hostname` -r /tmp/cookbooks -o recipe[apache2]\n");
+ "installChefGems || return 1\nmkdir -p /var/chef\n"
+ "chef-solo -N `hostname` -r /tmp/cookbooks -o recipe[apache2]\n");
}
public void testChefSoloWithCookbooksLocationAndMultipleRecipes() throws IOException {
@ -80,7 +71,8 @@ public class ChefSoloTest {
script,
Resources.toString(Resources.getResource("test_install_ruby." + ShellToken.SH.to(OsFamily.UNIX)),
Charsets.UTF_8)
+ "installChefGems || return 1\nchef-solo -N `hostname` -r /tmp/cookbooks -o recipe[apache2],recipe[mysql]\n");
+ "installChefGems || return 1\nmkdir -p /var/chef\n"
+ "chef-solo -N `hostname` -r /tmp/cookbooks -o recipe[apache2],recipe[mysql]\n");
}
public void testChefSoloWithCookbooksLocationAndMultipleRecipesInList() throws IOException {
@ -90,7 +82,8 @@ public class ChefSoloTest {
script,
Resources.toString(Resources.getResource("test_install_ruby." + ShellToken.SH.to(OsFamily.UNIX)),
Charsets.UTF_8)
+ "installChefGems || return 1\nchef-solo -N `hostname` -r /tmp/cookbooks -o recipe[apache2],recipe[mysql]\n");
+ "installChefGems || return 1\nmkdir -p /var/chef\n"
+ "chef-solo -N `hostname` -r /tmp/cookbooks -o recipe[apache2],recipe[mysql]\n");
}
public void testChefSoloWithCookbooksLocationAndAttributes() throws IOException {
@ -106,4 +99,48 @@ public class ChefSoloTest {
+ "chef-solo -N `hostname` -r /tmp/cookbooks -j /var/chef/node.json -o recipe[apache2]\n");
}
public void testChefSoloWithRoleDefinitionAndRecipe() throws IOException {
Role role = Role.builder().name("foo").installRecipe("apache2").build();
String script = ChefSolo.builder().cookbooksArchiveLocation("/tmp/cookbooks").defineRole(role)
.installRecipe("apache2").build().render(OsFamily.UNIX);
assertEquals(
script,
Resources.toString(Resources.getResource("test_install_ruby." + ShellToken.SH.to(OsFamily.UNIX)),
Charsets.UTF_8)
+ "installChefGems || return 1\nmkdir -p /var/chef\nmkdir -p /var/chef/roles\n"
+ "cat > /var/chef/roles/foo.json <<-'END_OF_JCLOUDS_FILE'\n"
+ "\t{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\",\"run_list\":[\"recipe[apache2]\"]}"
+ "\nEND_OF_JCLOUDS_FILE\nchef-solo -N `hostname` -r /tmp/cookbooks -o recipe[apache2]\n");
}
public void testChefSoloWithRoleDefinitionAndRole() throws IOException {
Role role = Role.builder().name("foo").installRecipe("apache2").build();
String script = ChefSolo.builder().cookbooksArchiveLocation("/tmp/cookbooks").defineRole(role).installRole("foo")
.build().render(OsFamily.UNIX);
assertEquals(
script,
Resources.toString(Resources.getResource("test_install_ruby." + ShellToken.SH.to(OsFamily.UNIX)),
Charsets.UTF_8)
+ "installChefGems || return 1\nmkdir -p /var/chef\nmkdir -p /var/chef/roles\n"
+ "cat > /var/chef/roles/foo.json <<-'END_OF_JCLOUDS_FILE'\n"
+ "\t{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\",\"run_list\":[\"recipe[apache2]\"]}"
+ "\nEND_OF_JCLOUDS_FILE\nchef-solo -N `hostname` -r /tmp/cookbooks -o role[foo]\n");
}
public void testChefSoloWithRoleDefinitionAndRoleAndRecipe() throws IOException {
Role role = Role.builder().name("foo").installRecipe("apache2").build();
String script = ChefSolo.builder().cookbooksArchiveLocation("/tmp/cookbooks").defineRole(role).installRole("foo")
.installRecipe("git").build().render(OsFamily.UNIX);
assertEquals(
script,
Resources.toString(Resources.getResource("test_install_ruby." + ShellToken.SH.to(OsFamily.UNIX)),
Charsets.UTF_8)
+ "installChefGems || return 1\nmkdir -p /var/chef\nmkdir -p /var/chef/roles\n"
+ "cat > /var/chef/roles/foo.json <<-'END_OF_JCLOUDS_FILE'\n"
+ "\t{\"name\": \"foo\",\"description\":\"\",\"default_attributes\":{},\"override_attributes\":{},"
+ "\"json_class\":\"Chef::Role\",\"chef_type\":\"role\",\"run_list\":[\"recipe[apache2]\"]}"
+ "\nEND_OF_JCLOUDS_FILE\nchef-solo -N `hostname` -r /tmp/cookbooks -o role[foo],recipe[git]\n");
}
}