diff --git a/builtin/bins/provider-random/main.go b/builtin/bins/provider-random/main.go new file mode 100644 index 000000000..f83de47dc --- /dev/null +++ b/builtin/bins/provider-random/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/random" + "github.com/hashicorp/terraform/plugin" + "github.com/hashicorp/terraform/terraform" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: func() terraform.ResourceProvider { + return random.Provider() + }, + }) +} diff --git a/builtin/bins/provider-random/main_test.go b/builtin/bins/provider-random/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-random/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/random/provider.go b/builtin/providers/random/provider.go new file mode 100644 index 000000000..309c50d6c --- /dev/null +++ b/builtin/providers/random/provider.go @@ -0,0 +1,31 @@ +package random + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{}, + + ResourcesMap: map[string]*schema.Resource{ + "random_id": resourceId(), + "random_shuffle": resourceShuffle(), + }, + } +} + +// stubRead is a do-nothing Read implementation used for our resources, +// which don't actually need to do anything on read. +func stubRead(d *schema.ResourceData, meta interface{}) error { + return nil +} + +// stubDelete is a do-nothing Dete implementation used for our resources, +// which don't actually need to do anything unusual on delete. +func stubDelete(d *schema.ResourceData, meta interface{}) error { + d.SetId("") + return nil +} diff --git a/builtin/providers/random/provider_test.go b/builtin/providers/random/provider_test.go new file mode 100644 index 000000000..92d16c509 --- /dev/null +++ b/builtin/providers/random/provider_test.go @@ -0,0 +1,31 @@ +package random + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "random": 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) { +} diff --git a/builtin/providers/random/resource_id.go b/builtin/providers/random/resource_id.go new file mode 100644 index 000000000..9bb36b9ef --- /dev/null +++ b/builtin/providers/random/resource_id.go @@ -0,0 +1,76 @@ +package random + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "math/big" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceId() *schema.Resource { + return &schema.Resource{ + Create: CreateID, + Read: stubRead, + Delete: stubDelete, + + Schema: map[string]*schema.Schema{ + "keepers": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + + "byte_length": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "b64": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "hex": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "dec": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func CreateID(d *schema.ResourceData, meta interface{}) error { + + byteLength := d.Get("byte_length").(int) + bytes := make([]byte, byteLength) + + n, err := rand.Reader.Read(bytes) + if n != byteLength { + return fmt.Errorf("generated insufficient random bytes") + } + if err != nil { + return fmt.Errorf("error generating random bytes: %s", err) + } + + b64Str := base64.RawURLEncoding.EncodeToString(bytes) + hexStr := hex.EncodeToString(bytes) + + int := big.Int{} + int.SetBytes(bytes) + decStr := int.String() + + d.SetId(b64Str) + d.Set("b64", b64Str) + d.Set("hex", hexStr) + d.Set("dec", decStr) + + return nil +} diff --git a/builtin/providers/random/resource_id_test.go b/builtin/providers/random/resource_id_test.go new file mode 100644 index 000000000..ed6b8af8d --- /dev/null +++ b/builtin/providers/random/resource_id_test.go @@ -0,0 +1,58 @@ +package random + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccResourceID(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccResourceIDConfig, + Check: resource.ComposeTestCheckFunc( + testAccResourceIDCheck("random_id.foo"), + ), + }, + }, + }) +} + +func testAccResourceIDCheck(id string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[id] + if !ok { + return fmt.Errorf("Not found: %s", id) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + b64Str := rs.Primary.Attributes["b64"] + hexStr := rs.Primary.Attributes["hex"] + decStr := rs.Primary.Attributes["dec"] + + if got, want := len(b64Str), 6; got != want { + return fmt.Errorf("base64 string length is %d; want %d", got, want) + } + if got, want := len(hexStr), 8; got != want { + return fmt.Errorf("hex string length is %d; want %d", got, want) + } + if len(decStr) < 1 { + return fmt.Errorf("decimal string is empty; want at least one digit") + } + + return nil + } +} + +const testAccResourceIDConfig = ` +resource "random_id" "foo" { + byte_length = 4 +} +` diff --git a/builtin/providers/random/resource_shuffle.go b/builtin/providers/random/resource_shuffle.go new file mode 100644 index 000000000..31108aeee --- /dev/null +++ b/builtin/providers/random/resource_shuffle.go @@ -0,0 +1,82 @@ +package random + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceShuffle() *schema.Resource { + return &schema.Resource{ + Create: CreateShuffle, + Read: stubRead, + Delete: stubDelete, + + Schema: map[string]*schema.Schema{ + "keepers": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + + "seed": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "input": &schema.Schema{ + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "result": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "result_count": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func CreateShuffle(d *schema.ResourceData, meta interface{}) error { + input := d.Get("input").([]interface{}) + seed := d.Get("seed").(string) + + resultCount := d.Get("result_count").(int) + if resultCount == 0 { + resultCount = len(input) + } + result := make([]interface{}, 0, resultCount) + + rand := NewRand(seed) + + // Keep producing permutations until we fill our result +Batches: + for { + perm := rand.Perm(len(input)) + + for _, i := range perm { + result = append(result, input[i]) + + if len(result) >= resultCount { + break Batches + } + } + } + + d.SetId("-") + d.Set("result", result) + + return nil +} diff --git a/builtin/providers/random/resource_shuffle_test.go b/builtin/providers/random/resource_shuffle_test.go new file mode 100644 index 000000000..5770e4105 --- /dev/null +++ b/builtin/providers/random/resource_shuffle_test.go @@ -0,0 +1,91 @@ +package random + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccResourceShuffle(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccResourceShuffleConfig, + Check: resource.ComposeTestCheckFunc( + // These results are current as of Go 1.6. The Go + // "rand" package does not guarantee that the random + // number generator will generate the same results + // forever, but the maintainers endeavor not to change + // it gratuitously. + // These tests allow us to detect such changes and + // document them when they arise, but the docs for this + // resource specifically warn that results are not + // guaranteed consistent across Terraform releases. + testAccResourceShuffleCheck( + "random_shuffle.default_length", + []string{"a", "c", "b", "e", "d"}, + ), + testAccResourceShuffleCheck( + "random_shuffle.shorter_length", + []string{"a", "c", "b"}, + ), + testAccResourceShuffleCheck( + "random_shuffle.longer_length", + []string{"a", "c", "b", "e", "d", "a", "e", "d", "c", "b", "a", "b"}, + ), + ), + }, + }, + }) +} + +func testAccResourceShuffleCheck(id string, wants []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[id] + if !ok { + return fmt.Errorf("Not found: %s", id) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + attrs := rs.Primary.Attributes + + gotLen := attrs["result.#"] + wantLen := strconv.Itoa(len(wants)) + if gotLen != wantLen { + return fmt.Errorf("got %s result items; want %s", gotLen, wantLen) + } + + for i, want := range wants { + key := fmt.Sprintf("result.%d", i) + if got := attrs[key]; got != want { + return fmt.Errorf("index %d is %q; want %q", i, got, want) + } + } + + return nil + } +} + +const testAccResourceShuffleConfig = ` +resource "random_shuffle" "default_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" +} +resource "random_shuffle" "shorter_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" + result_count = 3 +} +resource "random_shuffle" "longer_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" + result_count = 12 +} +` diff --git a/builtin/providers/random/seed.go b/builtin/providers/random/seed.go new file mode 100644 index 000000000..7d16322fd --- /dev/null +++ b/builtin/providers/random/seed.go @@ -0,0 +1,24 @@ +package random + +import ( + "hash/crc64" + "math/rand" + "time" +) + +// NewRand returns a seeded random number generator, using a seed derived +// from the provided string. +// +// If the seed string is empty, the current time is used as a seed. +func NewRand(seed string) *rand.Rand { + var seedInt int64 + if seed != "" { + crcTable := crc64.MakeTable(crc64.ISO) + seedInt = int64(crc64.Checksum([]byte(seed), crcTable)) + } else { + seedInt = time.Now().Unix() + } + + randSource := rand.NewSource(seedInt) + return rand.New(randSource) +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index ada3a4373..7e9b246c8 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -36,6 +36,7 @@ import ( packetprovider "github.com/hashicorp/terraform/builtin/providers/packet" postgresqlprovider "github.com/hashicorp/terraform/builtin/providers/postgresql" powerdnsprovider "github.com/hashicorp/terraform/builtin/providers/powerdns" + randomprovider "github.com/hashicorp/terraform/builtin/providers/random" rundeckprovider "github.com/hashicorp/terraform/builtin/providers/rundeck" softlayerprovider "github.com/hashicorp/terraform/builtin/providers/softlayer" statuscakeprovider "github.com/hashicorp/terraform/builtin/providers/statuscake" @@ -87,6 +88,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "packet": packetprovider.Provider, "postgresql": postgresqlprovider.Provider, "powerdns": powerdnsprovider.Provider, + "random": randomprovider.Provider, "rundeck": rundeckprovider.Provider, "softlayer": softlayerprovider.Provider, "statuscake": statuscakeprovider.Provider, diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 4a1a6319f..ef7bf7f69 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -36,6 +36,7 @@ body.layout-openstack, body.layout-packet, body.layout-postgresql, body.layout-powerdns, +body.layout-random, body.layout-rundeck, body.layout-statuscake, body.layout-softlayer, diff --git a/website/source/docs/providers/random/index.html.markdown b/website/source/docs/providers/random/index.html.markdown new file mode 100644 index 000000000..32bf038d8 --- /dev/null +++ b/website/source/docs/providers/random/index.html.markdown @@ -0,0 +1,73 @@ +--- +layout: "random" +page_title: "Provider: Random" +sidebar_current: "docs-random-index" +description: |- + The Random provider is used to generate randomness. +--- + +# Random Provider + +The "random" provider allows the use of randomness within Terraform +configurations. This is a *logical provider*, which means that it works +entirely within Terraform's logic, and doesn't interact with any other +services. + +Unconstrained randomness within a Terraform configuration would not be very +useful, since Terraform's goal is to converge on a fixed configuration by +applying a diff. Because of this, the "random" provider provides an idea of +*managed randomness*: it provides resources that generate random values during +their creation and then hold those values steady until the inputs are changed. + +Even with these resources, it is advisable to keep the use of randomness within +Terraform configuration to a minimum, and retain it for special cases only; +Terraform works best when the configuration is well-defined, since its behavior +can then be more readily predicted. + +Unless otherwise stated within the documentation of a specific resource, this +provider's results are **not** sufficiently random for cryptographic use. + +For more information on the specific resources available, see the links in the +navigation bar. Read on for information on the general patterns that apply +to this provider's resources. + +## Resource "Keepers" + +As noted above, the random resources generate randomness only when they are +created; the results produced are stored in the Terraform state and re-used +until the inputs change, prompting the resource to be recreated. + +The resources all provide a map argument called `keepers` that can be populated +with arbitrary key/value pairs that should be selected such that they remain +the same until new random values are desired. + +For example: + +``` +resource "random_id" "server" { + keepers = { + # Generate a new id each time we switch to a new AMI id + ami_id = "${var.ami_id}" + } + + byte_length = 8 +} + +resource "aws_instance" "server" { + tags = { + Name = "web-server ${random_id.server.hex}" + } + + # Read the AMI id "through" the random_id resource to ensure that + # both will change together. + ami = "${random_id.server.keepers.ami_id}" + + # ... (other aws_instance arguments) ... +} +``` + +Resource "keepers" are optional. The other arguments to each resource must +*also* remain constant in order to retain a random result. + +To force a random result to be replaced, the `taint` command can be used to +produce a new result on the next run. diff --git a/website/source/docs/providers/random/r/id.html.md b/website/source/docs/providers/random/r/id.html.md new file mode 100644 index 000000000..2fdcdd4a4 --- /dev/null +++ b/website/source/docs/providers/random/r/id.html.md @@ -0,0 +1,69 @@ +--- +layout: "random" +page_title: "Random: random_id" +sidebar_current: "docs-random-resource-id" +description: |- + Generates a random identifier. +--- + +# random\_id + +The resource `random_id` generates random numbers that are intended to be +used as unique identifiers for other resources. + +Unlike other resources in the "random" provider, this resource *does* use a +cryptographic random number generator in order to minimize the chance of +collisions, making the results of this resource when a 32-byte identifier +is requested of equivalent uniqueness to a type-4 UUID. + +This resource can be used in conjunction with resources that have, +the `create_before_destroy` lifecycle flag set, to avoid conflicts with +unique names during the brief period where both the old and new resources +exist concurrently. + +## Example Usage + +The following example shows how to generate a unique name for an AWS EC2 +instance that changes each time a new AMI id is selected. + +``` +resource "random_id" "server" { + keepers = { + # Generate a new id each time we switch to a new AMI id + ami_id = "${var.ami_id}" + } + + byte_length = 8 +} + +resource "aws_instance" "server" { + tags = { + Name = "web-server ${random_id.server.hex}" + } + + # Read the AMI id "through" the random_id resource to ensure that + # both will change together. + ami = "${random_id.server.keepers.ami_id}" + + # ... (other aws_instance arguments) ... +} +``` + +## Argument Reference + +The following arguments are supported: + +* `byte_length` - (Required) The number of random bytes to produce. The + minimum value is 1, which produces eight bits of randomness. + +* `keepers` - (Optional) Arbitrary map of values that, when changed, will + trigger a new id to be generated. See + [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +The following attributes are exported: + +* `b64` - The generated id presented in base64, using the URL-friendly character set: case-sensitive letters, digits and the characters `_` and `-`. +* `hex` - The generated id presented in padded hexadecimal digits. This result will always be twice as long as the requested byte length. +* `decimal` - The generated id presented in non-padded decimal digits. diff --git a/website/source/docs/providers/random/r/shuffle.html.md b/website/source/docs/providers/random/r/shuffle.html.md new file mode 100644 index 000000000..bfae65bca --- /dev/null +++ b/website/source/docs/providers/random/r/shuffle.html.md @@ -0,0 +1,59 @@ +--- +layout: "random" +page_title: "Random: random_shuffle" +sidebar_current: "docs-random-resource-shuffle" +description: |- + Produces a random permutation of a given list. +--- + +# random\_shuffle + +The resource `random_shuffle` generates a random permutation of a list +of strings given as an argument. + +## Example Usage + +``` +resource "random_shuffle" "az" { + input = ["us-west-1a", "us-west-1c", "us-west-1d", "us-west-1e"] + result_count = 2 +} + +resource "aws_elb" "example" { + # Place the ELB in any two of the given availability zones, selected + # at random. + availability_zones = ["${random_shuffle.az.result}"] + + # ... and other aws_elb arguments ... +} +``` + +## Argument Reference + +The following arguments are supported: + +* `input` - (Required) The list of strings to shuffle. + +* `result_count` - (Optional) The number of results to return. Defaults to + the number of items in the `input` list. If fewer items are requested, + some elements will be excluded from the result. If more items are requested, + items will be repeated in the result but not more frequently than the number + of items in the input list. + +* `keepers` - (Optional) Arbitrary map of values that, when changed, will + trigger a new id to be generated. See + [the main provider documentation](../index.html) for more information. + +* `seed` - (Optional) Arbitrary string with which to seed the random number + generator, in order to produce less-volatile permutations of the list. + **Important:** Even with an identical seed, it is not guaranteed that the + same permutation will be produced across different versions of Terraform. + This argument causes the result to be *less volatile*, but not fixed for + all time. + +## Attributes Reference + +The following attributes are exported: + +* `result` - Random permutation of the list of strings given in `input`. + diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 93dac5b79..8bfb8e830 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -278,6 +278,10 @@ PowerDNS + > + Random + + > Rundeck diff --git a/website/source/layouts/random.erb b/website/source/layouts/random.erb new file mode 100644 index 000000000..224559c85 --- /dev/null +++ b/website/source/layouts/random.erb @@ -0,0 +1,29 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> +<% end %>