Merge pull request #6672 from apparentlymart/random-provider

Logical Resources for Random Values
This commit is contained in:
James Nugent 2016-05-29 11:58:42 -07:00
commit 5a0f6565d3
16 changed files with 646 additions and 0 deletions

View File

@ -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()
},
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -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
}

View File

@ -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) {
}

View File

@ -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
}

View File

@ -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
}
`

View File

@ -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
}

View File

@ -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
}
`

View File

@ -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)
}

View File

@ -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,

View File

@ -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,

View File

@ -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.

View File

@ -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.

View File

@ -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`.

View File

@ -278,6 +278,10 @@
<a href="/docs/providers/powerdns/index.html">PowerDNS</a>
</li>
<li<%= sidebar_current("docs-providers-random") %>>
<a href="/docs/providers/random/index.html">Random</a>
</li>
<li<%= sidebar_current("docs-providers-rundeck") %>>
<a href="/docs/providers/rundeck/index.html">Rundeck</a>
</li>

View File

@ -0,0 +1,29 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/providers/index.html">&laquo; Documentation Home</a>
</li>
<li<%= sidebar_current("docs-random-index") %>>
<a href="/docs/providers/random/index.html">Random Provider</a>
</li>
<li<%= sidebar_current(/^docs-random-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-random-resource-id") %>>
<a href="/docs/providers/random/r/id.html">random_id</a>
</li>
<li<%= sidebar_current("docs-random-resource-shuffle") %>>
<a href="/docs/providers/random/r/shuffle.html">random_shuffle</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>