Skip to content

Commit

Permalink
Merge #3084: Chef provider
Browse files Browse the repository at this point in the history
  • Loading branch information
apparentlymart committed Dec 13, 2015
2 parents e9d152d + 764ea7f commit 86882e3
Show file tree
Hide file tree
Showing 23 changed files with 1,829 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
FEATURES:
* **New provider: `vcd` - VMware vCloud Director** [GH-3785]
* **New provider: `postgresql` - Create PostgreSQL databases and roles** [GH-3653]
* **New provider: `chef` - Create chef environments, roles, etc** [GH-3084]
* **New resource: `aws_autoscaling_schedule`** [GH-4256]
* **New resource: `google_pubsub_topic`** [GH-3671]
* **New resource: `google_pubsub_subscription`** [GH-3671]
Expand Down
12 changes: 12 additions & 0 deletions builtin/bins/provider-chef/main.go
Original file line number Diff line number Diff line change
@@ -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,
})
}
112 changes: 112 additions & 0 deletions builtin/providers/chef/provider.go
Original file line number Diff line number Diff line change
@@ -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
}
62 changes: 62 additions & 0 deletions builtin/providers/chef/provider_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
77 changes: 77 additions & 0 deletions builtin/providers/chef/resource_data_bag.go
Original file line number Diff line number Diff line change
@@ -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
}
120 changes: 120 additions & 0 deletions builtin/providers/chef/resource_data_bag_item.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 86882e3

Please sign in to comment.