diff --git a/builtin/bins/provider-chef/main.go b/builtin/bins/provider-chef/main.go new file mode 100644 index 000000000000..b1bd8b537ef7 --- /dev/null +++ b/builtin/bins/provider-chef/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/chef" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: chef.Provider, + }) +} diff --git a/builtin/providers/chef/provider.go b/builtin/providers/chef/provider.go new file mode 100644 index 000000000000..7a04b977583e --- /dev/null +++ b/builtin/providers/chef/provider.go @@ -0,0 +1,112 @@ +package chef + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + + chefc "github.com/go-chef/chef" +) + +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "server_url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("CHEF_SERVER_URL", nil), + Description: "URL of the root of the target Chef server or organization.", + }, + "client_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("CHEF_CLIENT_NAME", nil), + Description: "Name of a registered client within the Chef server.", + }, + "private_key_pem": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: providerPrivateKeyEnvDefault, + Description: "PEM-formatted private key for client authentication.", + }, + "allow_unverified_ssl": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Description: "If set, the Chef client will permit unverifiable SSL certificates.", + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + //"chef_acl": resourceChefAcl(), + //"chef_client": resourceChefClient(), + //"chef_cookbook": resourceChefCookbook(), + "chef_data_bag": resourceChefDataBag(), + "chef_data_bag_item": resourceChefDataBagItem(), + "chef_environment": resourceChefEnvironment(), + "chef_node": resourceChefNode(), + "chef_role": resourceChefRole(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := &chefc.Config{ + Name: d.Get("client_name").(string), + Key: d.Get("private_key_pem").(string), + BaseURL: d.Get("server_url").(string), + SkipSSL: d.Get("allow_unverified_ssl").(bool), + Timeout: 10 * time.Second, + } + + return chefc.NewClient(config) +} + +func providerPrivateKeyEnvDefault() (interface{}, error) { + if fn := os.Getenv("CHEF_PRIVATE_KEY_FILE"); fn != "" { + contents, err := ioutil.ReadFile(fn) + if err != nil { + return nil, err + } + return string(contents), nil + } + + return nil, nil +} + +func jsonStateFunc(value interface{}) string { + // Parse and re-stringify the JSON to make sure it's always kept + // in a normalized form. + in, ok := value.(string) + if !ok { + return "null" + } + var tmp map[string]interface{} + + // Assuming the value must be valid JSON since it passed okay through + // our prepareDataBagItemContent function earlier. + json.Unmarshal([]byte(in), &tmp) + + jsonValue, _ := json.Marshal(&tmp) + return string(jsonValue) +} + +func runListEntryStateFunc(value interface{}) string { + // Recipes in run lists can either be naked, like "foo", or can + // be explicitly qualified as "recipe[foo]". Whichever form we use, + // the server will always normalize to the explicit form, + // so we'll normalize too and then we won't generate unnecessary + // diffs when we refresh. + in := value.(string) + if !strings.Contains(in, "[") { + return fmt.Sprintf("recipe[%s]", in) + } + return in +} diff --git a/builtin/providers/chef/provider_test.go b/builtin/providers/chef/provider_test.go new file mode 100644 index 000000000000..1d12945f4604 --- /dev/null +++ b/builtin/providers/chef/provider_test.go @@ -0,0 +1,62 @@ +package chef + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// To run these acceptance tests, you will need access to a Chef server. +// An easy way to get one is to sign up for a hosted Chef server account +// at https://manage.chef.io/signup , after which your base URL will +// be something like https://api.opscode.com/organizations/example/ . +// You will also need to create a "client" and write its private key to +// a file somewhere. +// +// You can then set the following environment variables to make these +// tests work: +// CHEF_SERVER_URL to the base URL as described above. +// CHEF_CLIENT_NAME to the name of the client object you created. +// CHEF_PRIVATE_KEY_FILE to the path to the private key file you created. +// +// You will probably need to edit the global permissions on your Chef +// Server account to allow this client (or all clients, if you're lazy) +// to have both List and Create access on all types of object: +// https://manage.chef.io/organizations/saymedia/global_permissions +// +// With all of that done, you can run like this: +// make testacc TEST=./builtin/providers/chef + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "chef": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("CHEF_SERVER_URL"); v == "" { + t.Fatal("CHEF_SERVER_URL must be set for acceptance tests") + } + if v := os.Getenv("CHEF_CLIENT_NAME"); v == "" { + t.Fatal("CHEF_CLIENT_NAME must be set for acceptance tests") + } + if v := os.Getenv("CHEF_PRIVATE_KEY_FILE"); v == "" { + t.Fatal("CHEF_PRIVATE_KEY_FILE must be set for acceptance tests") + } +} diff --git a/builtin/providers/chef/resource_data_bag.go b/builtin/providers/chef/resource_data_bag.go new file mode 100644 index 000000000000..a9c08748cdc3 --- /dev/null +++ b/builtin/providers/chef/resource_data_bag.go @@ -0,0 +1,77 @@ +package chef + +import ( + "github.com/hashicorp/terraform/helper/schema" + + chefc "github.com/go-chef/chef" +) + +func resourceChefDataBag() *schema.Resource { + return &schema.Resource{ + Create: CreateDataBag, + Read: ReadDataBag, + Delete: DeleteDataBag, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "api_uri": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func CreateDataBag(d *schema.ResourceData, meta interface{}) error { + client := meta.(*chefc.Client) + + dataBag := &chefc.DataBag{ + Name: d.Get("name").(string), + } + + result, err := client.DataBags.Create(dataBag) + if err != nil { + return err + } + + d.SetId(dataBag.Name) + d.Set("api_uri", result.URI) + return nil +} + +func ReadDataBag(d *schema.ResourceData, meta interface{}) error { + client := meta.(*chefc.Client) + + // The Chef API provides no API to read a data bag's metadata, + // but we can try to read its items and use that as a proxy for + // whether it still exists. + + name := d.Id() + + _, err := client.DataBags.ListItems(name) + if err != nil { + if errRes, ok := err.(*chefc.ErrorResponse); ok { + if errRes.Response.StatusCode == 404 { + d.SetId("") + return nil + } + } + } + return err +} + +func DeleteDataBag(d *schema.ResourceData, meta interface{}) error { + client := meta.(*chefc.Client) + + name := d.Id() + + _, err := client.DataBags.Delete(name) + if err == nil { + d.SetId("") + } + return err +} diff --git a/builtin/providers/chef/resource_data_bag_item.go b/builtin/providers/chef/resource_data_bag_item.go new file mode 100644 index 000000000000..ff6f7ac67327 --- /dev/null +++ b/builtin/providers/chef/resource_data_bag_item.go @@ -0,0 +1,120 @@ +package chef + +import ( + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform/helper/schema" + + chefc "github.com/go-chef/chef" +) + +func resourceChefDataBagItem() *schema.Resource { + return &schema.Resource{ + Create: CreateDataBagItem, + Read: ReadDataBagItem, + Delete: DeleteDataBagItem, + + Schema: map[string]*schema.Schema{ + "data_bag_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "content_json": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: jsonStateFunc, + }, + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func CreateDataBagItem(d *schema.ResourceData, meta interface{}) error { + client := meta.(*chefc.Client) + + dataBagName := d.Get("data_bag_name").(string) + itemId, itemContent, err := prepareDataBagItemContent(d.Get("content_json").(string)) + if err != nil { + return err + } + + err = client.DataBags.CreateItem(dataBagName, itemContent) + if err != nil { + return err + } + + d.SetId(itemId) + d.Set("id", itemId) + return nil +} + +func ReadDataBagItem(d *schema.ResourceData, meta interface{}) error { + client := meta.(*chefc.Client) + + // The Chef API provides no API to read a data bag's metadata, + // but we can try to read its items and use that as a proxy for + // whether it still exists. + + itemId := d.Id() + dataBagName := d.Get("data_bag_name").(string) + + value, err := client.DataBags.GetItem(dataBagName, itemId) + if err != nil { + if errRes, ok := err.(*chefc.ErrorResponse); ok { + if errRes.Response.StatusCode == 404 { + d.SetId("") + return nil + } + } else { + return err + } + } + + jsonContent, err := json.Marshal(value) + if err != nil { + return err + } + + d.Set("content_json", string(jsonContent)) + + return nil +} + +func DeleteDataBagItem(d *schema.ResourceData, meta interface{}) error { + client := meta.(*chefc.Client) + + itemId := d.Id() + dataBagName := d.Get("data_bag_name").(string) + + err := client.DataBags.DeleteItem(dataBagName, itemId) + if err == nil { + d.SetId("") + d.Set("id", "") + } + return err +} + +func prepareDataBagItemContent(contentJson string) (string, interface{}, error) { + var value map[string]interface{} + err := json.Unmarshal([]byte(contentJson), &value) + if err != nil { + return "", nil, err + } + + var itemId string + if itemIdI, ok := value["id"]; ok { + itemId, _ = itemIdI.(string) + } + + if itemId == "" { + return "", nil, fmt.Errorf("content_json must have id attribute, set to a string") + } + + return itemId, value, nil +} diff --git a/builtin/providers/chef/resource_data_bag_item_test.go b/builtin/providers/chef/resource_data_bag_item_test.go new file mode 100644 index 000000000000..9630d8b6c878 --- /dev/null +++ b/builtin/providers/chef/resource_data_bag_item_test.go @@ -0,0 +1,95 @@ +package chef + +import ( + "fmt" + "reflect" + "testing" + + chefc "github.com/go-chef/chef" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDataBagItem_basic(t *testing.T) { + var dataBagItemName string + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccDataBagItemCheckDestroy(dataBagItemName), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDataBagItemConfig_basic, + Check: testAccDataBagItemCheck( + "chef_data_bag_item.test", &dataBagItemName, + ), + }, + }, + }) +} + +func testAccDataBagItemCheck(rn string, name *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[rn] + if !ok { + return fmt.Errorf("resource not found: %s", rn) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("data bag item id not set") + } + + client := testAccProvider.Meta().(*chefc.Client) + content, err := client.DataBags.GetItem("terraform-acc-test-bag-item-basic", rs.Primary.ID) + if err != nil { + return fmt.Errorf("error getting data bag item: %s", err) + } + + expectedContent := map[string]interface{}{ + "id": "terraform_acc_test", + "something_else": true, + } + if !reflect.DeepEqual(content, expectedContent) { + return fmt.Errorf("wrong content: expected %#v, got %#v", expectedContent, content) + } + + if expected := "terraform_acc_test"; rs.Primary.Attributes["id"] != expected { + return fmt.Errorf("wrong id; expected %#v, got %#v", expected, rs.Primary.Attributes["id"]) + } + + *name = rs.Primary.ID + + return nil + } +} + +func testAccDataBagItemCheckDestroy(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*chefc.Client) + _, err := client.DataBags.GetItem("terraform-acc-test-bag-item-basic", name) + if err == nil { + return fmt.Errorf("data bag item still exists") + } + if _, ok := err.(*chefc.ErrorResponse); err != nil && !ok { + return fmt.Errorf("got something other than an HTTP error (%v) when getting data bag item", err) + } + + return nil + } +} + +const testAccDataBagItemConfig_basic = ` +resource "chef_data_bag" "test" { + name = "terraform-acc-test-bag-item-basic" +} +resource "chef_data_bag_item" "test" { + data_bag_name = "terraform-acc-test-bag-item-basic" + depends_on = ["chef_data_bag.test"] + content_json = < + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> + <% end %> diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index ff088e8e8cbe..3deb5be98f1f 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -133,6 +133,10 @@ Azure + > + Chef + + > CloudFlare